読んだ。この日記で以前書いたようなこと(20080116p01, 20080111p01)は全て書いてあった。もちろんそれ以上に知らないこと(NFAのマッチングのしかた、NFA型正規表現エンジンに適用できる正規表現のチューニングの具体例、Unicodeサポート、Perl, .NET, Java, PHPの正規表現、\Gの使い方などなど)が書かれていた。
非常に読みやすい文章で書かれているし、必要なところでは必ず前後のページへの参照先が書かれている。章の始めには Overviewがあり、その章から読み始めた読者への配慮も忘れない。当たり前のことだけど、徹底されている。「まずこの本を読め。正規表現について話すのはそれからだ。」と言い切れる良い本。正規表現を初めて学ぶ人にも、効率について考える余地ができてくるほど既に正規表現を使っている人にも役に立つ。
すごく実用的なテクニックで、でも全く想像が及ばなかったものがある。168ページの「4.5.8.1 肯定の先読みを使ったアトミックグループの模倣」がそれ。
/(?>pattern)/ // アトミックグループを使ったパターン /(?=(pattern))\1/ // 先読みでアトミックグループを模倣したパターン
高機能化する他の実装にくらべて、昔のままの JavaScriptの正規表現はバックトラックを抑制する構文を持っていない。JavaScriptでは非常に有用。
20080116p01でも書いたが、次の終わらない正規表現
/"(?:[^\\"]+|\\.)*"/ // マッチに失敗するとき死ぬほど遅い
はアトミックグループや絶対最大量指定子が使えるなら次のように書けるが JavaScriptは両方ともサポートしていない。
/"(?:[^\\"]+|\\.)*+"/ // JavaScriptでは使えない /"(?>(?:[^\\"]+|\\.)*)"/g // JavaScriptでは使えない /"(?:[^\\"]++|\\.)*"/ // JavaScriptでは使えない。※上2つとは少し意味が違う
次のように先読みでアトミックグループを模倣すると組み合わせの爆発を避けることができる。
/"(?=((?:[^\\"]+|\\.)*))\1"/ /"\1"/ // 上のパターンから先読み部分を取り除いたもの。
先読みを取り除いたパターンを見ると一目瞭然だが、引用符がペアになっていなくて \1 の後ろの " のマッチに失敗したとしても戻る場所がない。あるのは " と \1 にマッチしたという結果で、どちらもオプションではないので取り消すことはできず、繰り返しでもないのでマッチした部分を少しずつ手放させることもできない。なので、ちょっとずつ後じさりしながら延々とあらゆる組み合わせのマッチを試行することなしに、マッチが失敗に終わったことが即座に判断できるようになるというわけ。本物のアトミックグループよりは劣るが効率も悪くない。同じ働きをする次の二つのパターンとかかる時間を比較してみた。
/"[^\\"]*(?:\\.[^\\"]*)*"/ /"(?:[^\\"]|\\.)*"/
バックトラックによる組み合わせの爆発が起きない 3つのパターンでかかる時間を比較。3回実行した。(3回繰り返しても一回一回の中の試行順が固定されていたら傾向は同じになるわな。無意味。あてみやむいみ)
var re = [ /"(?:[^\\"]|\\.)*"/, /"(?=((?:[^\\"]+|\\.)*))\1"/, /"[^\\"]*(?:\\.[^\\"]*)*"/ ]; var s = [ '"'+ new Array(5000+1).join('\\"'), // 1/100 '"'+ new Array(500000+1).join('\\"') +'"', '"'+ new Array(500000+1).join("\\'"), '"'+ new Array(500000+1).join("\\'") +'"', '"'+ new Array(500000+1).join('a'), '"'+ new Array(500000+1).join('a') +'"' ]; var results = []; for(var j = 0; j !== s.length; ++j) { var result = []; for(var i = 0; i !== re.length; ++i) { var t0 = new Date(); var m = re[i].exec(s[j]); result[i] = new Date() - t0; } results[j] = result; } WScript.Echo(results.join("\n"));
数の単位は msec。
パターン1 | パターン2 | パターン3 | |||
工夫なし | アトミックグループの模倣 | ループ展開 | |||
/"(?:[^\\"]|\\.)*"/ | /"(?=((?:[^\\"]+|\\.)*))\1"/ | /"[^\\"]*(?:\\.[^\\"]*)*"/ | |||
---|---|---|---|---|---|
文字列1 | マッチしない(F) | "\"\"......\"\" | 2910×100, 2928×100, 2914×100 | 2551×100, 2581×100, 2595×100 | 2372×100, 2387×100, 2377×100 |
マッチする(T) | "\"\"......\"\"" | 124, 124, 124 | 138, 137, 134 | 108, 107, 108 | |
文字列2 | マッチしない(F) | "\'\'......\'\' | 138, 140, 151 | 125, 127, 125 | 122, 118, 118 |
マッチする(T) | "\'\'......\'\'" | 138, 126, 126 | 140, 128, 133 | 135, 105, 106 | |
文字列3 | マッチしない(F) | "aa..........aa | 174, 172, 166 | 14, 11, 13 | 96, 90, 92 |
マッチする(T) | "aa..........aa" | 155, 119, 126 | 32, 15, 14 | 15, 12, 11 |
ところで、文字列1Fがどのパターンでも一様に遅いのは文字列長に比例したバックトラックが行われているからなんだろうが、パターン2(先読みによるアトミックグループの模倣)でもそれを抑制できていないのは、なんとかできないものか。それでこそ若干のオーバーヘッドをのんででもアトミックグループの模倣を採用する理由になるのだが。
split()の第一パラメータ separatorが空文字列にマッチするかどうかで結果が異なる。
"".split(" ").length; // 1 (空文字列にマッチしないから) "".split("").length; // 0 (空文字列にマッチするので) "".split(/\s+/).length; // 1 (空文字列にマッチしないから) "".split(/^$|\s+/).length; // 0 (空文字列にマッチするので) function getClasses(element) { return element.className.split(/^$|\s+/); }
上の functionでは classNameが空っぽの時には空文字列の要素を作らないが、頭や尻尾に空白が付いていると空文字列の要素が残る (IEを除いて)。事前にトリミングする手間をかけるくらいなら一個二個の空文字列を気にせず(だけど連続する空文字列の要素は気にしてる) className.split(/\s+/) とする方が好みだな。
IE7と Firefox2で異なる。Firefox2の方が正しいが無限ループに陥りやすい。
var re = /\b/g; // 単語境界にマッチする、幅0のメタ文字。 var str = "012 456 89A"; re.lastIndex = 0; for(var i = 0; i !== 5; ++i) { alert("("+ re.exec(str).index +","+ re.lastIndex +")"); // IE7: (0,1) (3,4) (4,5) (7,8) (8,9) ... // Fx2: (0,0) (0,0) (0,0) (0,0) (0,0) ... }
空文字列にマッチしていれば(IE7でスキップされるマッチがでてくる) exec()の前後で lastIndexの値が変わっていなければ(Fx2でのマッチ回数が IE7より増える) indexと lastIndexが同じならば lastIndexを 1インクリメント、としておくとどちらでも間違いが起こらない。
var re = /\b/g; var str = "012 456 89A"; re.lastIndex = 0; for(var i = 0; i !== 5; ++i) { var index = re.exec(str).index; alert("("+ index +","+ re.lastIndex +")"); // IE7: (0,1) (3,4) (4,5) (7,8) (8,9) ... // Fx2: (0,0) (3,3) (4,4) (7,7) (8,8) ... if(index === re.lastIndex) { ++re.lastIndex; } }
ループで
if(index === re.lastIndex) { ++re.lastIndex; }
なんて分岐を増やすより、文字列末尾にマッチする /$/ を例外として*、正規表現から空文字列にマッチする可能性を排除する方が良さそう。
* /$/.exec("str") の後の lastIndexプロパティは IE7、Fx2とも、最後の文字の次を指す。
コメント欄がないのでここでひっそりつっこんでおきます。
Stringと Boolean(と Numberと Date)だけこのように比較方法を === から == にしたりする必要はなくて、
String.prototype.equals = Boolean.prototype.equals = function(arg){ return this == arg; };
こう。
Object.prototype.equals = function(that) { return this.valueOf() === that.valueOf(); }
これでオブジェクトに関しては同一性をチェックし、プリミティブでは値が等しいかどうかをチェックする。
ところで、配列の場合、
valueOf メソッドの動作は、オブジェクトの種類に応じて異なります
配列の要素は、カンマによって連結される文字列に変換されます。Array.toString メソッドや Array.join メソッドと同じように動作します。
Microsoft JScript 5.5 リファレンス (chm)
と書かれていて同一性のチェックができなさそうに思えるけど、JScript5.7、Firefox(javascript1.5)ともにチェックできていた。
Array.prototype.valueOf() は String、Boolean、Number、Dateのそれと違って特別に用意されてはいなかった。
参照: 15.4 Array オブジェクト (Array Objects)
ということは Object.prototype.valueOf()が呼ばれるはずで、これは thisを返す。
var a = [1]; alert(a.valueOf() === a); // true
JScript5.5のいいかげんなリファレンスを書いた人、出てきなさい。(はじめての言語だった JScriptと HSPのヘルプには、変数の概念から始まる丁寧な説明を受けました。大変感謝しています)
最終更新: 2010-03-21T03:27+0900
SHJSの sh_main.jsを高速化したことを以前書いた。(20080204p01)
SHJSのページには動作を確認したブラウザとして以下が挙げられている。
sh_main.jsの修正版は Firefox2と IE7と Opera9で正しく動作することの確認と速度の比較を行っている。
IE6での確認は IE7から戻すのが面倒なので省略する。
Sarari3は Vistaで動くものがダウンロードできるので確認してみたところ動いた。(表示も正常)
いじったことで対応ブラウザが減っていなくて良かった。(IE6は?)
SHJSの作者は Code Conventions for the JavaScript Programming Language や jslint: The JavaScript Verifier かそれに類似した文書を読んでいるに違いない。(これらのページを今日発見した)
というのも、sh_main.jsを JSLintでチェックしてみたが、こういうエラーしか出なかった。
Error: Implied global: document 362, sh_languages 347, window 332
このエラーは JSLint向けにコメントを埋め込めば取り除けるものだし、そうしないと不可避だともいえるもの。
Error: Implied global: document 207 360, sh_languages 332, window 329 Problem at line 73 character 48: Use the array literal notation []. matchCaches = language.matchCaches = new Array(language.length); Problem at line 86 character 17: 'i' is already defined. for(var i = matchCaches.length-1; i !== -1; --i) { Problem at line 97 character 22: 'i' is already defined. for (var i = state.length-1; i !== -1; --i) { Problem at line 110 character 17: 'i' is already defined. var i = (pair[0] & 0x3F); Problem at line 280 character 15: Use '!==' to compare with '0'. while(0 != this._currentStyles.length) { Problem at line 389 character 14: 'node' is already defined. var node = this.free_;
「var array = new Array(length);」を「var array = []; array.length = length;」や「var array = Array.prototype.slice.call({length:length}, 0);」 と書き換えることは拒否する。
(new Array(length)が一番簡潔で自然な書き方)
(JavaScript 1.7の配列内包に書き換えるのには吝かでない)
(一つを除いて) 無視できる警告*ばかりで良かった。
* 無視したら警告の意味がない。forループの変数なんて(古い VC使い以外には)スコープの誤解を招きやすいという理由で、避けなければいけないものの筆頭ともいえる。
Under Translation of ECMA-262 3rd Edition を読んでいて見つけた「故意にに汎用的である」と書かれたメソッド群。Arrayと Stringのほとんどのメソッドが該当する。
Firefoxが Array.prototype.methodなどを Array.methodからも参照できるようにする(している)のも 仕様が汎用的で再利用が可能になっているからだろう。
それら汎用的なメソッドを使い回してやるために、その動作を javascriptのコードで表してみる。
function() { var array = []; var array_index = 0; var item = this; var arg_index = 0; do { if(item instanceof Array) { // (1) var item_index = 0; while(item_index !== item.length) { if(item_index in item) { array[array_index++] = item[item_index++]; } else { ++array_index; ++item_index; // (2) } } } else { array[array_index++] = item; } } while(arg_index !== arguments.length && (item = arguments[arg_index++] || true) ); array.length = array_length; return array; }
function(separator) { var length = this.length>>>0; // (1)-1 if(typeof(separator) === "undefined") { separator = ","; } separator = ToString(separator); if(length === 0) { return ""; } var result; var index = 0; var item = this[index++]; item = (item == null) ? "" : ToString(item); result = item; while(index !== length) { // (1)-2 item = this[index++]; item = (item == null) ? "" : ToString(item); result += separator + item; } return result; } // argが数値の場合は厳密には違うかも。 // 以後もたびたび登場する予定。 function ToString(arg) { return ""+arg.valueOf(); }
function() { var length = this.length>>>0; if(length === 0) { this.length = length; // (1)-1 return; // undefined } length -= 1; var result = this[length]; delete this[length]; this.length = length; // (1)-2 return result; }
function() { var length = this.length>>>0; for(var i = 0; i < arguments.length; ++i) { this[length++] = arguments[i]; } this.length = length; // (1) return length; }
function() { var length = this.length>>>0; var mid = Math.floor(length/2); for(var p = 0; p !== mid; ++p) { var q = length-p-1; var P = this[p], Q = this[q]; if(!(q in this)) { delete[p]; } else { this[p] = Q; } if(!(p in this)) { delete[q]; } else { this[q] = P; } } return this; }
sort()については、this.length>>>0 が用いられることと、thisを変更して thisを返すことだけを書いておいて、i, j要素を比較する手順について。
function compare(i, j) { // (A) 存在しない要素を最後尾へ。 if(!(i in this)) { if(!(j in this)) { return +0; // (1) } else { return 1; } } else if(!(j in this)) { return -1; } var I = this[i], J = this[j]; // (B) undefinedな要素を後ろへ。 if(typeof(I) === "undefined") { if(typeof(J) === "undefined") { return +0; // (1) } else { return 1; } } else if(typeof(J) === "undefined") { return -1; } // (C) ユーザー比較関数 if(typeof(comparefn) !== "undefined") { return comparefn(I, J); } // (D) デフォルト比較方法 (文字列化して昇順) I = ToString(I), J = ToSTring(J); if(I < J) { return -1; } else if(I > J) { return 1; } return +0; }
function() { var length = this.length>>>0; if(length === 0) { this.length = length; // (1) return; // undefined } var result = this[0]; // ひとつずつ前へスライド for(var k = 1; k !== length; ++k) { if(k in this) { this[k-1] = this[k]; } else { delete this[k-1]; } } delete this[length-1]; this.length = length-1; return result; }
function() { var length = this.length>>>0; // 要素を引数の数だけ後ろへシフト。 for(var k = length; k !== 0; --k) { var j = k-1; if(j in this) { this[j + arguments.length] = this[j]; } else { delete this[j + arguments.length]; } } // 先頭に引数をコピー for(var k = 0; k !== arguments.length; ++k) { this[k] = arguments[k]; } this.length = length + arguments.length; // (1) return this.length; }
function(start, end) { var array = []; var array_index = 0; var length = this.length>>>0; start = ToInteger(start); start = (start < 0) ? Math.max(length+start, 0) : Math.min(start, length); end = (typeof(end) === "undefined") ? length : ToInteger(end); end = (end < 0) ? Math.max(length+end, 0) : Math.min(end, length); for(var k = start; k < end; ++k) { if(k in this) { array[array_index++] = this[k]; } else { ++array_index; // (1) } } array.length = array_index; return array; } function ToInteger(x) { x = +x; if(isNaN(x)) { return +0; // (2) } if(x === 0 || !isFinite(x)) { return x; } // 0 に近づける。(小数点以下を切り捨てる) return (0 <= x) ? Math.floor(x) : Math.ceil(x); }
function(start, deleteCount) { var array = []; var length = this.length>>>0; start = ToInteger(start); start = (start < 0) ? Math.max(length+start, 0) : Math.min(start, le deleteCount = ToInteger(deleteCount); deleteCount = Math.min(Math.max(deleteCount, 0), length-start); // 取り出す要素をコピー。(まだ削除はしない) for(var k = 0; k !== deleteCount; ++k) { var l = start+k; if(l in this) { array[k] = this[l]; } } // (A) 要素を後ろへずらして空きを作る // (後で上書きされてしまう要素までずらしてない?) if(deleteCount < arguments.length) { for(var k = length-deleteCount; k !== start; --k) { var l = k + deleteCount -1; var m = k + arguments.length -1; if(l in this) { this[m] = this[l]; } else { delete this[m]; } } // (B) 要素を前へ詰めて、空きを新要素と同じ数にする } else if(arguments.length < deleteCount) { for(var k = start; k !== length-deleteCount; ++k) { var l = k + arguments.length; var m = k + deleteCount; if(m in this) { this[l] = this[m]; } else { delete this[l]; } } // (なぜ逆順で削除する?) for(var k = length; k !== length-deleteCount+arguments.length; --k) { delete this[k-1]; } } // 追加する要素で thisを上書き if(arguments.length === deleteCount) { for(var k = 0; k !== arguments.length; ++k) { this[start+k] = arguments[k]; } } this.length = length - deleteCount + arguments.length; return array; }
パターンがわかってきた。
Firefox(JavaScript1.5?)では適用すると Array.prototype.method.call(string) is read-onlyという警告がいくつも出る。
一度の呼び出しでいくつも出ることから read-onlyなのはメソッドではなく、自己破壊的な Array.prototype.method()を適用された stringだと思われる。
警告が出るだけで、戻り値は得られるので pop()で末尾の文字、shift()で先頭の文字が得られる。
unshift()、push()の返す数値は使えない。(その長さを持った文字列は存在しないので)
reverse(), sort()は全く役に立たない。役に立つ値を返さないし、並べ替えには失敗しているから。
slice()は String.prototypeに同名のメソッドがすでに存在する。
splice()がちょっと面白く、splice(startと deleteCount) の引数はちょうど String.prototype.substr(start, length)と対応するが、返ってくるのが substr()->"文字列" なのに対し、splice()->["文","字","列"] となる。
string.split("")の代わりに Array.prototype.splice.call(string, 0)としてみるのはいかが? (警告が出る上に IE7では使えませんが)
Stringではなく、配列のようなオブジェクト(コレクションとか argumentsとか)に適用するのが正解か。
// unknown_objectが配列なら unknown_objectのコピー、 // それ以外なら unknown_objectを唯一の要素とする配列が返る。 var array = Array.prototype.concat.call(unknown_object);
// lengthプロパティと [] での要素アクセスが可能な、 // 配列っぽいオブジェクト(NodeListとか argumentsとか)を配列化。 var array = Array.prototype.slice.call(document.getElementsByTagName("pre"), 0);
// " " が得られます。 var softtab = Array.prototype.join.call({length:8+1}, " "); // softtabを使ったレベル3のインデントが得られます。 var indent = new Array(3+1).join(softtab); // prototypeいらねー
二番目の書き方を使うなら Arrayのビルトインメソッドを他のオブジェクトに使い回すという趣旨が……。
今回から String.prototypeのメソッド。Stringは自己破壊的なメソッドを持っていないから、Arrayのビルトインメソッドと違って適用範囲が広いことを期待している。
……と思ったが、Stringのビルトインメソッドはどれも事前に ToString(this)を呼んでしまう。(ToString()については (1))
indexOfで配列の探索ができるんじゃないかと期待していたが無理だった。がっかり。
これにて終了。
split()に渡す正規表現のサブマッチに特別な意味があるとか、split()はグローバルフラグを無視するとか、グローバルフラグの立った正規表現で exec(string)を呼んで空文字列にマッチしたときはこちらで lastIndexプロパティを 1増やさないといけないとか、もやもやしてたことが Under Translation of ECMA-262 3rd Editionにはいろいろ書いてありました。訳者に感謝。
switch-caseのラベル部分に何でも書ける*という変態的な部分は置いておいて、1/2が 0.5になるのと同じ類の話。
返るのは [1, 10, 5]。どうも文字列としてソートされている。数字だけの配列だ(と自分が知っている)からって無駄な期待をしてはいけない。
stringと表示される。i に入るのが配列の要素 1, 5, 10 ではなく添え字(key)だというだけで意外だが、さらに意表をついてそれが数字でもなく、"0", "1", "2" 。i+1 としても隣の要素にはアクセスできない。array[1] = 5; と数字の添え字で代入した後でも同じだった。
for-in ループは遅いと評判だったので使っておらず、最近まで知らなかった。
ついでにいうと for-inとは別の in 演算子の存在も、つい最近まで知らなかった。(SHJSのソースを読んで発見した)
グローバル変数の存在確認方法も SHJSを読んで知った。今までは typeof(global_var) == "undefined" とやっていた。これは存在確認とは微妙に意味が違う。
deleteできるグローバル変数とできないグローバル変数については amachangのブログを読んで知った。
ということを知っていれば for-inループで文字列が渡されても驚くことはない。(じゃあなぜ驚いた)
var object = {}; alert(""+ (1==="1")); // false o[1] = 1; o["1"] = "1"; alert(""+ (o[1]===o["1"])); // true var a = [1,2,3]; var b = [1,2,3]; alert(""+ (a===b)); // false o[a] = a; o[b] = b; alert(""+ (o[a]===o[b])); // true var s = "1,2,3"; alert(""+ (a===s)); // false o[s] = s; alert(""+ (o[a]===o[s])); // true
* http://arton.no-ip.info/diary/20061224.html#p02
こんなやりとりがあった。
dankogaiの指摘の一つ、「MyObject.prototype = { /* ... */ }は避けるべし」について。
MyBox.prototype = Box.prototype; // ここで継承しているのに MyBox.prototype = { // ここで台無し speed:8 };
指摘には同意するが、上の例だと親の Boxにも MyBoxと共通のクラス変数 speedが追加されている。
amachangの反論にあるように、継承するなら次の書き方の方が優れている。
MyBox.prototype = new Box; // new Box()と同じ
ところで、amachangはこう続けている。これがプロトタイプ継承の正しい形式であり
MyBox.prototype = {}; MyBox.prototype = new Object;
指摘を受けた上段の書き方は下段のシンタックスシュガーなので両方とも Objectからの正しい継承方法である。このことがわかっていれば、他から継承しておきながら Objectからも継承して台無しにするような間違いは犯さない。(だから必要に応じて違う書き方をすれば良いし、必要がなければ .prototype={}と書いても良い)。
でも違う理由で MyObject.prototype = {}; という書き方は避けている。 理由は一つで
MyObject = function(){}; MyObject.prototype = {/* MyObjectの定義 */}; alert(new MyObject().constructor); // function Object(){ [natiive code] } と表示
constructorプロパティが MyObjectではなくなってしまっている。これは MyObject.prototype = new MyParent; としたときも同じで constructorは MyParentになる。
constructorプロパティだけを気にするなら
MyObject.prototype = { constructor:MyObject.prototype.constructor };
というように明示的にコピーすればごまかせるが、一時しのぎ感がぷんぷんしている。(constructor以外のプロパティが追加されたらその都度書き換えるの?)
だから、継承の仕方で逆につっこまれてしまったが、dankogaiが提示した 3つの方法に自分は同意していて、そのうちの一つ目を自分は使っている。
SHJSのブラウザでの実行時間を削るには sh_main.js(SHJSのメインスクリプト)を速くするか、正規表現を効率的なものにする方法がある。(>遅い正規表現(20080116p01))。
正規表現に関してできることは限られるうえ、知識も少ない(『詳説 正規表現 第三版』待ち)ので、可能な限り文字クラスや文字集合といわれるものを使うように気を付けただけにとどまる。(sh_ruby.js, sh_javascript.js)
メインスクリプトの sh_main.jsに対してできることは多い。この日記の現在?の最新ページ(2008年1月12日から7日間)を表示して、sh_highlightDocument()前後での経過時間を表示したところこのようになった。
Firefox2 | IE7(64-bit) | IE7(32-bit) | Opera9.25 | |
---|---|---|---|---|
sh_main.js (0.4.2) | 935ms | 1050ms | 1270ms | 1260±150ms |
改変版 | 600ms | 680ms | 865ms | 1200±150ms |
削減率 | 36% | 35% | 32% | 5% |
ハイライト対象が少なくて数ミリ秒で処理が終わるような場合はオーバーヘッドのために改変版の方が 1-2ミリ秒遅くなるが、それよりもスクリプトがブラウザをロックする時間が長くなるような場合にこそ速度改善が必要なので OK。
代償としてファイルサイズが sh_main.jsで 10.5KiBから 12.7KiBへ +2.2KiB。jsmin圧縮後の sh_main.min.jsで 6.22KiBから 7.82KiBへ +1.60KiB。Apacheによる gzip圧縮やブラウザのキャッシュに期待します。
普段は全く Operaを使わないし、詳しくもない。むしろ Operaではキーボードを使ったブラウジングもままならない。そんな人間が Firefox+Firebugを頼りに sh_main.jsの修正を行ったので Operaの速度が改善しないのは仕方のない部分がある。(IEは改善したが)。(あんだけいじってトータルで変わらない方がすごい。どこが足を引っぱっているのだろう)。リテラル文字列と Stringオブジェクトの差が他のブラウザより大きいらしいが、それが原因?
EfficientJavaScript - Dev.Opera - 効率的な JavaScript (www.hyuki.com)
Operaでの JavaScriptの実行時間が他のブラウザに比べて長いのははっきりした理由があって、Operaはスクリプトが全力疾走中であってもユーザーの操作に対する反応を後回しにしたりしない。これは偉い。ユーザーを待たせない代わりにスクリプトが遅れるのは当然の代償で仕方がない。
あ、スクリプトでなく再描画が律速してるから改善しないということ?
(常に最新版だが一時的にバグが混入していることがあるかも)
すぐ上のリンク先はすでに変更が反映されている。
これら二つの記事を参考に escapeHTML()を変更した。測定に使ったページでは 9000回ちかく呼び出されるメソッドなので影響はバカにならない。といっても 600msだったのが 590msを切るようになった、というレベル。むしろ下請けfunctionを隠蔽できたことの方が嬉しい。
escapeHTML()自体、sh_builderのインターフェイスではないので、外部から呼び出せないようにすべきかもしれないが、functionをかぶせるたびに呼び出しのオーバーヘッドが増える気がしてそうはしていない。
SHJSの patternStackオブジェクトは外部と完全に独立して動作するのに、sh_highlightString()が呼ばれるたびに無名クラスとそのインスタンスを作成するような方法がとられている。コンストラクタと prototypeを書こう。(sh_highlightString()は HTML文書内の <pre class="sh_XXX">の数だけしか呼ばれないから影響は小さいが。件のページでは 58回)。
sh_highlightString()からしか使われないのにスタックの可視範囲が広がるのが気になるなら、さっき覚えた無名functionで二つをくるんでしまえば良い。
var sh_highlightString = (function(){ var Stack = function(){ this.stack_ = []; }; Stack.prototype.getLength = function(){/* ... */}; // …… return function(){ var patternStack = new Stack(); /* sh_highlightStringの中身がつづく…… */ }; })();
まあ、速度が改善するわけではないので、書き直さないんだけど。
innerHTMLや textContent、innerTextの使用は堕落だという気もするが、冗長な上に呼び出しを重ねることで遅くなる DOMメソッドがいけない。
SHJSの javascript定義ファイル(lang/sh_javascript.js)の元になったファイル(javascript.lang)の中身がこれ。
include "java.lang" subst keyword = "abstract|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|final|finally|for|function|goto|if|implements|in|instanceof|interface|native|new|null|private|protected|prototype|public|return|static|super|switch|synchronized|throw|throws|this|transient|true|try|typeof|var|volatile|while|with"
javaて……。キーワードにしても使ったことのないものがいっぱい。
あまりにあんまりなんで一から書いた。(sh_javascript.js, sh_javascript.min.js)。 参照したのは JScript5.5の HTMLHelpなので JScript.NETや ECMAScript4には対応していない。古典的な JavaScript。
ついでに同じものを SakuraEditorにも。(javascript_keywords.zip)
オブジェクトリテラルの存在も JScript5.5の HTMLHelpでの扱いが一行しかなかった*ために気付くのが遅れたが、これはその上をいく。古典的な javascriptで何年ぶりの新発見。しかもこれもちゃんと書いてあった。(但し一行⁑)
block: { break block; }
ループにラベルを付けて、ネストしたループから外側のループを continueしたり breakしたりできるのは知っていた(が忘れていた)が、ラベル付きブロックと breakのこの使い方は全く知らなかった。
labelのスコープはローカル変数と同じみたいで⁂、functionをまたいで goto代わりには使えなかった。
function(){ alert(1) }(); void function(){ alert(2) }(); (function(){ alert(3) })();
alertが表示されるのは 2と 3。二行目ので初めて voidの使い途を見つけたと思ったのだったが三番目の書き方もあるらしく、またそれが一般的っぽい。まあ、なんとなくわかる。あれでしょ。
1行目の functionだけ functionExpressionではなく functionDeclarationだと解釈されて、続く () は関数呼び出しのかっこではなく、式をグルーピングするかっこになるのだとか。(functionDeclarationでは関数名を省略できないのだから、(省略できる)functionExpressionだと解釈してくれても良さそうなものだが)
そういう理由なのでこんなのもあり。
+function(){ alert(4) }(); (function(){ alert(5) }());
ファイルはこちら20080101p01。
あいだにコメントを挟みながら一連の DIFF差分(sh_ruby.js.diff)を見ていく。
--- sh_ruby.js.original Fri Aug 3 12:16:32 2007 +++ sh_ruby.js Sat Jan 19 01:35:32 2008 @@ -2,123 +2,427 @@ this.sh_languages = {}; } sh_languages['ruby'] = [ [ { - 'regex': /\b(?:require)\b/g, - 'style': 'sh_preproc' + regex: /\brequire\b/g, + style: 'sh_preproc' },
- { - 'next': 1, - 'regex': /#/g, - 'style': 'sh_comment'
+ { // part of Kernel methods. + regex: /\b(?:exit!?|(?:abort|at_exit|BEGIN|callcc|END|eval|exec|fork|load|spawn|syscall|system|trap|warn)\b)/g, + style: 'sh_preproc' },
{ - 'regex': /\b[+-]?(?:(?:0x[A-Fa-f0-9]+)|(?:(?:[\d]*\.)?[\d]+(?:[eE][+-]?[\d]+)?))u?(?:(?:int(?:8|16|32|64))|L)?\b/g, - 'style': 'sh_number' + regex: /[+-]?\b(?:0(?:x[A-Fa-f0-9_]+|d[\d_]+|b[01_]+|o?[0-7_]+)|(?:0\.)?[\d_]+(?:[Ee][+-]?[\d_]+)?)\b/g, + style: 'sh_number' },
{ - 'next': 2, - 'regex': /"/g, - 'style': 'sh_string' + next: 2, + regex: /"/g, + style: 'sh_string' }, { - 'next': 3, - 'regex': /'/g, - 'style': 'sh_string' + next: 3, + regex: /'/g, + style: 'sh_string' },
{ - 'next': 4, - 'regex': /</g, - 'style': 'sh_string' + next: 4, + regex: /<(?=[\w\/])/g, + style: 'sh_string' },
{ - 'regex': /\/[^\n]*\//g, - 'style': 'sh_regexp' + regex: /\/(?:\\.|[^\n\\\/])*\/[eimnosux]*(?![A-Za-z])/g, + style: 'sh_regexp' },
{ - 'regex': /(%r)(\{(?:\\\}|#\{[A-Za-z0-9]+\}|[^}])*\})/g, - 'style': ['sh_symbol', 'sh_regexp']
+ regex: /(?:\b(?:alias|attr(?:_reader|_writer|_accessor)?|begin|break|case|do|else|elsif|end|ensure|for|if|in|include|lambda|loop|next|proc|raise|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield|and|not|or|def|class|module|catch|fail|throw)\b|&&|\|\|)/g, + style: 'sh_keyword' }, { - 'regex': /\b(?:alias|begin|BEGIN|break|case|defined|do|else|elsif|end|END|ensure|for|if|in|include|loop|next|raise|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield|false|nil|self|true|__FILE__|__LINE__|and|not|or|def|class|module|catch|fail|load|throw)\b/g, - 'style': 'sh_keyword'
+ next: 5, + regex: /^=begin/g, + style: 'sh_comment' }, { - 'next': 5, - 'regex': /(?:^\=begin)/g, - 'style': 'sh_comment'
+ regex: /@@?[A-Za-z_][A-Za-z0-9_]*/g, + style: 'sh_type' }, - { - 'regex': /(?:\$[#]?|@@|@)(?:[A-Za-z0-9_]+|'|\"|\/)/g, - 'style': 'sh_type'
+ { // global variables + regex: /\$(?:[_&~`'\+\?!@=\/\\,;\.<>\*\$:"]|-?[A-Za-z0-9_]+\b)/g, + style: 'sh_type' + },
+ { // %r(regexp) + next: 6, + regex: /%r(?=[\(<\[\{])/g, + style: 'sh_regexp' + }, + { // %x(command), %w(array) + next: 11, + regex: /%[xWw](?=[\(<\[\{])/g, + style: 'sh_normal' + }, + { // %(string), %s(symbol) + next: 16, + regex: /%[Qqs]?(?=[\(<\[\{])/g, + style: 'sh_string' + },
+ { // %r!regexp!i + regex: /%r([ -'*-\/:;=?@\\^_`|~])(?:\\.|.)*?\1[eimnosux](?![A-Za-z])/g, + style: 'sh_regexp' + }, + { // %x!command!, %w!array! + regex: /%[xWw]?([ -'*-\/:;=?@\\^_`|~])(?:\\.|.)*?\1/g, + style: 'sh_string' + }, + { // %!string!, %s!symbol! + regex: /%[Qqs]?([ -'*-\/:;=?@\\^_`|~])(?:\\.|.)*?\1/g, + style: 'sh_string' + },
+ { // Symbol + regex: /(:)((?:@@?|\$|[A-Za-z_])\w+\b[!\?]?)/g, + style: ['sh_symbol', 'sh_string'] + }, + { // Symbol + regex: /(:)(\+|~|\*\*?|-|\/|%|<=>|<<?|>>?|^|===?|=~|!~|&|\|)(?=[^\w\d]|$)/g, + style: ['sh_symbol', 'sh_string'] + },
+ { // Constants + regex: /\b[A-Z]\w+\b/g, + style: 'sh_function' + }, + { // Constants + regex: /\b(?:self|nil(?!\?)|true|false|__FILE__|__LINE__)\b/g, + style: 'sh_function' + },
+ { // don't highlight ? and ! as symbols if they are part of a method call + regex: /\b[a-z_]\w*[!\?]/g, + style: 'sh_normal' }, { - 'regex': /[A-Za-z0-9]+(?:\?|!)/g, - 'style': 'sh_normal'
+ regex: /~|!|%|\^|\*|\(|\)|-|\+|=|\[|\]|\\|::?|;|,|\.|\/|\?|&|<|>|\|/g, + style: 'sh_symbol' }, { - 'regex': /~|!|%|\^|\*|\(|\)|-|\+|=|\[|\]|\\|:|;|,|\.|\/|\?|&|<|>|\|/g, - 'style': 'sh_symbol' + regex: /(#)(\{)/g, + style: ['sh_symbol', 'sh_cbracket'] }, { - 'regex': /(#)(\{)/g, - 'style': ['sh_symbol', 'sh_cbracket'] + regex: /\{|\}/g, + style: 'sh_cbracket' }, { - 'regex': /\{|\}/g, - 'style': 'sh_cbracket'
+ next: 1, + regex: /#/g, + style: 'sh_comment' } ],
[ { - 'exit': true, - 'regex': /$/g + exit: true, + regex: /$/g } ], [ { - 'exit': true, - 'regex': /$/g + exit: true, + regex: /$/g }, { - 'regex': /\\(?:\\|")/g + regex: /\\[\\"]/g }, { - 'exit': true, - 'regex': /"/g, - 'style': 'sh_string' + exit: true, + regex: /"/g } ], [ { - 'exit': true, - 'regex': /$/g + exit: true, + regex: /$/g }, { - 'regex': /\\(?:\\|')/g + regex: /\\[\\']/g }, { - 'exit': true, - 'regex': /'/g, - 'style': 'sh_string' + exit: true, + regex: /'/g } ], [ { - 'exit': true, - 'regex': /$/g + exit: true, + regex: /$/g }, { - 'exit': true, - 'regex': />/g, - 'style': 'sh_string' + exit: true, + regex: />/g } ], [ { - 'exit': true, - 'regex': /^(?:\=end)/g, - 'style': 'sh_comment' + exit: true, + regex: /^=end/g } + ],
+ [ // state 6-10: %r(regexp) + { + exit: true, + regex: /$/g + }, + { + next: 7, + regex: /\(/g, + style: 'sh_regexp' + }, + { + next: 8, + regex: /</g, + style: 'sh_regexp' + }, + { + next: 9, + regex: /\[/g, + style: 'sh_regexp' + }, + { + next: 10, + regex: /\{/g, + style: 'sh_regexp' + }, + { + exit: true, + regex: /[)>\]}][eimnosux]*/g, + style: 'sh_regexp' + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 6, + regex: /(?=\()/g + }, + { + exit: true, + regex: /(?=\))/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 6, + regex: /(?=<)/g + }, + { + exit: true, + regex: /(?=>)/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 6, + regex: /(?=\[)/g + }, + { + exit: true, + regex: /(?=])/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 6, + regex: /(?={)/g + }, + { + exit: true, + regex: /(?=})/g + } + ], + [ // state 11-15: %x(command) + { + exit: true, + regex: /$/g + }, + { + next: 12, + regex: /\(/g, + style: 'sh_normal' + }, + { + next: 13, + regex: /</g, + style: 'sh_normal' + }, + { + next: 14, + regex: /\[/g, + style: 'sh_normal' + }, + { + next: 15, + regex: /\{/g, + style: 'sh_normal' + }, + { + exit: true, + regex: /[)>\]}]/g, + style: 'sh_normal' + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 11, + regex: /(?=\()/g + }, + { + exit: true, + regex: /(?=\))/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 11, + regex: /(?=<)/g + }, + { + exit: true, + regex: /(?=>)/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 11, + regex: /(?=\[)/g + }, + { + exit: true, + regex: /(?=])/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 11, + regex: /(?={)/g + }, + { + exit: true, + regex: /(?=})/g + } + ], + [ // state 16-20: %Q(string) + { + exit: true, + regex: /$/g + }, + { + next: 17, + regex: /\(/g, + style: 'sh_string' + }, + { + next: 18, + regex: /</g, + style: 'sh_string' + }, + { + next: 19, + regex: /\[/g, + style: 'sh_string' + }, + { + next: 20, + regex: /\{/g, + style: 'sh_string' + }, + { + exit: true, + regex: /[)>\]}]/g, + style: 'sh_string' + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 16, + regex: /(?=\()/g + }, + { + exit: true, + regex: /(?=\))/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 16, + regex: /(?=<)/g + }, + { + exit: true, + regex: /(?=>)/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 16, + regex: /(?=\[)/g + }, + { + exit: true, + regex: /(?=])/g + } + ], + [ + { + exit: true, + regex: /$/g + }, + { + next: 16, + regex: /(?={)/g + }, + { + exit: true, + regex: /(?=})/g + } ] ];
SHJSのスクリプトは全て、機能が同じでファイルサイズが違う hoge.jsと hoge.min.jsの二種類が収録されている。言語ごとに定義ファイルが分かれているのもおそらく転送量を抑えるためで、個々の jsファイルのほとんどが数キロバイトに収まっている。
*.min.jsファイルは JSMINというツールで空白を詰めることで作られている。JSMINのオリジナルは DOS実行ファイルだけど、C#、Java、JavaScript、Perl、PHP、Python、OCAML、Rubyの実装もある。javascriptを圧縮するのなら javascriptを使いたいよね、ということで javascriptバージョンの jsmin.jsをダウンロードしてきた。
jsmin.jsの中には jsmin()という関数が一つだけある。これに javascriptのソースを渡すとコンパクトになったソースが返ってくるのだけどどうやって実行しよう。jsmin.jsと同じ場所にあった test.htmlをブラウザで表示してテキストエリアにソースを貼り付けて実行するのもありだが sh_ruby.jsをちょこちょこいじってる身としては毎回となると面倒くさい。
というわけで J(ava)Scriptで exec_jsmin.jsというのを書いた。jsmin.jsと同じ場所に置いたこのファイルに *.jsファイルをドロップすると *.min.jsというファイルを作成する。
var fso = new ActiveXObject("Scripting.FileSystemObject"); function ReadFile(path) { var ts = fso.OpenTextFile(path, 1, false); var text = ts.ReadAll(); ts.Close(); return text; } function WriteFile(path, text) { var ts = fso.CreateTextFile(path, true, false); ts.Write(text); ts.Close(); } eval(ReadFile(fso.BuildPath(fso.GetParentFolderName(WScript.ScriptFullName), "jsmin.js"))); var args = WScript.Arguments; for(var i = 0; i < args.Length; ++i) { var path = args(i); if(fso.FileExists(path)) { var path_min = fso.BuildPath(fso.GetParentFolderName(path), fso.GetBaseName(path)) + '.min.js'; WriteFile(path_min, jsmin(ReadFile(path))); } else { WScript.Echo("FileNotExist:"+path); } }
最初から最後まで J(ava)Scriptで完結して満足です。
ファイルはこちら。20080101p01。
頭の方から変更点を見ていく。
- 'regex': /\b(?:require)\b/g, + 'regex': /\brequire\b/g,
require一つだけだからかっこで囲む必要はない。
- 'regex': /\b(?:defined\?|Array|Floar|Integer|String|abort|callcc|exec|exit!?|fork|proc|lambda|set_trace_func|spawn|syscall|system|trace_var|trap|untrace_var|warn)\b/g, + 'regex': /\b(?:defined\?|exit!?|(?:abort|callcc|exec|fork|set_trace_func|spawn|syscall|system|trace_var|trap|untrace_var|warn)\b)/g,
Array、Floar(Floatのスペルミスでした)、Integer、Stringを取り除いて、定数のルールが適用されるように。sh_preprocではなく sh_functionになる。
lambdaと procも取り除いて、sh_keywordに含めることにした。
\bは defined?の ?と exit!の !の直前にマッチし、?の後や !の後にはマッチしないので正しくマッチするように修正。
- { // Symbol - 'regex': /:(?:(?:@@|@|\$)?\w+[\?!]?|\+=?|!=?|~|\*\*=?|-=?|\*=?|\/=?|%=?|<<=?|>>=?|&=?|\|=?|^=?|>=?|<=?|<=>|===?|=~|!~|&&=?|\|\|=?|\.\.|\.\.\.|=)(?=\s|$)/g, - 'style': 'sh_string' - }, + { // Symbol + 'regex': /(:)((?:@@|@|\$)?\w+\b[!\?]?)/g, + 'style': ['sh_symbol', 'sh_string'] + }, + { // Symbol + 'regex': /(:)(\+|~|\*\*|-|\*|\/|%|<<?|>>?|^|<=>|===?|=~|!~|&|\|)(?=[^\w\d]|$)/g, + 'style': ['sh_symbol', 'sh_string'] + },
あまりにルールが乖離してるので Symbolのルールを分割。加えて、不正な Symbolリテラルをルールから除外(代入、複合代入、:&&、:||、:...など)
リテラルの先頭の : を sh_stringから sh_symbolにしたのは
:"hoge" :hoge
の整合性をとるため。
- 'regex': /\/[^\n]*\//g, + 'regex': /\/(?:\\.|[^\n\\\/])*\/[eimnosux]*(?!\w)/g,
正規表現リテラルのオプション部分もマッチに含めるように。あと条件を厳しくしたので URLに誤マッチすることが減るはず。
- 'regex': /(?:\b(?:alias|begin|BEGIN|at_exit|break|case|do|else|elsif|end|END|ensure|for|if|in|include|loop|next|raise|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield|and|not|or|def|class|module|catch|fail|load|throw)\b|&&|\|\|)/g, + 'regex': /(?:\b(?:alias|begin|BEGIN|at_exit|break|case|do|else|elsif|end|END|ensure|for|if|in|include|lambda|loop|next|proc|raise|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield|and|not|or|def|class|module|catch|fail|load|throw)\b|&&|\|\|)/g,
lambdaと procを sh_preprocから sh_keywordへ持ってきた。どちらもメソッドになりうる重要な要素だと思うから。
- 'regex': /\b[A-Z]\w+[!\?]?(?=\b|$)/g, + 'regex': /\b[A-Z]\w+\b[!\?]?/g,
\bを正しく使用。最後の [!\?]?は不要でした。試してみたらエラーになった。
- 'regex': /\b(?:false|nil(?!\?)|true|self|__FILE__|__LINE__)(?=\b|$)/g, + 'regex': /\b(?:false|nil(?!\?)|true|self|__FILE__|__LINE__)\b/g,
- 'regex': /[a-z0-9_]+(?:\?|!)/g, + 'regex': /\b[a-z0-9_]+[!\?]?/g,
末尾が ?や !のメソッドだけを拾い上げたかったのだろうか?ローカル変数っぽいものにもマッチするようにしたけど、どのみち色はつかないので害はない。因みに文字配列リテラル( %w(one two three) )も適切なクラスが見つからなかったので sh_normalにしている。
- 'style': 'sh_string' - 'style': 'sh_string' - 'style': 'sh_string' - 'style': 'sh_commend'
'string'、"string"、<tagname>、=begin〜=endの終了条件部分から styleを取り除く。なくても出力は変わらない。それにしても HTMLタグっぽいものにマッチするルールがあるのはなぜだろう。Web用言語だと思われてるのかな?(<stdio>や <stdlib> のたぐいの可能性もある)。不都合はないので消さないけど。
20080101p01からの続き。正式な sh_ruby.js (私的改訂版)はそちらから。
機能は同じ(はず)なのになぜか全く様子の違う二つのスクリプトができてしまった。こんな感じ。
{ // %r(regexp) 'next': 6, 'regex': /%r(?=[\(<\[\{])/g, 'style': 'sh_regexp' }, { // %x(command), %w(array) 'next': 11, 'regex': /%[xWw](?=[\(<\[\{])/g, 'style': 'sh_normal' }, { // %(string), %s(symbol) 'next': 16, 'regex': /%[Qqs]?(?=[\(<\[\{])/g, 'style': 'sh_string' },
[ // state 6-10: %r(regexp) { 'exit': true, 'regex': /$/g }, { 'next': 7, 'regex': /\(/g, 'style': 'sh_regexp' }, { 'next': 8, 'regex': /</g, 'style': 'sh_regexp' }, { 'next': 9, 'regex': /\[/g, 'style': 'sh_regexp' }, { 'next': 10, 'regex': /\{/g, 'style': 'sh_regexp' }, { 'exit': true, 'regex': /[)>\]}]/g, 'style': 'sh_regexp' } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 6, 'regex': /(?=\()/g, }, { 'exit': true, 'regex': /(?=\))/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 6, 'regex': /(?=<)/g, }, { 'exit': true, 'regex': /(?=>)/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 6, 'regex': /(?=\[)/g, }, { 'exit': true, 'regex': /(?=])/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 6, 'regex': /(?={)/g, }, { 'exit': true, 'regex': /(?=})/g, } ], [ // state 11-15: %x(command) { 'exit': true, 'regex': /$/g }, { 'next': 12, 'regex': /\(/g, 'style': 'sh_normal' }, { 'next': 13, 'regex': /</g, 'style': 'sh_normal' }, { 'next': 14, 'regex': /\[/g, 'style': 'sh_normal' }, { 'next': 15, 'regex': /\{/g, 'style': 'sh_normal' }, { 'exit': true, 'regex': /[)>\]}]/g, 'style': 'sh_normal' } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 11, 'regex': /(?=\()/g, }, { 'exit': true, 'regex': /(?=\))/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 11, 'regex': /(?=<)/g, }, { 'exit': true, 'regex': /(?=>)/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 11, 'regex': /(?=\[)/g, }, { 'exit': true, 'regex': /(?=])/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 11, 'regex': /(?={)/g, }, { 'exit': true, 'regex': /(?=})/g, } ], [ // state 16-20: %Q(string) { 'exit': true, 'regex': /$/g }, { 'next': 17, 'regex': /\(/g, 'style': 'sh_string' }, { 'next': 18, 'regex': /</g, 'style': 'sh_string' }, { 'next': 19, 'regex': /\[/g, 'style': 'sh_string' }, { 'next': 20, 'regex': /\{/g, 'style': 'sh_string' }, { 'exit': true, 'regex': /[)>\]}]/g, 'style': 'sh_string' } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 16, 'regex': /(?=\()/g, }, { 'exit': true, 'regex': /(?=\))/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 16, 'regex': /(?=<)/g, }, { 'exit': true, 'regex': /(?=>)/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 16, 'regex': /(?=\[)/g, }, { 'exit': true, 'regex': /(?=])/g, } ], [ { 'exit': true, 'regex': /$/g }, { 'next': 16, 'regex': /(?={)/g, }, { 'exit': true, 'regex': /(?=})/g, } ]
{ // %r(regexp) 'next': 6, 'regex': /%r(?=[\(<\[\{])/g, 'style': 'sh_regexp' }, { // %x(command), %w(array) 'next': 8, 'regex': /%[xWw](?=[\(<\[\{])/g, 'style': 'sh_normal' }, { // %(string), %s(symbol) 'next': 10, 'regex': /%[Qqs]?(?=[\(<\[\{])/g, 'style': 'sh_string' },
[ { 'exit': true, 'regex': /$/g }, { // from 7. next sibling exists. 'next' : 7, 'regex': /(?:\)[^\(\)]*(?=\()|>[^<>]*(?=<)|][^\[\]]*(?=\[)|}[^\{}]*(?={))/g, 'style': 'sh_regexp' }, { // from 7. no next sibling. 'exit' : true, 'regex': /(?:\)[^\)]*\)|>[^>]*>|][^\]]*]|}[^}]*})/g, }, { // from 0. no nesting parenthesis. 'exit' : true, 'regex': /(?:\([^\()]*\)|<[^<>]*>|\[[^\[\]]*]|\{[^\{}]*})/g, }, { // from 0. nesting parenthesis. 'next' : 7, 'regex': /(?:\([^\()]*(?=\()|<[^<>]*(?=<)|\[[^\[\]]*(?=\[)|\{[^\{}]*(?=\{))/g, 'style': 'sh_regexp' } ], [ { 'exit': true, 'regex': /$/g }, { // from 7. next sibling exists. 'next': 7, 'regex': /(?:\)[^\(\)]*(?=\()|>[^<>]*(?=<)|][^\[\]]*(?=\[)|}[^\{}]*(?={))/g, 'style': 'sh_regexp' }, { // from 7. no next sibling. 'exit': true, 'regex': /(?:\)[^\)]*(?=\))|>[^>]*(?=>)|][^\]]*(?=])|}[^}]*(?=}))/g, }, { // from 6. no nesting parenthesis. 'exit' : true, 'regex': /(?:\([^\()]*(?=\))|<[^<>]*(?=>)|\[[^\[\]]*(?=])|\{[^\{}]*(?=}))/g, }, { // from 6. nesting parenthesis. 'next': 7, 'regex': /(?:\([^\()]*(?=\()|<[^<>]*(?=<)|\[[^\[\]]*(?=\[)|\{[^\{}]*(?=\{))/g, 'style': 'sh_regexp' } ], [ { 'exit': true, 'regex': /$/g }, { // from 9. next sibling exists. 'next' : 9, 'regex': /(?:\)[^\(\)]*(?=\()|>[^<>]*(?=<)|][^\[\]]*(?=\[)|}[^\{}]*(?={))/g, 'style': 'sh_normal' }, { // from 9. no next sibling. 'exit' : true, 'regex': /(?:\)[^\)]*\)|>[^>]*>|][^\]]*]|}[^}]*})/g, }, { // from 0. no nesting parenthesis. 'exit' : true, 'regex': /(?:\([^\()]*\)|<[^<>]*>|\[[^\[\]]*]|\{[^\{}]*})/g, }, { // from 0. nesting parenthesis. 'next' : 9, 'regex': /(?:\([^\()]*(?=\()|<[^<>]*(?=<)|\[[^\[\]]*(?=\[)|\{[^\{}]*(?=\{))/g, 'style': 'sh_normal' } ], [ { 'exit': true, 'regex': /$/g }, { // from 9. next sibling exists. 'next': 9, 'regex': /(?:\)[^\(\)]*(?=\()|>[^<>]*(?=<)|][^\[\]]*(?=\[)|}[^\{}]*(?={))/g, 'style': 'sh_normal' }, { // from 9. no next sibling. 'exit': true, 'regex': /(?:\)[^\)]*(?=\))|>[^>]*(?=>)|][^\]]*(?=])|}[^}]*(?=}))/g, }, { // from 8. no nesting parenthesis. 'exit' : true, 'regex': /(?:\([^\()]*(?=\))|<[^<>]*(?=>)|\[[^\[\]]*(?=])|\{[^\{}]*(?=}))/g, }, { // from 8. nesting parenthesis. 'next': 9, 'regex': /(?:\([^\()]*(?=\()|<[^<>]*(?=<)|\[[^\[\]]*(?=\[)|\{[^\{}]*(?=\{))/g, 'style': 'sh_normal' } ], [ { 'exit': true, 'regex': /$/g }, { // from 11. next sibling exists. 'next' : 11, 'regex': /(?:\)[^\(\)]*(?=\()|>[^<>]*(?=<)|][^\[\]]*(?=\[)|}[^\{}]*(?={))/g, 'style': 'sh_string' }, { // from 11. no next sibling. 'exit' : true, 'regex': /(?:\)[^\)]*\)|>[^>]*>|][^\]]*]|}[^}]*})/g, }, { // from 0. no nesting parenthesis. 'exit' : true, 'regex': /(?:\([^\()]*\)|<[^<>]*>|\[[^\[\]]*]|\{[^\{}]*})/g, }, { // from 0. nesting parenthesis. 'next' : 11, 'regex': /(?:\([^\()]*(?=\()|<[^<>]*(?=<)|\[[^\[\]]*(?=\[)|\{[^\{}]*(?=\{))/g, 'style': 'sh_string' } ], [ { 'exit': true, 'regex': /$/g }, { // from 11. next sibling exists. 'next': 11, 'regex': /(?:\)[^\(\)]*(?=\()|>[^<>]*(?=<)|][^\[\]]*(?=\[)|}[^\{}]*(?={))/g, 'style': 'sh_string' }, { // from 11. no next sibling. 'exit': true, 'regex': /(?:\)[^\)]*(?=\))|>[^>]*(?=>)|][^\]]*(?=])|}[^}]*(?=}))/g, }, { // from 10. no nesting parenthesis. 'exit' : true, 'regex': /(?:\([^\()]*(?=\))|<[^<>]*(?=>)|\[[^\[\]]*(?=])|\{[^\{}]*(?=}))/g, }, { // from 10. nesting parenthesis. 'next': 11, 'regex': /(?:\([^\()]*(?=\()|<[^<>]*(?=<)|\[[^\[\]]*(?=\[)|\{[^\{}]*(?=\{))/g, 'style': 'sh_string' } ]
stateいっぱい版の方が素性がいいのは一目瞭然ですね。(;^_^A アセアセ… 書くのにかかった時間は数分の一から十分の一だし、読み返して理解できるのもそっちだし。
ありえない正規表現の方は SHJSのエンジン部分(sh_main.js)を全く利用していないところに複雑さの原因がありそう。括弧の種類ごとに一つの stateが必要でなおかつそれが×3(=12)という stateいっぱい版の見通しに後込みしてこっちの泥沼にはまりこんでいった感じ。
尚どちらも、似てるけどちょっとだけ違うコードがほとんどの部分を占めている。例えば stateいっぱい版の state7-10、state12-15、state17-20の相違点は
'next': 6, // state7-10 'next': 11, // state12-15 'next': 16, // state17-20
の部分だけ。ここを
'next': 'caller'
と書ければ共通化できるのに……。また、state6、state11、state16の違いは
'style': 'sh_regexp' // state6 'style': 'sh_normal' // state11 'style': 'sh_string' // state16
の部分だけここを
'style': 'inherit'
と書ければ共通化できるのに……。
それなら追加部分のサイズが今のほぼ 1/3になったものを。