JavaScriptに色を付けるの5

昨日の続き。 並んだ割り算と正規表現リテラルを区別する別の方法について。

正規表現に特徴的な文字、例えば ^ や $ が割り算ではあり得ない位置に出てくれば、正規表現と判定できる。 /^ とか $/ とか。 でも出てこない場合もあるので、こいつらには頼れないと結論した。 したのだが。

出てこないなら出せばいいのでは?

判定用の正規表現を眺めていて気付いたのだが、 / に挟まれた文字列が正規表現だった場合、最初の / の直後に ^ を入れても構文エラーにはならないんだよな。 マッチの結果は全然違ってくるけど、構文的な正しさは崩れない。 二番目の / の直前に $ を入れた場合も同様。 しかしこれらが並んだ割り算だった場合にはエラーになる。

処理対象のソースコードが文法・構文的に正しいことを前提としているので、この操作をした場合、該当部分が正規表現だったらソースコード全体も正しいまま。 でも割り算だった場合は構文エラー。

なので、この操作をした結果を eval() に渡して、ちゃんと評価されれば正規表現で、駄目だったら割り算と判定できるのではないか。 要は評価のコンテキストをソースコード全体にまで広げ、かつその評価を人任せにしようという話。

あ、いや、実行されちゃまずい場合もあるのか。 というか実行していい場合の方が圧倒的に少ないんだよな。 eval() じゃなくて Function() に渡すべきか。

まあ、強引なんだけどさ。 あんまりにもあんまりな力技。 HTML上のソースコードならそう長くはないだろうが、それでも再帰処理の中でそんなメモリ喰いなことをして大丈夫なのかって心配もあるし。

でもやってみる。

const jsDefs = [ [ /^(.*?)(\/\*.*?\*\/)(.*)$/s , "comment" ], [ /^(.*?)(\/\/.*?)(\n.*)$/s , "comment" ], [ /^([^"]*?)("(?:\\.|[^"\\])*")(.*)$/s , "string" ], [ /^([^']*?)('(?:\\.|[^'\\])*')(.*)$/s , "string" ], [ /^([^`]*?)(`(?:\\.|[^`\\])*`)(.*)$/s , "string" ], [ /^([^\/]*?)(\/[^\/\*](?:\\.|[^\/\\])*\/)(.*)$/s , "regexp", jsRegexpSubCheck ], [ /^(.*?)\b(break)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(case|catch|class|const|continue)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(debugger|default|delete|do)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(else|enum|export|extends)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(finally|for|function)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(if|import|in|instanceof)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(let)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(new)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(return)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(super|switch)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(this|throw|try|typeof)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(var|void)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(while|with)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(yield)\b(.*)$/s , "keyword" ], [ /^(.*?)\b(false|null|true|undefined)\b(.*)$/s , "keyvalue" ], ]; function jsRegexpSubCheck( acc, matched ) { const src = ( acc + matched[1] ).replace( /<span class=".+?">(.+?)<\/span>/g, '$1' ) + matched[2].replace( /^\//, '/^' ) + matched[3]; try { Function( src.replace( /&lt;/g, '<' ) .replace( /&gt;/g, '>' ) .replace( /&amp;/g, '&' ) .replace( /&quot;/g, '"' ) ); return true; } catch( e ) { return false; } } class Parser { constructor( defs ) { this.finders = defs.map( d => ( text, acc ) => { const matched = text.match( d[0] ); return !matched ? { pre : text, got : "", pst : "" } : d[2] && !d[2]( acc, matched ) ? { pre : text, got : "", pst : "" } : { pre : matched[1], got : `<span class="${d[1]}">${matched[2]}</span>`, pst : matched[3] } } ); } findFirst( text, acc ) { return this.finders.map( f => f( text, acc ) ) .reduce( ( r0, r1 ) => r0.pre.length < r1.pre.length ? r0 : r1 ) } parse( text, acc = "" ) { const r = this.findFirst( text, acc ); if ( !r.got ) { return acc + r.pre; } return this.parse( r.pst, acc + r.pre + r.got ); } } const setColor = () => { const jsParser = new Parser( jsDefs ); const elm = document.getElementById( "smpl01" ); elm.innerHTML = jsParser.parse( elm.innerHTML ); document.getElementById( "btn01" ).disabled = true; let c = 1/2 + 3/4; // テスト }

上のボタンをクリックすると、上記処理を上記ソースコード表示に適用して色を付ける。

やってみたらあっさりできた。 まあ、昨日のをほぼそのまま、正規表現判定の補助関数の中身を変えただけだからね。 その中身も、色付け用のタグを外すのは昨日と同じだし。 それから今日の処理。 正規表現の可能性がある / の直後に ^ を置いて、全体を合体して Function() に渡して判定。

若干面倒だったのは、HTML用にエスケープした文字の扱い。 最初はこれに気付かず、ソースコード文字列を渡した Function() が毎回構文エラーを返してきて、結果として正規表現が全く判定されなくなっていた。

見た目は全く問題ないのに、なぜ構文エラー?

と、しばらく悩んで、デバッガで処理を追って、ようやく気付いたのだった。

あれ? じゃあ昨日のも事情は同じでは?

当然のように湧いくる疑問だが、こちらはちょっと考えてすぐに不要と結論した。 HTML用にエスケープされる文字は、エスケープされていなければ判定に引っかかり、エスケープされていればセミコロンで終わるのでやっぱり引っかかるんだよな。

性能的にも、この程度なら問題ない。 もっと遅くなるかと思ったが、何かと遅いと評判のSafariで実行しても一瞬で終わる。 まあ、今のところSafariでしか動かないんだけどさ。

さて、できたはいいが、普段使いなら昨日の方かな。

今日のこっちのメリットは厳密に判定できることだが、そのためには、ソースコードが完全かつ正しい必要がある。 これが厳しいんだよな。 ソースの一部だけ表示するとか、この条件を満たさない場合も結構あるだろうし。

しかしせっかく作ったものを捨ててしまうのも勿体無いし、どっちを使うか指定できるようにするか。

ここまででJavaScriptは大体いけるようになったので、明日は違う言語を対象に色を付けてみる。