最終更新: 2016-11-12T11:41+0900
Rubyの、括弧を使った %記法だって。
irb19> re = /%[Qq]?(?<brace>\{[^\{}]*(?:\g<brace>[^\{}]*)*})/ irb19> strings = %w(%{z}a %{a{b}z}c %{a{b}c{d{e}f}z}g %{{{{}}}z}a %{a{b}c %{z}a}b) irb19> strings.each{|str| p str[re] } "%{z}" "%{a{b}z}" "%{a{b}c{d{e}f}z}" "%{{{{}}}z}" nil "%{z}" => ["%{z}a", "%{a{b}z}c", "%{a{b}c{d{e}f}z}g", "%{{{{}}}z}a", "%{a{b}c", "%{z}a}b"]
どの例も正しい範囲( %{ から z} まで)を切り取っているのがわかる。
/%[Qq]?(?<brace>\{(?:[^\{}]++|\g<brace>)*})/
若干速い。同じパターン( [^\{}] )の繰り返しも存在しない。http://fleur.hio.jp/perldoc/perl/5.10.0/pod/perl5100delta.mix.html#Regular_expressions を参考にした。
/%[Qq]?(?<brace>\{(?:[^\{}]+|\g<brace>)*})/
上のものの + が一つ落ちたもの。開き括弧が余分にある文字列を食わせると待てども待てども返ってこない。 http://mlog.euqset.org/archives/ruby-list/42232.html で既に書かれている。それに対する返答が http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/42233 。
最終更新: 2016-11-12T11:41+0900
ブラウザの等幅フォントを Consolasにした(20080110p01)だけではあきたらずエディタのフォントも Consolasにしようと思ったが……。
Firefoxは FontLinkを設定しなくても Consolasに足りない文字を他のフォントで補完してくれた。SakuraEditorは化けた。レジストリをいじって FontLinkを有効にして Windowsを再起動しても化けた。でも Unicode版の SakuraEditorなら化けなかった。
Unicode版はまだ SakuraEditorのメインストリームではない(というかバグバグ&レイアウト遅い)が、フォントのために乗り換えた。MSゴシックには戻りたくない。
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontLink\SystemLink] Consolas | REG_MULTI_SZ | M+1VM+IPAG-circle.ttf
http://sourceforge.net/tracker/index.php?func=detail&aid=1832567&group_id=12488&atid=312488
最終更新: 2016-11-12T11:41+0900
テキストエディタは、メモ帳、TeraPadときて、現在は SakuraEditorを、もう五年以上使っている。xyzzy(正式な読み方はないらしい。ジッズィと読んでいる。saxyunの読みがサッキュンなのは詐欺だと思います)や Meadowに移る気は無し。
構文ハイライトには興味がなくて、どこかからダウンロードしてきた強調キーワードファイルや正規表現キーワードファイルを使っていた。間違いがあっても正規表現を直したりはせず、オフにするだけ。邪魔にならなければいい程度の扱いだった。
ところが最近 SHJSをいじってたこともあって中途半端なハイライトが許せなくなってしまった。
そのためには鬼車が必要。BREGEXP.dllでは %Q<a<b<c>d<e>f>g> のように入れ子になった括弧の対応を調べられないので。
キーワードファイルはこの日記の SHJS(with カスタマイズ版 sh_ruby.js)を使ったハイライトとコンパチ。ただし完全移植ではない。サクラエディタの正規表現キーワードは正規表現一発で、マッチした全体を特定の配色にするだけだけど、SHJSはパターン中のキャプチャグループごとに配色を分けることができる。サクラエディタの制限で、正規表現が行をまたいでマッチできないことも影響がある。正規表現マッチが行をまたげないのは SHJSも同じなのだが、SHJSは正規表現キーワードから利用できるスタックを用意していて、行を超えて色指定を継続できる。
Firefoxも IE7も可能な場所はすべてメイリオを使うことにしているが、Serif(明朝)と等幅フォントをメイリオにするわけにはいかない。いままで等幅フォントはデフォルトの MS ゴシックのままにしていたのだが、よく耐えられたものだと思う。日本語部分は MS ゴシックなりなんなりに頼らなければならない*にしても英字部分の選択肢は無数にあるじゃないか、ということに気付いてしまった。
http://www.codestyle.org/css/font-family/sampler-Monospace.shtml
これはもう Bitstream Vera Sans Monoしかない。Nimbus Mono Lも悪くはないけど、Bitstream Vera Sans Monoは線が太めで、MSゴシックみたいに掠れた感じが全くしないのがいい。他のフォントはヒゲが多かったり、四角かったりで好みではない。
Firefoxで Bitstream Vera Sans Monoの文字をどれだけ拡大しても太字と普通の字が区別できないのはフォントの問題じゃない気がしてきた。ノーマル、太字、斜体、太字&斜体の四種がインストールされてるのは確認してるし、フォントビューアでは区別できるのに、Firefoxでは区別できない。
……。Firefoxを再起動したら区別できるようになった。フォントをインストールした後は一回再起動した方がいいみたい。
フォントの選択肢が少なすぎる。Bitstream Vera Sans Monoも設定できなかった。それに日本語のページでのデフォルトのフォントは指定できるのに英語のページでのフォントが指定できないのはなんで?⁑
等幅でも明朝でもゴシックでもない。そもそも Googleの検索ボックスの中の文字があまりに見苦しかったから新しい等幅フォントを探してきたというのに。どのフォントの設定をいじっても変化しない。ユーザースタイルシートでなんとかなるか?
……。userContent.cssに
input { font-family: monospace }
を追加したら Bitstream Vera Sans Monoになった。ヽ(^0^)ノわーい
なんでもかんでも monospaceにしたら場所(幅)をとって仕方ないのでやっぱりちょっと変更。
input { font-family: monospace } input[type] { font-family: sans-serif } input[type=text], input[type=file] { font-family: monospace }
Consolasは Vistaに付いてくる、Bitstream Vera Sans Monoの左右を詰めたようなフォント。空間効率って大事。(自分は気にしないけど、ゼロの形もシータの大文字みたいなのから一般的な斜線の入ったものになっている)
body { padding : 0 2.5% ; font-family : 'Verdana', 'Osaka', 'MS UI Gothic', sans-serif ; font-size : 100% ; }
Verdanaは嫌いじゃないよ、プロポーショナルだから Sakura Editorでは使えないけど。MS UI ゴシックも悪くない。メイリオ以前はこれを常用していた。sans-serif(ゴシック)も serif(明朝)より断然好きだ。
でもフォントは自分で(見る人間が)選ぶ。ファウスト並みにこだわってるのならその意志は尊重する(けど読みにくかったら読まない)けれども。
ファイルはこちら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で。
makerss.rbプラグインを有効にして、index.rbと同じ場所に空の index.rdfをアップロードしただけ。Firefoxのアドレス欄の右端にオレンジの電波マークが出現した。HTML中に rdfタグを埋め込むのだけは(たとえ HTMLの文法を満たしていようとも)いやだと思ってた*が下の一行が付け加えられただけのよう。
<link rel="alternate" type="application/rss+xml" title="RSS" href="http://vvvvvv.sakura.ne.jp/ds14050/diary/index.rdf">
実はフィード、RSS、rdf、Atomと新出単語が多くてよくわかっていない。lirsも含めて全部同じか全部違うものだと思っていて、どちらにしてもゲンナリだな、と。
過去の日記を「編集」するとその日の全てのセクションが rdfに上ってくるのね。tDiary-2.3系の目玉はセクション単位での編集機能だなあ。blogスタイルではできるんだろか? xmlrpc.rbではできるみたいだけど、日記更新のクライアントはブラウザという点は譲れない(下書きをするときはエディタを使う)。
パッと思いついたのは複数人で一つの日記を書いてる場合の更新の競合(セクションの削除と挿入が一番の問題。⁑)とかインターフェイスが分かりにくくならないかという問題。個人でちょちょいと自己責任でやる分には目をつむって(つぶる?)しまえるけど……。
一つ目のページで
- たぶん2.3行き
- 実装の要望が高いためいちおう検討されているもの
- モバイルモードでのセクション単位の編集(携帯で全文編集できないため)
RSS方面からの要望はないの? 「編集」するときは必ず「ちょっとした修正(RSSを更新しない)⁂」にチェックを入れてるって?
* テキスト形式のものを二回繰り返し、その後に HTML形式のものをくっつけた 80KB近いメールマガジンを送ってくる通販サイトがあるけれど、読めない無駄なものがくっついてるという点が似ている。そのメール、ソースを見ると vbCrLfとか見えてんだけど…… > Web!keさん
⁑ 日付とセクションナンバーに加えてサブタイトルも編集フォームに<input type="hidden">として埋め込んでおいて、サーバーで照合して不一致なら再編集、でほとんど問題なさそう。それとも本文も含めて MD5を算出するか。それなら同一セクションへの変更が競合したときに、先の変更が失われるのを防げるし。
⁂ Tab、スペース→プレビューという流れができあがっているので、これのチェックボックスの位置は少し邪魔。Alt+Shift+P(Fx on Windowsの場合)は面倒すぎるしぃ。
eval(<<__END_OF_TOPLEVEL__,TOPLEVEL_BINDING) module TDiary end __END_OF_TOPLEVEL__
に類する evalは
module ::TDiary end
でいいじゃない。
(行間に、構造に関係しないコードが省略されています)
module TDiary class DefaultIO < IOBase private def restore( fh, diaries ) diary = eval( "#{style( style_name )}::new( headers['Date'], headers['Title'], body, Time::at( headers['Last-Modified'].to_i ) )" )
最後の行はこれ↓で。
diary = style( style_name )::new( headers['Date'], headers['Title'], body, Time::at( headers['Last-Modified'].to_i ) )
anchor_str = @plugin.instance_eval( %Q[anchor "#{@diary.date.strftime('%Y%m%d')}"].untaint )
は
anchor_str = @plugin.anchor( @diary.date.strftime('%Y%m%d' ) ).untaint
で OK。
あえて寝た子を起こすまねをして新たなエラーを引き起こすこともないとは思うけど……。とはいえ、
@plugin_files.grep(/\/category.rb$/).empty?
のコピペの連鎖のようなものは断ち切りたい。emptyかどうかを知りたいのならマッチする全ての要素を集めてくる必要はなくて
not @plugin_files.any?{|pi| /\/category.rb\z/ =~ pi } @plugin_files.find{|pi| /\/category.rb\z/ =~ pi }.nil?
のどちらかで十分です。速度的なもの(なんという婉曲さw)は計ってないけど、category.rbは cで始まるから (有効になっているのなら) @plugin_filesの最初の方にあって、すぐに結果がでるもののはず。
irb(main):044:0> plugin_files = Dir.glob('./*.rb') => ["./a.rb", "./akismet.rb", "./amazon.rb", "./append-css.rb", "./bq.rb", "./calendar2.rb", "./calendar3.rb", "./category.rb", "./comment_mail-qmail.rb", "./comment_mail-sendmail.rb", "./comment_mail-smtp.rb", "./comment_rank.rb", "./counter.rb", "./daily_theme.rb", "./disp_referrer.rb", "./doctype-html401tr.rb", "./dropdown_calendar.rb", "./edit_today.rb", "./footnote.rb", "./gradation.rb", "./gradient.rb", "./hide-mail-field.rb", "./highlight.rb", "./html_anchor.rb", "./image.rb", "./kw.rb", "./list.rb", "./makelirs.rb", "./makerss.rb", "./my-ex.rb", "./my-sequel.rb", "./navi_user.rb", "./number_anchor.rb", "./pb-show.rb", "./ping.rb", "./pingback.rb", "./random_google.rb", "./recent_comment.rb", "./recent_comment3.rb", "./recent_list.rb", "./recent_namazu.rb", "./recent_rss.rb", "./recent_trackback3.rb", "./referer-antibot.rb", "./referer-utf8.rb", "./referer_scheme.rb", "./search_control.rb", "./search_form.rb", "./sn.rb", "./speed_comment.rb", "./squeeze.rb", "./src.rb", "./tb-send.rb", "./tb-show.rb", "./title_list.rb", "./title_tag.rb", "./tlink.rb", "./todo.rb", "./weather.rb", "./whatsnew.rb", "./xmlrpc.rb"] irb(main):045:0> Benchmark.bmbm{|j| irb(main):046:1* j.report('grep'){ 10000.times{ plugin_files.grep(/\/category.rb$/).empty? } } irb(main):047:1> j.report('grep2'){ 10000.times{ plugin_files.grep(/\/category.rb\z/).empty? } } irb(main):048:1> j.report('any?'){ 10000.times{ not plugin_files.any?{|pi| /\/category.rb\z/ =~ pi } } } irb(main):049:1> j.report('find'){ 10000.times{ plugin_files.find{|pi| /\/category.rb\z/ =~ pi }.nil? } } irb(main):050:1> } Rehearsal ----------------------------------------- grep 0.359000 0.000000 0.359000 ( 0.361000) grep2 0.297000 0.000000 0.297000 ( 0.275000) any? 0.109000 0.000000 0.109000 ( 0.104000) find 0.094000 0.000000 0.094000 ( 0.105000) -------------------------------- total: 0.859000sec user system total real grep 0.297000 0.000000 0.297000 ( 0.270000) grep2 0.281000 0.000000 0.281000 ( 0.275000) any? 0.094000 0.000000 0.094000 ( 0.100000) find 0.140000 0.000000 0.140000 ( 0.101000)
最新のプラグイン集のプラグインを全て有効にしたのと同じ状態では any? と findの勝ち。これでは全ての要素を調べる grepがあまりに不利。(ちなみに category.rbが見つからなくて grep同様に配列の最後まで調べた場合、any?と findは grepの二倍強の時間がかかっていた。yieldがあるからなあ)
plugin_filesの要素数を半分の 30にしても any?、findの勝ちだったので、有効なプラグイン数15(category.rbを含む)の状態でもう一度。
irb(main):058:0> plugin_files = plugin_files[0,15] => ["./a.rb", "./akismet.rb", "./amazon.rb", "./append-css.rb", "./bq.rb", "./calendar2.rb", "./calendar3.rb", "./category.rb", "./comment_mail-qmail.rb", "./comment_mail-sendmail.rb", "./comment_mail-smtp.rb", "./comment_rank.rb", "./counter.rb", "./daily_theme.rb", "./disp_referrer.rb"] irb(main):059:0> Benchmark.bmbm{|j| irb(main):060:1* j.report('grep'){ 10000.times{ plugin_files.grep(/\/category.rb$/).empty? } } irb(main):061:1> j.report('grep2'){ 10000.times{ plugin_files.grep(/\/category.rb\z/).empty? } } irb(main):062:1> j.report('any?'){ 10000.times{ not plugin_files.any?{|pi| /\/category.rb\z/ =~ pi } } } irb(main):063:1> j.report('find'){ 10000.times{ plugin_files.find{|pi| /\/category.rb\z/ =~ pi }.nil? } } irb(main):064:1> } Rehearsal ----------------------------------------- grep 0.188000 0.000000 0.188000 ( 0.175000) grep2 0.109000 0.000000 0.109000 ( 0.099000) any? 0.110000 0.000000 0.110000 ( 0.106000) find 0.109000 0.000000 0.109000 ( 0.107000) -------------------------------- total: 0.516000sec user system total real grep 0.125000 0.000000 0.125000 ( 0.096000) grep2 0.094000 0.000000 0.094000 ( 0.094000) any? 0.110000 0.000000 0.110000 ( 0.101000) find 0.125000 0.000000 0.125000 ( 0.105000)
category.rbを含めて 15のプラグインが有効の場合、僅差で any?、findの負け。category.rbを使ってない場合、any?、findはさらに不利になるわけだけど……。(みんな使ってるよね?) この日記では category.rbを含めて 23(+必ず有効な4つ)のプラグインが有効だから any?、findがもう少し有利になって、結論は「どっちでもいい」。けど、それなら findを使う。プラグインは増やすことはできても減らすと過去の日記でエラーが出たりするから。
なじみがあって処理内容が明確だから使いやすいのかも。同じことを find_allでやろうとするよりも(any?や findの結果から考えて)倍近く高速だろうことも想像ができるので、使いどころが正しければ優秀なメソッド。
2.2.0までは Ruby 1.6もサポートしていたので any?の使用は考えられないのだった。でも findは Ruby 1.6からあるようなのでここまで書いたことが全否定されたというわけでもない。よかった。
* ほんとうに?なんで?
最終更新: 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で初期化とかしてるとはまる。
最近ちょこちょこ日記に画像を貼ったりアップロードしたファイルにリンクを張ったりしてたのは image.rbに代えて、image.rbを手直ししたプラグインを試してたから。
プラグインの仕様はこう↓。
image( number, 'altword', thumbnail, size, place )
大小二枚の画像をアップロードし、サムネイルの下の「本文に追加」ボタンを押すとそれぞれ下のようなテキストが日記本文に追加される。('オリジナル'、'サムネイル'の部分は自分で書き換えました)
{{image 0, 'オリジナル', nil, [500,500]}} {{image 1, 'サムネイル', nil, [50,50]}}
画像1(サムネイル)を <img>要素として日記中に埋め込み、それをクリックすると 画像0(オリジナル)が表示されるようにしたいとする。その場合のプラグイン呼び出しの記述は下のようになる。
{{image 0, '画像の説明。(クリックで拡大画像)', 1, [50,50]}}
「本文に追加」ボタンで挿入されるどちらのテキストからも遠い。加えて第一パラメータの意味が第三パラメータの有無によって変わるところがユーザーを大混乱に陥れる。自分なんてたまにしか画像をアップロードしないから毎回だまされる。
SVGファイルをアップロードしようとしても拒否された。埋め込みはできなくてもアップロードとリンクくらいはしたかったし、ルールを追加することで SVGでも FLVでも適切に日記中に埋め込めるようにできるのが理想。
べつにヘンじゃないけど、20071229_0.jpg のような名前というより IDのようなものではなく、ローカルでのファイル名を維持したいな、と思う。
file( 'filename' ) #=> <a href="fileurl">filename</a> file( 'displaytext', :href=>'filename' ) #=> <a href="fileurl">displaytext</a> file( 'imagename', :href=>'filename' ) #=> <a href="fileurl"><img src="imageurl"></a> file( 'imagename', :title=>'altword', :size=>[50,50], :type=>'photo' ) #=> <img src="imageurl" title="altowrd" alt="altword" width="50" height="50" class="photo"> file( 'thumbnail', :title=>'altword', :href=>'number', :size=>size, :type=>'photo' ) #=> image( number, 'altword', thumbnail, size ) と同じ
(シンタックスハイライト機能が欲しい……*。なんて読みにくいんだ)
「本文に追加」ボタンを押すと四番目のものが本文に追加されるのでそこに :href=>'number'を付け加えるだけでリンク付きのサムネイル画像を日記に埋め込める。パラメータを入れ替えたりしなくてもよい。
第一引数はいつだって日記中に埋め込まれるテキストや画像のことだし、その他の引数はすべてオプションで、第一引数の付加情報(リンク先、代替テキスト、大きさなど)になっている。
skel/preview.rhtml に <%%=form_proc( @date )%> を追加してプレビュー後でもファイルをアップロードできるようにし、アップロードするときには書きかけの本文が消えることの確認を表示してキャンセルもできるようにした。(そうしたら本文をクリップボードにコピーしてからアップロードできるし)
また、form_procで表示される縮小画像に自身へのリンクを(target="_blank"で)持たせた。縮小表示されてる画像を別窓で本来のサイズで表示するだけだけど割と便利。
http://www.tdiary.org/archive/devel/threads.html#00718
互換性や image.rbを利用する他のプラグインとのからみで使いにくくなってるのかぁ、と思ったがどうも違う。第三引数(thumbnail)は新しく追加されたもののようだ。サムネイルの議論をしてたからって第三引数の名前を thumbnailにしちゃったのが悲劇の始まり。link_toにしておけば第一引数の意味を変えなくて済んだのに。
* 付けた。http://vvvvvv.sakura.ne.jp/ds14050/diary/20071230.html#p01
日記を更新すると、TDiary::Config#data_path/category/ 以下の、カテゴリごとに作られるキャッシュファイルがずいぶんたくさん更新される。全部ではないが半分近い 21のファイルが更新されていた。日記の内容はというと一つのカテゴリしか使っていない。
どこのコードが無駄にファイルを更新しているのかと絞っていくと、category.rbの中の Category::Cache#replace_sectionsだとわかった。ではこの replace_sectionsが悪いのかというとそうではない。replace_sectionsの中の
PStore.new(cache_file(c)).transaction do |db| end
に囲まれた部分をすべてコメントアウトしても 21のキャッシュファイルが一斉に更新されたのだから。一部の PStoreファイルは開いて閉じるだけで常に更新されるのだとしか考えられない。
PStoreは transactionの前後で Marshal::dump の戻り値のサイズと MD5が変化したかどうかを見て、変更があったかどうかを判断し、ファイルに書き込みをするかしないかを決めている。Marshal::load/dump が対称ではないのだろか。(そもそも Hashを Marshal::dumpした結果が一定だと仮定してよいのだろうか*)
原因が何であるにせよサーバーの pstore.rbを書き換えるわけにもいかないので、tDiaryの category.rbに対策を施した。
読み出し専用であることが予めわかっている PStore#transactionはすべて trueを引数にして(readonly=trueの意)呼び出す。実はそういう transactionはすべて PStore#abortで終わっているのでこの対策は必要ないのだが、ファイルを排他ロックすることと MD5を計算する手間が省けるので一応。
肝心の Category::Cache#replace_sectionsは transactionの開始前に変更があるのかどうかがわからないので PStore#transaction(true)は使えない。日付の削除が空振りに終わったときにだけ PStore#abortを呼ぶことにする。これで必要最低限のキャッシュファイルだけが更新されるようになった。
# # cache each section of diary # used in update_proc # def replace_sections(diary) return if diary.nil? or !diary.categorizable? categorized = categorize_diary(diary) categories = restore_categories deleted = [] ymd = diary.date.strftime('%Y%m%d') categories.each do |c| PStore.new(cache_file(c)).transaction do |db| db['category'] = {} unless db.root?('category') if categorized[c] and diary.visible? db['category'].update(categorized[c]) else # diary is invisible or sections of this category is deleted db.abort unless db['category'].delete(ymd) deleted << c if db['category'].empty? end end end if !deleted.empty? deleted.each do |c| File.unlink(cache_file(c)) end replace_categories(categories - deleted) end end
* 追記:最近の Ruby(1.8.7だか 1.9.0)は Hashの keyの順序を保存しているみたいだけど。
以下のコードと同じ出力をより短いコードで得よ。(パーは 27バイト)
-2000.step(-10000,-10) do |v| puts v end
n=1990;801.times{p -n+=10}
-1 (26バイト)だからバーディーかな。アルバトロス(-3)が上限とは限らないけど。数値リテラルが冗長な気がするけど他の表現が思いつかない。文字コード(?X)を使おうと思ったけどできなかった。
irb(main):115:0> require :benchmark.to_s irb(main):119:0> Benchmark.bmbm{|j| j.report } NameError: uninitialized constant Benchmark::Job::ArgmentError from C:/Program Files (x86)/ruby/lib/ruby/1.8/benchmark.rb:333:in `report' from (irb):119 from C:/Program Files (x86)/ruby/lib/ruby/1.8/benchmark.rb:250:in `bmbm' from (irb):119 from :0 irb(main):120:0>
ArgmentError -> ArgumentError
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/44601
2.2.0に変更すると半分程度の月の日記データ(YYYYMM.td2)が消えた。残ってる月も表示されるのは一、二件程度。原因を調べると、もちろん原因は自分にあったんだけど、2.2.0の更新された tdiary/wiki_style.rbに、DIFF差分をもとに過去に自分が加えた変更をもう一度加えたときに行頭の - を一カ所取り除き忘れたのが原因だった。
if /\A(?:http|https|ftp|mailto)\z/ =~ scheme
を、
if /\A(?:http|https|ftp|mailto|javascript)\z/ =~ scheme
こう(↑)すべきところ、
- if /\A(?:http|https|ftp|mailto|javascript)\z/ =~ scheme
こう(↑)してしまったのが原因。
Rubyでは ifといえども値を返すので、Stringに単項マイナスのメソッドは存在しないというエラーになっていたんだけど、そのエラーは tdiary/defaultio.rbの restore()で rescue NameErrorされて捨てられていました。
スクリプトのエラー(原因は自分だけど)で日記データが消えるのはウレシクナイ。スクリプトの間違いは起こりやすく修正も容易だけど、消えた日記データは復活しないのだから。バックアップをとっていてさえ直前のバックアップより新しいものは元に戻しようがない。
エラーで表示できない日記データでも TDiary::WikiDiaryなどのかわりに TDiary::BadDiary などとして保持しておいてほしいな(そしてデータファイルを更新するときには正常なデータと一緒に書き込む)、と思いつきだけで書く。
(怖くなってサーバーの tDiaryがまだアップデートできてません)
セクションごとに表示されるようになったのはもちろんのこと、飛び先が同一ページ内になったことが嬉しい。プライベートな変更(→20040228p02, →20050401p01)は維持するのが面倒くさいし、アップデートの失敗例が直上にあるので。
Productize and officially support the group_concat() SQL function.
試してみる。
>sqlite3 SQLite version 3.5.4 Enter ".help" for instructions sqlite> create table t1(c1); sqlite> insert into t1 values(1); sqlite> insert into t1 values(2); sqlite> insert into t1 values(3); sqlite> select * from t1; 1 2 3 sqlite> select group_concat(c1) from t1; 1,2,3 sqlite> select group_concat(c1+1) from t1; 2,3,4 sqlite> select group_concat(c1||0) from t1; 10,20,30 sqlite> select group_concat(c1, "-") from t1; 1-2-3 sqlite> select group_concat(c1, c1) from t1; 12233
期待通り。
もう create_function()で自作して Segmentation faultに困らされたりすることもなくなるね。(sqlite3-rubyの話)
もっとも sqlite3-rubyは一年以上のブランクを経て今年の二月に新バージョンが出てるので Segmentation faultは出なくなってると思う。下の変更点が多分そう。
2007-01-13 11:42 jamis
* Fix for use of callbacks (busy_handler, set_authorize and trace) (thanks Sylvain Joyeux, closes #2955)http://rubyforge.org/frs/shownotes.php?group_id=254&release_id=9438
数日前からこちら(SmallStyle - category プラグインを利用した タグクラウド 表示プラグイン)から入手したタグクラウド表示プラグインを手直ししたものを使っている。
直した理由は、日記を書かなくても毎日キャッシュが更新されるとか(<それ日記じゃない)、すぐに組み立てられる URLをキャッシュに含めなくてもいいじゃないかとか、addとか print_htmlというメソッドにプラグインの名前空間を汚されたくないとか、プラグインファイルの地の部分で returnするとどこへ戻るんだろう無事に戻れるんだろうかとか、そういうこと。いじった結果、ファイルサイズが増加し、キャッシュサイズもたぶん増加し、計算量が増加して、機能はほとんど変わらず、なので変更点は(この人目につかない日記にも)書けない。
本題。変更の過程でフォントサイズの指定をクラスからインラインスタイルに変更し、style="font-size: xxx%" というのを HTMLタグに埋め込むことにした。そして、フォントサイズのパーセンテージの決定方法を以前自分が書いたものから流用した。(ここが問題)。
以前書いたものはタグの出現数の分布を、最小フォントサイズ(100%)から最大フォントサイズ(200%)の間にそのままマップするものだった。これだと一つだけ突出したタグがあると他がみんなどんぐりの背比べになってしまう。では出現数の二乗根を使うとどうなるか、実験してみた。
[1, 2, 3, 10, 20, 50] # タグの出現数のリスト => [100, 102, 104, 118, 138, 200] # 出現数をそのまま使って font-size(100%-200%)を求めた => [100, 106, 112, 135, 157, 200] # √出現数 => [100, 117, 128, 158, 176, 200] # log(e)(出現数)
ここの日記のように一部のタグだけが突出していて、その他のほとんどのタグの出現数が 10以下のような場合は、出現数の自然対数を使うと出現数のわずかな増加にフォントサイズが敏感に反応する。というわけで、Rubyでは sqrtと logの時間コストがほとんど同じであることだし、タイトルを無視して logを使ってフォントサイズを決めることにした。
単純に出現数を使った場合、上で書いたように一つを除いて豆粒サイズになる問題があったが、logを使った場合は一部を除いほとんどが最大に近いサイズになってしまう問題が発覚した。あいだをとってルート。
http://www.machu.jp/diary/20070719.html#p02
はてブ指数というものに使われているという。定義は「N以上の数がN個以上含まれる場合の最大のN」
これなら一部のカテゴリがどれだけ突出していていようとも、「N以上である」としか評価されないわけだ。たとえば求めた H指数が 100だったとき、値が100の要素も10000の要素も「100以上である」とだけ評価されているわけで、重みは等しい。なるほど。でも H指数をフォントサイズの決定にどのように使おう。