zenn-editor の 数式検出正規表現を解読する
やりたいことまとめ
- Markdown エディタで 数式を KaTeX にレンダリングさせるべく正規表現で
$~$
を抜き出している - 抜き出したくないケースもあり(HTTPリンクに埋め込まれているケースなど)やや複雑化してきている
- 意図しないマッチもISSUEにあげていただいたこともありこの機会に解読することに
当該箇所
{
name: 'math_inline',
rex: /\$((?:\S)|(?:\S(?!.*\]\(http.*\$.*\)).*?\S))\$/gy, // fixed so that the expression [$something](https://something.com/$example) is skipped. (?:\S(?!.*\]\(http.*\$.*\)) means somthing like "](https://hoge.com/$/hoge)"
tmpl: `<embed-katex><eq class="${katexClassName}">$1</eq></embed-katex>`,
tag: '$',
pre: preHandler,
post: postHandler,
},
ウゥメマイガ…
把握する
ビジュアライズ
regulexというサイトを使わせていただく。
- でかい OR がある。ひとつめは
\$(?:\S)\$
-
$~$
で囲まれた制御文字以外の文字列にマッチする - 意図が謎。任意の一文字を数式にレンダリングするためかな?
-
$a$
=> みたいな…a - どうやら一文字のときとそれ以外のときでマッチングを明確に分けたい意図があるっぽい
- おそらくだが、1文字のときはややこしい除外判定がいらないから、というのがありそう
- 1文字のときは除外判定の影響を受けないようにしたい、ということか
- 複数文字のほうはこの先もいろいろ判定が追加されていく可能性があるため
-
- ふたつめは
\$(?:\S(?!.*\]\(http.*\$.*\)).*?\S)\$
- なんとなくURLに含まれる
$
は無視したい意図はわかるが、少しずつ分解する -
(?!.*\]\(http.*\$.*\)
これ以外をマッチさせたい -
任意の文字列
](http
任意の文字列
$
)
- つまり、URLの中に
$
が含まれている場合、その塊はキャプチャしないようにしたい、となるほど
- なんとなくURLに含まれる
(?:pattern)
この記号の意味なんですか 置き換え、キャプチャパターンのひとつ。正規表現でキャプチャするとマッチした文字列を $0
や$1
といった変数で抽出できるが、そのやり方にバリエーションがある。ここを参考にさせていだきました。
また、JavaScript MDNにも乗っている。
非キャプチャグループ: x にマッチしますが、マッチした内容は記憶しません。マッチの部分文字列は、結果の配列の要素 ([1], ..., [n]) や、あらかじめ定義されている RegExp オブジェクトのプロパティ ($1, ..., $9) から呼び出すことはできません。.
(?:pattern)
は、パターンを含むマッチ文字列全体がひとつの変数に入る(=マッチ文字列が保持されない) という動きになります。文字で書いてもわからない。
/\$(?:\S*)\$/g.exec('それではこの数式を見てください。$a=b$')
["$a=b$"] // キャプチャ部分のマッチは破棄され、正規表現全体のマッチのみが保持される
/\$(?:\S*)\$/g.exec('それではこの数式を見てください。$a=b$')
["$a=b$", "a=b"] (2) // キャプチャ部分のマッチも、正規表現全体のマッチも保持される
つまり、キャプチャグループの中は特にいらなくて、今回の場合は数式全体 $~$
が欲しいので (?:)
を使っているということですね。
(?!pattern)
この記号の意味なんですか 置き換え、キャプチャパターンのひとつ。やはりここを参考にさせていだきました。
MDNでは 言明として説明がありました。
(?!:pattern)
は、パターンを含まない文字列全体がひとつの変数に入る という動きになります。文字で書いてもやっぱりわからない。
/\$(?!script).*\$/.exec('それではこの数式を見てください。$a=b$')
["$a=b$"] // script に一致する文字列はないので $~$ が抽出される
/\$(?!script).*\$/.exec('それではこの数式を見てください。$script:alert("a=b")$')
null // script に一致する文字列があるので抽出しない
例えば、上記のように危険な文字列、不利益な文字列などを含む場合はマッチさせないようにする、パターンをスキップする、といったケースで役に立ちそうですね。
問題になるケース、ならないケース
こちらのISSUEで教えていただいたケースを考察する。
問題がある
あああ$\omega$いいい『[uuu](https://arxiv.org)』えええ$(7)$おおお
👇
あああ
$\omega$
も数式になってほしいが、ならない
理由:
\omega$いいい『[uuu](https://arxiv.org)』えええ$(7)
- この部分が、
任意の文字列
](http
任意の文字列
$
)
にマッチしてしまっている - スキップ対象の文字列を検知したので、この
$~$
はキャプチャされない(数式変換対象にならない)
問題がない
あああ$a$いいい[uuu](https://arxiv.org)えええ$(7)$おおお
👇
あああ
理由:
-
$~$
で囲まれた任意の一文字は問答無用でキャプチャされるため意図に反してスキップされることがない
あああ$\omega$いいい『[uuu](https://arxiv.org)』えええ$(7$おお
👇
あああ
理由:
- これが決め手になった。
の閉じ括弧がないことで、(7 任意の文字列
](http
任意の文字列
$
)
これにマッチすることがない - ということでひとつめの
$\omega$
も正常に数式判定される
どうすれば良いか
URLに含まれる$
の検出を厳密にする。
スキップ対象: 任意の文字列
](http
任意の文字列
$
)
この部分の解釈が広がってしまったことで起きた。…そもそもURLに含まれる $
の検出が必要かしら?
この対応の後、
この対応があった。
理屈ではなく神託を待ったら
緩和策がでたかもしれない。
\$((?:\S)|(?:\S(?![^\$]*\]\(http.*).*?\S))\$
これならどうだろう。これをベースにテストを書いていく。
意味的なやつ:
-
$$
に挟まれる1文字 -
$$
に挟まれる文字列で、次のパターンを 持たないもの全体-
制御以外1文字
$以外の任意の文字列
](http
任意の文字列
- 先読み否定でパターンを持たないってのがややこしい。要するに **間に
](https
って文字列があるからきっと数式判定したくないものだと思うけど、URLぽいやつの前に$
記号があったらそれは$ ~ $ ](https $
というパターンにハマって意図せず広い範囲をスキップしてしまう可能性があるため、スキップする範囲を限定するため パターン中に$が含まれていたらスキップしない とした
-
- この措置によって、
$ ~ $ ](http $
の広い範囲にわたってスキップされることがなくなり、また(.*?)
の最小マッチにより、 個別の$~$
が意図どおりにマッチするようになる。
理解の助け
神託とはいったけど実際にはかなり調べた。食事中も上の空で家族にはスマンコトヲシタ。で、一番勘違い(というかわかってなかった)のが (?!Pattern)
部分の 先読み否定。ここを理解していないと挙動がわからない。
先読みは文字通り一致するパターンを調べるわけなんだけど、Pattern自体は全体でどういうマッチをさせようとしているかを把握していない。ので、自分は自分で最後まで調べようとする。例えば、
あああ$\omega$いいい『[uuu](https://arxiv.org)』えええ$(7)$
こういう文字列に対して text](https://
を含むパターンの場合は数式にマッチさせたくないな、というケース。コレに対して、
\$((?:\S)|(?:\S(?!.*\]\(http.*\$.*\)).*?\S))\$
ひとつめの数式がマッチしない。これは、除外パターンに $\omega$いいい『[uuu](https://arxiv.org)』えええ$(7)
が入っているから。 開始$
、終了$
で囲んでいるのにどうして、と思ったが、先に述べたとおりPatternはそのうしろでどのような正規表現があるかは気にしない。ので、http.*\$.*\)).*?\S)
ここの、「$
ではじまり、任意の文字列のあとhttpのあとに$
と閉じ括弧)
がある」というパターンに「$\omega$いいい『[uuu](https://arxiv.org)』えええ$(7)
」が入っていたというわけ。
残念ながら完璧にするのは不可能
このやり方では対処出来ないパターンもある。
$a=b$という数式はこちらを参照[text$text](https://...$text)$(7)$
なんかがそう。ただ、これは$
という、開始と終了が明確でない記号を使って数式を判定する限りパーフェクトは無理といえる。もし言語やDSLを設計するときは、開始と終了が明確なトークンを使うようにしような!
テスト用
こちらを拝借。
昔対応したパターン(数式にならなければOK)
[text$text](https://...$text
=> バグっぽい。修正前は数式になっちゃうはず
$text](https://...$
=> バグっぽい。修正前は数式になっちゃうはず
修正によって解消されてほしいパターン
オメガも数式になっていればOK。修正前は数式になっていないはず。
あああ
あああ
あああ
あああ
修正前も修正後も正しいパターン
デグレチェック用
あああ
あああ
あああ
あああ
あああ
あああ
あああ
あああ
あああ
残念だが、解消できないかもしれないパターン
と思ったけど、リンク部分がアンカーになるならワンちゃんあるかも。