例えばこのページ http://vvvvvv.sakura.ne.jp/ds14050/diary/20080112-7.html 。Endキーで末尾に移動して PageUpで戻っていくと空白の PREが目に入ると思う。その少し上にはページの内容を覆い隠す黒い領域があるはず。(そうでなければ修正されたのだろう。Firefox2で最初に確認し、Firefox3.0RC1でも直っていなかったが)
大量の PREが存在したり、一つだけでも巨大な PREが存在する場合に起こる様子。innerHTMLで PREの内容を置き換えているのも原因になっているかもしれない。
画面の末端にスクロールした状態でページをリロード(F5 or Ctrl+R)すると下方の PREが正常に表示される反面、上端付近の PREに同じ問題が生じる。遠方の PREの書き換えに問題があるのでは?
真っ白の PREの中で、右クリックしたりテキストを選択したりといったアクションを起こせば正常に表示されることが多い。
あと、PREの中から開始した選択は PREの外に出られなかったり。(これは TEXTAREAと違い PREでは Ctrl+Aで全文選択ができないために用意された代替手段だという気もする)
オリジナルの sh_javascript.jsはコメントの中の URLっぽい部分とメールアドレスっぽい部分をハイパーリンクにしていた。機能が劣るのは遺憾なので sh_javascript.jsと sh_ruby.jsに、コメントと文字列の中の URLっぽい部分をハイパーリンク化する機能を追加した。
その過程で気付いた、一行コメントの終了条件などに使われている $アンカーのマッチングが行われない場合があったのを修正した。(行末に達した時点でマッチングを打ち切っていたのが間違い。$は空文字列にもマッチする。全てのマッチに失敗するまで続ける必要があった)。これは自分が 2008-02-25に持ち込んだバグでオリジナルには存在しない。
# http://vvvvvv.sakura.ne.jp/ds14050/badboy/log/ How is this line highlighted ?
最終更新: 2009-09-01T05:05+0900
このようにハイライトされます。
'%04d-%02d-%02d' % [2008, 5, 8]
(整形した)HTMLソースはこう。
<span class="sh_string">'%04d-%02d-%02d'</span> <span class="sh_string">% [2008, </span> <span class="sh_number">5</span> <span class="sh_symbol">,</span> <span class="sh_number">8</span> <span class="sh_cbracket">]</span>
「% [2008, 」が一つの文字列にされてしまっている。どういう判断なのかと調べれば、%!string! と同じものだと見なされていた。(そのルールは自分で書いたんだけども)
知っていたでしょうか? %リテラルの区切りには空白(改行も!)が使えるのでした。(alnumと mbchar以外なら OKっぽい。変態すぎるよ)
最終更新: 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使い以外には)スコープの誤解を招きやすいという理由で、避けなければいけないものの筆頭ともいえる。
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)
ファイルはこちら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になったものを。
以下、変更点のリスト。(\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 },
文字列リテラルの終了条件に上のは必要ない、むしろこれがあることで複数行にまたがったリテラルを正しく認識できない、のだけど強力すぎる正規表現は誤認識があったときにソースを最後まで一色に染めてしまう危険性があるのでそのままにしている。ヒアドキュメントに対応しないのも同じ理由。
{ // %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";
やっぱりダメだ〜。
上で出した「こんなのも通る」と「やっぱりダメだ〜」の例が、言葉とは裏腹に「通ってない」と「ちゃんとできてる」状態になってると思う。だとしたら成功。
変更点は20080102p01で。
# for variable interpolation, #{ is not a comment
というコメントを付けて #{}のハイライトルールを定義しているにも関わらず、それが #コメントルール よりも後ろにあるために機能していなかった。#コメントルールを後ろに持ってきて解決。
続きは20080105p01で。
最終更新: 2010-01-06T04:21+0900
こちらを参考にしました。http://www.revulo.com/blog/?date=20070817#p01
http://shjs.sourceforge.net/doc/download.html の download a binary distribution をたどってダウンロードした ZIPファイルを tDiaryのインストールディレクトリの下に展開する。
今日まで複数行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に従って構文を色分けしてくれるというわけだ。
SHJSのスタイルシートとスクリプトを日記に埋め込むためのプラグイン。SHJSに同梱されているたくさんの CSSファイルのプレビュー機能が欲しくて設定画面も作った。
日記の中で明示的に呼び出して使うプラグインではないので、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
プラグインメソッド 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を発生させるとその日の日記がなかったことになるので、避けられる危険は避けるに越したことはない。
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のレイアウトを優先するつもりがスクリプトの実行を優先する結果になってしまった。
<link>は <head>内に置かなければいけない。
ハイライト機能を使ったときだけ SHJS関連の CSSや JSファイルを参照したいから footer_procで <link>や <script>を出力しているが、これは本文が評価される前の header_procの時点*では SHJSが使われているか否か判断できないからこうなっている。
Firefox2も IE7もよきに計らってくれるので実害はない。
--- 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で初期化とかしてるとはまる。