JavaScriptに色を付けるの4

昨日の続き。 今日は並んだ割り算と正規表現リテラルを区別する。

区別しようと思って朝からずっと考えていたのだが、確実かつ綺麗な方法が思いつかない。 どうしたものか。

正規表現に特徴的な文字、例えば ^ や $ が割り算ではあり得ない位置に出てくれば、正規表現と判定できる。 /^ とか $/ とか。 でも、必ず出るってものでもない。 こいつらが出てこない場合、もっと一般的に / に挟まれた文字列だけでは判断できない場合、その前後、つまりコンテキストってものを判断材料に入れないとどうしようもない。

でもコンテキストをちゃんと判定しようとすると、頭から順に見ていくしかない。 いや、判定には文のレベルで十分なのだが、これが文だと正確に切り分けていくためには、頭から順に見る必要があるのだな。

その辺を端折って、評価対象のコンテキストを最小に / の直前が割り算として妥当な文字かどうかを判定するのも一つの方法だと思うが、これだと誤判定の可能性を0にするのが難しそう。

なんてことを、朝からずっとグルグルと。

まあ悩んでいても何も始まらないので、とりあえず / の直前を判定する方法でやってみよう。

割り算として妥当であるためには、 / の前は

となるだろう。 正規表現リテラルか? という判定なら、ここで駄目だとしているものが来ているかどうかを見ることになる。

なんて言うのは簡単だが、演算子がいっぱいあるんだよな。 確か40ぐらいあったような…。 しかも演算子と他の要素との間にコメントや改行を挟むこともできるし。 だいたい正解ってところまで持っていこうとすると、結構たくさんの候補と場合で判定しなきゃいけないそう。 文法・構文的には正しい前提なので、予約語や予約値まで調べなくても良いのが不幸中の幸いか。

なんてサイレントモードで愚痴を垂れながら手を動かした結果。

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 text = ( acc + matched[1] ).replace( /<span class="comment">.+?<\/span>/g, '' ) .replace( /<span class=".+?">(.+?)<\/span>/g, '$1' ) .replace( /\s*$/, '' ); return text.length === 0 || /[-+*\/%!~<>=&^|?:,[{(;]$/.test( text ); } 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; // テスト }

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

面倒だと思ったのだが、思った程面倒でもなかった。 案ずるより産むが易しとはこのことか。

やってて気付いたのだが、演算子の種類はたくさんあっても、直前の文字を調べれば良いのだから = も == も != も += も全部まとめて = で十分なんだよな。 不等号を書き連ねたシフト演算も同様で、末尾の不等号一つだけを見れば良い。 そんな感じで演算子を集約していくと、20文字ぐらいでなんとかなった。

途中にコメントが入るのも判定を面倒にする要因なのだが、処理を進める過程でコメントと判断したものは

コメント → <span class="comment">コメント</span>

と変換しているので、これを頼りに検査対象文字列からごっそり削除することができた。

そこから他の色付け用のタグを削除し、更に末尾の空白文字をカットすることで / の直前の検査対象文字を特定し、これが空か、或いは規定の文字かで判定。 直前に置ける文字を見落としている可能性はあるが、追加は簡単なので、とりあえず良しとしておこう。

判定の補助関数をトップレベルに置いているのは暫定版のため。 この辺は後で整理するつもり。

ところで判定とは何の関係もないが 「案ずるより産むが易し」 って、そのうち使えなくなりそうだな。 「命がけで出産する女性を蔑ろにするのか!」 とか 「子供を産みたくても産めない女性の気持ちを考えたことがあるのか!」 みたいな突っ込みを受けて。 まあ突っ込む人たちも無理筋だと判ってはいるので、 「そんなつもりがあったかどうかじゃ無くて、そんな風に捉えてしまう人たちの気持ちに寄り添えない無神経さが駄目なんだ」 なんて予防線を張るのだが、だいたい過去の自身の発言を暴かれて逆突っ込みを受け、ツイッターをブロックするのだ。

まあ、どうでもいいか。 続きは明日。 もう一つ思いついた判定方法を試してみる。