CSS で inner-text を条件にしたセレクタを書いてみたいなぁ

CSS の属性セレクタを使うと、「~~の文言を含む要素」みたいなのを指定できる。

p[data-example*="ほげ"] {
  color: #f00;
}
<p data-example="ほげほげ">コレは赤文字になる</p>
<p data-example="ふがふが">コレには適用されない</p>

属性セレクタの色々な書き方は以下のデモを参照。*= の他に ~= も覚えておくと便利かも。

また、:empty 擬似クラスを指定すると、一切の子ノードを持たない要素の指定もできる。:empty って内側の改行や空白文字も存在するとダメみたいなのでシビア。

p:empty {
  border: 10px solid #f00;
}
<!-- ↓ コレは赤い枠線が付く -->
<p></p>

<!-- ↓ コレは子要素があるので NG -->
<p><span></span></p>

<!-- ↓ コレは改行があるので NG -->
<p>
</p>

2022年7月時点ではまだ対応しているブラウザがほぼないけど、:has() が使えるようになると、「こういう子要素を持つ親要素」を指定できる。

.parent:has(.child) {
  color: #f00;
}
<div class="parent">
  <div class="child">この .parent は赤文字になる</div>
</div>

<div class="parent">
  この .parent は対象外
</div>

…さて、ココまでやってきて、innerText を条件にした CSS セレクタを書いてみたいなぁ、と思った次第。想像上のセレクタだけどこんなことがしたい。

/* 実際には存在しないけど、こんなセレクタを書けたらいいなー */
div:inner-text*="ほげ" {
  color: #f00;
}
<div>この div 要素内には「ほげ」の文字が出てくるので赤文字になる</div>

<div>この div 要素は対象外</div>

:inner-text でも :text-content でも良いんだけど、こういうの出来ないかね?

ブラウザの Stylus 拡張機能で、ブラウザごとにユーザスタイルシートを書く時なんかにこういうことができるとメッチャ便利なんだよなー。Greasemonkey みたいなユーザスクリプトを書くのって大変だし、DOM 監視とかを自分でやらないと DOM 増減にも対応できないよね。だから CSS セレクタで記述できてブラウザネイティブに解釈してくれたらいいのになーなんて思ってた次第。

調べてみると、前述の :has() については以下に Polyfill が存在する。

この中の browser-global.js で、:has() ではなく独自属性を HTML 側に書き込んで、属性セレクタでスタイルを当てる、そして DOM 要素の変更などを MutationObserver で監視してその都度スタイルが当たるように独自属性の書き込みをやり直している、という仕組みだった。以下で勉強のため動作確認している。

頑張れば JS でこうした Polyfill を作れると分かったのだが、何やかんや大変そうなので、まずは簡単にそれっぽいモノを実現できないかと思ってコードを書いてみた。


つーワケで、とりあえず動くようになったサンプル実装は以下。

現状は一旦、JS 側で全ての処理をするようにしている。こんな感じで呼び出す。

// div:inner-text*="ほげ" { color: #f00; }
// ↑ こういう風に書きたかったのを、↓ のように JS で実現した
cssInnerText('div', '*=', 'ほげ', 'color: #f00;');

第1引数は CSS セレクタを書く。document.querySelectorAll() にそのまま渡すのでそれに対応している記法なら使える。

第2引数部分は部分一致とか完全一致とかを指定できる。属性セレクタと同じ =^=$=*= が使える他、=i とか *= I とかいう風に書くとケースインセンシティブ (大文字・小文字を区別しない) になる。さらに :not(^=) というような Not 指定もできる。

第3引数はマッチさせたい innerText のテキストを指定する。

第4引数に指定したい CSS を書く。現状は Element.style.cssText に結合してスタイル適用しているので、color: #f00; border: 1px solid #00f; font-weight: bold; なんつって複数の CSS を連ねた文字列を渡してやっても良い。

他に、第2・第3引数のところを正規表現オブジェクトでも指定できるようにもしてある。

// 第2・第3引数部分を正規表現オブジェクトにしても動くようにした
cssInnerText('div > p', /ほげ/i, 'color: #f00');

やっていることは単純で、本質的なコードは以下に収束する。

document.querySelectorAll('CSS セレクタ').forEach((element) => {
  if(new RegExp('マッチさせたい言葉').test(element.innerText.trim())) {
    element.style.cssText += 'color: #f00; だとか付与したい CSS';
  }
});

なので、前述の css-has-pseudo Polyfill と比べると、:hover だとか ::before だとかそういった擬似クラス、擬似要素をスタイリングできないのが難点。

また MutationObserver なども用意していないので、DOM 要素が後から追加されたりすると上手くスタイル適用されない。再度 cssInnerText() を呼んでやらないといけない。


本当は、JS ライブラリを読み込みさえすれば div:inner-text="" なんて書いた CSS 部分を自動的に解釈して処理してくれるようにしたいのだが、そうすると実装が大変だ。

つーワケで色々面倒臭くなってしまったのでココまで…。誰かやる気のある人、続き実装してください…。もしくは CSS の新しい記法として :inner-text を作ってください…。