/ 最近 .rdf 追記 設定 本棚

脳log[SHJS: 2008-02-12~]



2008年02月12日 (火) エクスプローラがメモリを放さない。プライベートワーキングセットは 14MBでも、Vistaより前のタスクマネージャが表示していた数字では 215MBになる。仮想メモリ不足ってメッセージが何度も出るんですけどー。

最終更新: 2010-03-21T03:27+0900

[javascript][SHJS] JSLint <http://www.jslint.com>

SHJSの sh_main.jsを高速化したことを以前書いた。(20080204p01)

 対応ブラウザ

SHJSのページには動作を確認したブラウザとして以下が挙げられている。

  • Firefox 2
  • Internet Explorer 6
  • Internet Explorer 7
  • Opera 9
  • Safari 3

sh_main.jsの修正版は Firefox2IE7Opera9で正しく動作することの確認と速度の比較を行っている。

IE6での確認は IE7から戻すのが面倒なので省略する。

Sarari3は Vistaで動くものがダウンロードできるので確認してみたところ動いた。(表示も正常)

いじったことで対応ブラウザが減っていなくて良かった。(IE6は?)

 JSLint

SHJSの作者は Code Conventions for the JavaScript Programming Languagejslint: 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_;
 いいわけ
  • ローカル変数の再宣言に関するものは全て forループで初期化される変数が原因。(i と node)
  • 「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の配列内包に書き換えるのには吝かでない)

  • != 0 の代わりに !== 0 と書け? 書き換えましたm( __ __ )m

(一つを除いて) 無視できる警告*ばかりで良かった。

* 無視したら警告の意味がない。forループの変数なんて(古い VC使い以外には)スコープの誤解を招きやすいという理由で、避けなければいけないものの筆頭ともいえる。


2008年02月04日 (月)

[SHJS][javascript] sh_main.jsの高速化

SHJSのブラウザでの実行時間を削るには sh_main.js(SHJSのメインスクリプト)を速くするか、正規表現を効率的なものにする方法がある。(>遅い正規表現(20080116p01))。

正規表現に関してできることは限られるうえ、知識も少ない(『詳説 正規表現 第三版』待ち)ので、可能な限り文字クラスや文字集合といわれるものを使うように気を付けただけにとどまる。(sh_ruby.js, sh_javascript.js)

メインスクリプトの sh_main.jsに対してできることは多い。この日記の現在?の最新ページ(2008年1月12日から7日間)を表示して、sh_highlightDocument()前後での経過時間を表示したところこのようになった。

Firefox2IE7(64-bit)IE7(32-bit)Opera9.25
sh_main.js (0.4.2)935ms1050ms1270ms1260±150ms
改変版600ms680ms865ms1200±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を使わないし、詳しくもない。むしろ Operaではキーボードを使ったブラウジングもままならない。そんな人間が Firefox+Firebugを頼りに sh_main.jsの修正を行ったので Operaの速度が改善しないのは仕方のない部分がある。(IEは改善したが)。(あんだけいじってトータルで変わらない方がすごい。どこが足を引っぱっているのだろう)。リテラル文字列と Stringオブジェクトの差が他のブラウザより大きいらしいが、それが原因?

EfficientJavaScript - Dev.Opera - 効率的な JavaScript (www.hyuki.com)

Operaでの JavaScriptの実行時間が他のブラウザに比べて長いのははっきりした理由があって、Operaはスクリプトが全力疾走中であってもユーザーの操作に対する反応を後回しにしたりしない。これは偉い。ユーザーを待たせない代わりにスクリプトが遅れるのは当然の代償で仕方がない。

あ、スクリプトでなく再描画が律速してるから改善しないということ?

 (かんせいしたへびのえにあしをかきくわえるこうい、とまでは言わないが) この日記で使用中のファイルへのリンク

(常に最新版だが一時的にバグが混入していることがあるかも)

 追記@今日:sh_main.jsをちょっと修正。

すぐ上のリンク先はすでに変更が反映されている。

これら二つの記事を参考に 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の中身がつづく…… */
  };
})();

まあ、速度が改善するわけではないので、書き直さないんだけど。

 追記@2008-02-25

innerHTMLや textContent、innerTextの使用は堕落だという気もするが、冗長な上に呼び出しを重ねることで遅くなる DOMメソッドがいけない。


2008年01月16日 (水) Pythonかわいいよ、Python

[SHJS][SakuraEditor][javascript] SHJSと SakuraEditor用のハイライトルールファイル

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)


2008年01月05日 (土)

[SHJS][javascript]SHJS Ruby定義ファイル(sh_ruby.js) 私的改訂版 まとめ。(2008-01-17 最終更新)

ファイルはこちら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'

 必ず最初に実行される処理(BEGIN)、最後に実行される処理(END、at_exit)、スクリプトを終了する(exit)など無視できない働きを持ったメソッドをハイライト。

+    { // 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'
     },

 \bの後ろの +-は絶対にマッチしないので前に出してマッチするように。

 数値リテラルの定義が cpp,java,pascal,perl,php,prolog,python,sh,sql,tclと共通だったので Ruby専用に。(先頭、末尾や連続するアンダースコアやが許容されているがべつに Rubyインタープリタじゃないので気にしない)

     {
-      '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'
     },

 javascriptのオブジェクトリテラルのプロパティ名部分は引用符を省ける(ので省く)。

     {
-      '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'
     },

 <<メソッドや <<HEREDOCUMENTに誤ってマッチしないように、HTMLタグっぽいものだけをハイライト(そもそもなんで <hoge>を文字列としてハイライトするのかは GNU Source-highlightのみぞ知る)。

     {
-      'next': 4,
-      'regex': /</g,
-      'style': 'sh_string'
+      next: 4,
+      regex: /<(?=[\w\/])/g,
+      style: 'sh_string'
     },

 /regexp/i 型の正規表現リテラルの条件を厳しくして URLに誤ってマッチしないように。またオプション部分もマッチに含めるように。

     {
-      'regex': /\/[^\n]*\//g,
-      'style': 'sh_regexp'
+      regex: /\/(?:\\.|[^\n\\\/])*\/[eimnosux]*(?![A-Za-z])/g,
+      style: 'sh_regexp'
     },

 別途定義したので不完全な %r{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")は現状でも、コロンがsh_symbol、その後ろが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'

 ::hogeを Symbolと誤認しないように、先に ::を sh_symbolにしてしまう。

 プロパティ名部分の引用符を省略

+      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'するときの 'style'指定はなくてもかまわないので削除。

 不要なグループ化とエスケープを削除。(?:\=end)

   [
     {
-      '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
+    }
   ]
 ];

2008年01月03日 (木)

[SHJS][javascript] jsmin.js

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で完結して満足です。

[SHJS][javascript]まだまだいじってます。>SHJS | \bを正しく使用 & わずかに減量

ファイルはこちら。20080101p01

頭の方から変更点を見ていく。

 #includeに相当するもの (sh_preproc)

-      'regex': /\b(?:require)\b/g,
+      'regex': /\brequire\b/g,

require一つだけだからかっこで囲む必要はない。

 使用頻度は低いけど無視できないメソッドたち (sh_preprocを流用)

-      '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!の !の直前にマッチし、?の後や !の後にはマッチしないので正しくマッチするように修正。

 シンボル (sh_string)

-    { // 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

の整合性をとるため。

 正規表現リテラル (/regexp/i、sh_regexp)

-      'regex': /\/[^\n]*\//g,
+      'regex': /\/(?:\\.|[^\n\\\/])*\/[eimnosux]*(?!\w)/g,

正規表現リテラルのオプション部分もマッチに含めるように。あと条件を厳しくしたので URLに誤マッチすることが減るはず。

 制御構造と定義に関わるキーワードやメソッド (sh_keyword)

-      '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へ持ってきた。どちらもメソッドになりうる重要な要素だと思うから。

 定数 (sh_function)

-      '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,

 よくわからないもの (sh_normal)

-      'regex': /[a-z0-9_]+(?:\?|!)/g,
+      'regex': /\b[a-z0-9_]+[!\?]?/g,

末尾が ?や !のメソッドだけを拾い上げたかったのだろうか?ローカル変数っぽいものにもマッチするようにしたけど、どのみち色はつかないので害はない。因みに文字配列リテラル( %w(one two three) )も適切なクラスが見つからなかったので sh_normalにしている。

 文字列リテラルとタグとコメント(=begin〜=end)

-      'style': 'sh_string'
-      'style': 'sh_string'
-      'style': 'sh_string'
-      'style': 'sh_commend'

'string'、"string"、<tagname>、=begin〜=endの終了条件部分から styleを取り除く。なくても出力は変わらない。それにしても HTMLタグっぽいものにマッチするルールがあるのはなぜだろう。Web用言語だと思われてるのかな?(<stdio>や <stdlib> のたぐいの可能性もある)。不都合はないので消さないけど。


2008年01月02日 (水)

[SHJS][tDiary][javascript] SHJSの Rubyルールを %[〜]に対応

20080101p01からの続き。正式な sh_ruby.js (私的改訂版)はそちらから。

機能は同じ(はず)なのになぜか全く様子の違う二つのスクリプトができてしまった。こんな感じ。

 Rubyの %記法対応 (stateいっぱい版)

    { // %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,
    }
  ]

 Rubyの %記法対応 (ありえない正規表現版)

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


2008年01月01日 (火) Application Dataなんてフォルダを掘って一人で中に収まってる Operaは恥を知れ (Vistaでの話)

[SHJS][Ruby][tDiary] SHJSの Rubyルールを修正。(sh_ruby.js, sh_ruby.min.js)

以下、変更点のリスト。(\bの使い方が適当なのでスペースの少ないソースで問題が出る可能性あり。\bの使いどころが全然わかってないせい)

    { // part of Kernel methods.
      '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,
      'style': 'sh_preproc'
    },

なくてもいいかな、と思うけど defined?と Kernelモジュールのメソッドの一部を sh_preprocとして追加。Rubyで sh_preprocなのは requireだけなので sh_preprocの配色を流用した。選んだのは abort、callcc、exit、fork、systemなど比較的重要そうなもの。(loopなど一部の他のメソッドは sh_keywordとして既に分類されている)

    {
      'next': 4,
      'regex': /<(?=[\w\/])/g,
      'style': 'sh_string'
    },

正規表現を /</g から変更。<<メソッドやヒアドキュメント(<<HOGE)にマッチしないように。

    { // Symbol
      'regex': /:(?:(?:@@|@|\$)?\w+[\?!]?|\+=?|!=?|~|\*\*=?|-=?|\*=?|\/=?|%=?|<<=?|>>=?|&=?|\|=?|^=?|>=?|<=?|<=>|===?|=~|!~|&&=?|\|\|=?|\.\.|\.\.\.|=)(?=\s|$)/g,
      'style': 'sh_string'
    },

新ルール。シンボル(:hoge)を sh_stringとして色付け。

    { // %!string!
      'regex': /%[Qq]?([!-'*-\/:;=?^]).*?\1/g,
      'style': 'sh_string'
    },

新ルール。%!string!、%Q!string!、%q!string!を sh_stringとして色付け。残念ながら %Q[]のように括弧を使ったものは入れ子になった括弧を数えられないので非対応。対応した。詳しくは下の方。

    {
      '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,
      'style': 'sh_keyword'
    },

ここにはプログラムの流れや定義に関するキーワードや Kernelメソッドが集められているようなので、既に登録されている ENDと同じ働きの at_exitを追加し、definedを削除(上で sh_preprocとして defined?を登録済み)、false、nil、self、true、__FILE__、__LINE__を削除し、あとで定数として定義。&& と || を and、orに対応するものとして追加。

    { // global variables
      'regex': /\$(?:[_&~`'\+\?!@=\/\\,;\.<>\*\$:"]|-?[A-Za-z0-9_]+)/g,
      'style': 'sh_type'
    },

グローバル変数の定義を追加。sh_typeはインスタンス変数やクラス変数のクラス名として使用されているもの。

    { // Constants
      'regex': /\b[A-Z]\w+[!\?]?(?=\b|$)/g,
      'style': 'sh_function'
    },
    { // Constants
      'regex': /\b(?:false|nil(?!\?)|true|self|__FILE__|__LINE__)(?=\b|$)/g,
      'style': 'sh_function'
    },

定数のルールを追加。sh_functionは Rubyでは使われていないクラス。

    {
      'regex': /[a-z0-9_]+(?:\?|!)/g,
      'style': 'sh_normal'
    },

正規表現を /[A-Za-z0-9_]+(?:\?|!)/g から変更。定数は区別したいじゃない。

 余談

    {
      'exit': true,
      'regex': /$/g
    },

文字列リテラルの終了条件に上のは必要ない、むしろこれがあることで複数行にまたがったリテラルを正しく認識できない、のだけど強力すぎる正規表現は誤認識があったときにソースを最後まで一色に染めてしまう危険性があるのでそのままにしている。ヒアドキュメントに対応しないのも同じ理由。

 追記@2008-01-02:括弧を使ったリテラルにも対応した

    { // %r(regexp)
      'next': 6,
      'regex': /%r[\(<\[\{]/g,
      'style': 'sh_regexp'
    },
    { // %x(command), %w(array)
      'next': 7,
      'regex': /%[xWw][\(<\[\{]/g,
      'style': 'sh_normal'
    },
    { // %(string)
      'next': 8,
      'regex': /%[Qq]?[\(<\[\{]/g,
      'style': 'sh_string'
    },
  [
    {
      'exit': true,
      'regex': /$/g
    },
    {
      'next': 6,
      'regex': /[\(<\[\{]/g,
      'style': 'sh_regexp'
    },
    {
      'exit': true,
      'regex': /[)>\]}]/g
    }
  ],
  [
    {
      'exit': true,
      'regex': /$/g
    },
    {
      'next': 7,
      'regex': /[\(<\[\{]/g,
      'style': 'sh_normal'
    },
    {
      'exit': true,
      'regex': /[)>\]}]/g
    }
  ],
  [
    {
      'exit': true,
      'regex': /$/g
    },
    {
      'next': 8,
      'regex': /[\(<\[\{]/g,
      'style': 'sh_string'
    },
    {
      'exit': true,
      'regex': /[)>\]}]/g
    }
  ],

括弧の対応をチェックすることはするけどカッコの種類を区別しないので

%(foo{bar)baz}

こんなのも通る。でも現実的には区別する必要ないよね。HTML断片を組み立てるときに問題がありそう。そしてそういうときにこそダブルクォーテーションを使わずに %[]を使うんだよね。試してみる。

html << %[<option value="#{h hoge}"] << (selected? ? ' selected="selected">' : '>') << h(hoge) << "</option>\n";

やっぱりダメだ〜。

 追記@2008-01-02:括弧を使ったリテラルに正式に対応した

上で出した「こんなのも通る」と「やっぱりダメだ〜」の例が、言葉とは裏腹に「通ってない」と「ちゃんとできてる」状態になってると思う。だとしたら成功。

変更点は20080102p01で。

 追記@2008-01-05:#コメントと #{interpolation}の順番を入れ替え

# for variable interpolation, #{ is not a comment

というコメントを付けて #{}のハイライトルールを定義しているにも関わらず、それが #コメントルール よりも後ろにあるために機能していなかった。#コメントルールを後ろに持ってきて解決。

続きは20080105p01で。


2007年12月30日 (日)

最終更新: 2010-01-06T04:21+0900

[tDiary][SHJS] SHJS - Syntax Highlighting in JavaScriptでシンタックスハイライト

こちらを参考にしました。http://www.revulo.com/blog/?date=20070817#p01

 追加や変更が必要なファイルとディレクトリの一覧。

shjs/sh_main.min.js
SHJSのメインスクリプト。
shjs/sh_style.css
デフォルトのハイライトテーマ。
shjs/lang/*.min.js
各種言語用の色分け定義ファイル。
shjs/css/*.css
切り替え可能なテーマ集。
misc/lib/hikidoc.rb
(tDiary-2.2.0からこの位置に存在する) Wikiスタイルが利用するライブラリ。
misc/plugin/shjs.rb
これから書く tDiaryプラグイン

 shjsディレクトリについて

http://shjs.sourceforge.net/doc/download.html の download a binary distribution をたどってダウンロードした ZIPファイルを tDiaryのインストールディレクトリの下に展開する。

  • shjs、shjs/lang、shjs/cssディレクトリのパーミッションには x が必要。(ブラウザの要求に応じて HTTPサーバーが中の個別のファイルにアクセスできなければいけないから)
  • cssファイルと jsファイルのパーミッションには r が必要。(ブラウザの要求に応じて HTTPサーバーがファイルの内容を読めないといけないから)
  • なお、shjsディレクトリの位置はパーミッションを満たしてさえいれば index.rbと同じディレクトリに限らずどこでも良い。あとで tDiaryの設定画面から、設定をデフォルトから変更する必要が生じるが。

 misc/lib/hikidoc.rbについて

今日まで複数行PRE記法の存在すら知らなかったわけだけど、hikidoc.rbには複数行PREにシンタックスハイライト機能を簡単に追加するためのコードが既に存在していた。(参照:http://kazuhiko.tdiary.net/20060915.html#p01)

けれど、もう SHJSを使うことに決めているので、その部分の二行をコメントアウトしてその下に一行付け加えた。

  def parse_pre( text )
    ret = text
    ret.gsub!( /^#{MULTI_PRE_OPEN_RE}[ \t]*(\w*)$(.*?)^#{MULTI_PRE_CLOSE_RE}$/m ) do |str|
      begin
        raise if $1.empty?
#        convertor = Syntax::Convertors::HTML.for_syntax($1.downcase)
#        "\n" + store_block( convertor.convert( unescape_html( restore_pre( $2 ) ) ) ) + "\n\n"
         "\n" + store_block( %Q[<pre class="sh_#{$1.downcase}">%s%s</pre>] % [parse_plugin( %Q{{{ shjs('#{$1.downcase}') }}} ), restore_pre( $2 )] ) + "\n"
      rescue

これにより

<<<ruby
ruby script here
>>>

<pre class="sh_ruby">
ruby script here
</pre>

へと変換される。あとはブラウザが javascriptに従って構文を色分けしてくれるというわけだ。

 misc/plugin/shjs.rbについて

SHJSのスタイルシートとスクリプトを日記に埋め込むためのプラグイン。SHJSに同梱されているたくさんの CSSファイルのプレビュー機能が欲しくて設定画面も作った。

shjs.rb 設定画面

日記の中で明示的に呼び出して使うプラグインではないので、shjs.rbを有効にして一度 設定を済ませてしまえば、あとは複数行PRE記法で言語名を指定したときに勝手に構文がハイライトされる。(ではどこで呼び出されるのかといえば、前項の misc/lib/hikidoc.rbに忍ばせてあったのだ)

動作テストもかねて shjs.rbの全文を貼り付けてみる。(後半は Rubyスクリプトというより HTMLなんだけど、なんで HTMLタグがうまく色づけされてるんだ?)

def shjs_init
	@shjs_required_langs = [];
	'';
end

def shjs(lang, code=nil)
	@shjs_required_langs.push(lang) if(@shjs_required_langs and not @shjs_required_langs.include?(lang));
	return code.nil? ? '' : %Q[</p>\n<pre class="sh_#{h lang}">#{h code}</pre>\n<p>];
end

def shjs_footer
	return (@shjs_required_langs && !@shjs_required_langs.empty?) ? <<"HTML" : '';
<link rel="stylesheet" type="text/css" href="#{h shjs_style_url}">
<script type="text/javascript" src="#{h shjs_js_url}"></script>
#{@shjs_required_langs.sort.map{|lang|
%Q[<script type="text/javascript" src="#{h shjs_js_url(lang)}"></script>]
}.join("\n")}
<script type="text/javascript">
	sh_highlightDocument();
	sh_languages = null;
</script>
HTML
end

def shjs_style_url(css=@options['shjs_style'])
	url = '';
	url << (@options['shjs_url'] || 'shjs');
	url << (css ? "/css/#{u css}.css" : "/sh_style.css");
	return url;
end

def shjs_js_url(lang=nil)
	url = '';
	url << (@options['shjs_url'] || 'shjs');
	url << (lang ? "/lang/sh_#{u lang}.min.js" : '/sh_main.min.js');
	return url;
end

add_header_proc{
	shjs_init;
	'';
}

add_footer_proc{
	shjs_footer;
}

if(@mode.index('conf'))

	def shjs_csslist
		unless(@shjs_csslist)
			@shjs_csslist = [];
			Dir.chdir("#{@options['shjs_dir'] || 'shjs'}/css"){
				Dir.glob('*.css').sort.each{|css|
					@shjs_csslist.push(css.chomp('.css'));
				}
			}
		end
		return @shjs_csslist;
	rescue Exception
		@shjs_csslist_errmsg = $!.to_s;
		return [];
	end

	def shjs_saveconf
		@conf['shjs_style'] = @cgi.params['shjs_style'][0].to_s;
		@conf['shjs_url'] = @cgi.params['shjs_url'][0].to_s.chomp('/');
		@conf['shjs_dir'] = @cgi.params['shjs_dir'][0].to_s.chomp('/');
		%w(shjs_style shjs_url shjs_dir).each{|key|
			@conf.delete(key) if(@conf[key].empty?);
		}
	end

	add_conf_proc( 'shjs', 'SHJS シンタックスハイライト', 'theme' ){
		shjs_saveconf if(@mode == 'saveconf');

		shjs_init; shjs('ruby');
		<<-"CONFFORM".sub('RUBYSCRIPT', h(<<-'RUBYSCRIPT'.gsub(/^\t+/, '')))
		<h2 class="subtitle">SHJS - Syntax Highlighting in JavaScript</h2>
			<p>http://shjs.sourceforge.net</p>

		<h3>配色設定 (shjs_style)</h3>
			<p><select name="shjs_style"><option value="">sh_style</option>#{
					shjs_csslist.map{|style| %Q[<option#{' selected="selected"'if(style==@conf['shjs_style'])}>#{h style}</option>] }.join('')
				}</select></p>
			<p>サンプル Rubyスクリプト</p>
				<pre class="sh_ruby">RUBYSCRIPT</pre>
			<p>デフォルトは <nobr>#{h @conf.base_url}shjs/sh_style.css</nobr></p>
			<p>すこし上にスタイルシートのリストが表示されていないときは shjs_dirを先に設定してください。</p>
			<p><strong>#{@shjs_csslist_errmsg}</strong></p>
		<h3>SHJSをインストールしたフォルダ (shjs_dir)</h3>
			<p>sh_main.min.jsファイルと sh_style.cssファイル、cssフォルダと langフォルダが入ったフォルダです。</p>
			<p>デフォルトは <nobr>#{h Dir.pwd}/shjs</nobr></p>
			<p><input name="shjs_dir" type="text" value="#{h @conf['shjs_dir']}" style="width:90%"></p>
		<h3>shjs_dirにブラウザでアクセスするときの URL (shjs_url)</h3>
			<p>デフォルトは <nobr>#{h @conf.base_url}shjs</nobr></p>
			<p><input name="shjs_url" type="text" value="#{h @conf['shjs_url']}" style="width:90%"></p>
		CONFFORM
			$KCODE = 's'
			require 'shjs'
			def say_hello
			   puts :hello
			end
			10000.times{ say_hello }
			%w[this is not string literal but array.]
			if @mode =~ /\A(save)?conf\z/i
			   # show config form.
			end
			exit unless defined? Const;
		RUBYSCRIPT
	}
end

 追記@2007-12-31:hikidoc.rbをいじらない方法

プラグインメソッド shjs()に二番目のパラメータ(code)を追加。これで、SHJSをサーバーへコピーして、shjs.rbを有効化&設定するだけでシンタックスハイライトを使うことができる。hikidoc.rbの変更は不要。

{{shjs 'ruby', <<RUBY
print "hello"
print "hello"
print "hello"
RUBY}}

と書くと

print "hello"
print "hello"
print "hello"

こうなる。複数行PRE記法よりタイプ数は多くなるけど、プラグインの枠内におさまってるので tDiary本体をいじるハードルはなくなった。この前書いたようにスタイル関連のスクリプトで迂闊に NameErrorを発生させるとその日の日記がなかったことになるので、避けられる危険は避けるに越したことはない。

 追記@2008-01-02

shjs.rbで埋め込む SCRIPTタグに defer="defer"を付けようかと思っていたら逆に window.onloadで実行していた sh_highlightDocument() をその場で実行することに。

window.onloadって画像も含めた全ての外部ファイル読み込みが終わってから呼ばれるんだよね。でもページが読める状態になっても読み込みが完了していないことってザラにあるわけで、それじゃ遅い。色のついてないコードを見せてしまうことになる。

今度 sh_highlightDocument()を呼ぶ場所(タイミング)は HTMLのほとんど末尾なので操作対象の DOMは既にアクセス可能になっている。問題なし。ではスクリプトの方は?

sh_highlightDocument()を定義する sh_main.js(sh_main.min.js)ファイルの評価が終わる前に sh_highlightDocument()を呼ぶことはできない。sh_main.min.jsを読み込む SCRIPTタグに試しに defer="defer"を付けたら Firefoxでは問題はなかったが IE7は期待通りにエラーを出してくれた。defer="defer"を付けるわけにはいかない。(付けなければ大丈夫なのかは別の問題だけど、今のところエラーは出ていない)

そんなわけで HTMLのレイアウトを優先するつもりがスクリプトの実行を優先する結果になってしまった。

 追記@2008-01-10:<link>要素の位置が文法違反

<link>は <head>内に置かなければいけない。

ハイライト機能を使ったときだけ SHJS関連の CSSや JSファイルを参照したいから footer_procで <link>や <script>を出力しているが、これは本文が評価される前の header_procの時点*では SHJSが使われているか否か判断できないからこうなっている。

Firefox2も IE7もよきに計らってくれるので実害はない。

 追記@2008-01-12: hikidoc.rb (VERSION 0.0.2)を変更する場合は……

--- hikidoc.rb.002	Sun Jan 13 01:19:03 2008
+++ hikidoc.rb	Sun Jan 13 04:02:25 2008
@@ -668,6 +669,12 @@ class HikiDoc
       syntax = info ? info.downcase : nil
       if syntax
         begin
+          # Use "Syntax Highlighting in JavaScript"
+          #  instead of Syntax::Convertors::HTML.
+          @f.print %Q(<pre class="sh_#{escape_html_param syntax}">), text(str), "</pre>\n"
+          @f.puts inline_plugin(%Q(shjs #{syntax.dump}))
+          return  
+
           convertor = Syntax::Convertors::HTML.for_syntax(syntax)
           @f.puts convertor.convert(str)
           return

* Hikiでは header_procの呼び出しより本文の評価の方が早い気がする。header_procで初期化とかしてるとはまる。