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になったものを。