JavaScriptに色を付けるの2

昨日の続き。 今日は予約語に色を付ける。

つもりだったのだが、微妙に変更。 最初は null や undefined も function や var と同じレベルの予約語として扱うつもりだったのだが、こいつらって立ち位置がちょっと違うんだよな。 この名前の変数や関数が作れないって意味では予約語なのだが、使われる場所が違うのだから違う色にした方がいいんじゃないかって気がしてきたのだ。

ということで、元は大雑把に予約語と考えていたものを、以下2つのに分けることにした。

予約語
break, case, catch, class, const, continue, debugger, default, delete, do, else, enum, export, extends, finally, for, function, if, import, in, instanceof, let, new, return, super, switch, this, throw, try, typeof, var, void, while, with, yield
予約値
false, null, true, undefined

この 「予約値」 というのは仮称。 きっと正式な用語があるのだろうが、今重要なのはそこじゃないからね。 undefined は厳密には組み込みのグローバル定数であって予約語じゃないんだけど、俺の中では null と同じ立ち位置なのでここで。

ちなみに予約語の一覧はMDNから拾ってきたもの。 async とか見当たらないと思ったら、ES2015での運用版と将来用だそうだ。 一方で知らないのもちらほら。 debugger なんて今日初めて知ったよ。 当然、これまで使ったことなんて無いわけだが、調べた結果は、きっとこれからも使わない。

さて、色を付ける話だが、当初は 「先頭から最初のブロック要素の前までは予約語などの検索対象とする」 の処理を

予約語 → <span class="keyword">予約語</span>

と一括置換する想定だった。 しかし予約語と予約値のような種別を導入するとなると、この方法は使えない。

で、どうしようかと暫く考えて、ブロック要素と同じ扱いでいいことに気が付いた。 ブロック要素と一緒にして、最初に見つかるまでが最も短いものを処理対象にすりゃいいんだよな。 わざわざブロック要素を分ける意味はなかったが、先行して実装したブロック要素用の処理はそのまま使える。

ということで予約語・予約値の定義を正規表現の配列に追加するのだが、ただ追加するだけじゃつまらないので、トップレベルにだらだら並んでいるあれこれをクラス内にまとめてみた。

const jsDefs = [ [ /^(.*?)(\/\*.*?\*\/)(.*)$/s , "comment" ], [ /^(.*?)(\/\/.*?)(\n.*)$/s , "comment" ], [ /^([^"]*?)("(?:\\.|[^"\\])*")(.*)$/s , "string" ], [ /^([^']*?)('(?:\\.|[^'\\])*')(.*)$/s , "string" ], [ /^([^`]*?)(`(?:\\.|[^`\\])*`)(.*)$/s , "string" ], [ /^([^\/]*?)(\/[^\/\*](?:\\.|[^\/\\])*\/)(.*)$/s , "regexp" ], [ /^(.*?)\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" ], ]; class Parser { constructor( defs ) { this.finders = defs.map( d => text => { const matched = text.match( d[0] ); return matched ? { pre : matched[1], got : `<span class="${d[1]}">${matched[2]}</span>`, pst : matched[3] } : { pre : text, got : "", pst : "" } } ); } findFirst( text ) { return this.finders.map( f => f( text ) ) .reduce( ( r0, r1 ) => r0.pre.length < r1.pre.length ? r0 : r1 ) } parse( text, acc = "" ) { const r = this.findFirst( text ); 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; }

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

予約語はいっぱいあったので、頭文字でまとめている。 「予約」 が reserved じゃなくて key なのは、この方が短いから。

そもそもの目的だった色は、俺的にはこのぐらいが程良いところ。 コメントが1文字もないサンプルで何を言ってるんだと思わなくもないが、これはこれで良いのだ。 駄目なのは、動くのがSafari限定ってこと。 シェア4%って、ねえ。 それもう実質動いてないだろ。

並んだ割り算と正規表現リテラルとの区別ができないのもなぁ…。 JavaScriptに限らず、同じ問題に直面する言語は多いんだよな。 なんとかしたいが、なんとかなるのだろうか。 やっぱり字句解析しないと駄目なんじゃないかって予感が…。

まあ、なんとかなるだろう。 続きは明日。