/ 最近 .rdf 追記 設定 本棚

脳log[tDiary: 2008-03-06~]



2008年03月06日 (木) 一日遅れで HUNTER×HUNTERの発売に気付く。考えられないミス。

[tDiary]バッドノウハウ: ヘッダ/フッタに #{} を含めたいときは

例えばこんな、<h1>の中身を<title>の中身と同じにし、かつ日記名部分をトップページへのリンクにする、ヘッダはエラーになる。

<h1>
  <a href="<%=h @conf.index%>"><%=@conf.html_title%></a>
  <%=title_tag[/\A<title>#{Regexp.quote h @conf.html_title}(.*?)<\/title>\z/oi, 1]%>
</h1>

原因は

 #{Regexp.quote h @conf.html_title}

この部分が tdiary.conf読み込み時に展開されてしまうから。

  1. \を使ってエスケープしても \自体が tdiary.conf保存時にエスケープされてしまっているので、tdiary.conf読み込み時にはエスケープの意味が残っていない。
  2. 二度 eRubyスクリプトとして評価されるわけではないので <%%= %> も使えない。

結局

 #{'#'}{Regexp.quote h @conf.html_title}

こうなった。

  1. 設定画面に表示されるヘッダの内容は、#{'#'} が # に展開された後のもの(つまり最初の例と全く同じもの)なので、ヘッダを修正するときは毎回 #{ を #{'#'}{ に書き換えた上で保存する必要がある。(面倒。絶対忘れる)
  2. ヘッダの内容に問題があるときは、正常に送信できたように見えて tdiary.confには保存されていないから、(忘れた頃に)絶対ハマる。
  3.  追記

    1と 2のコンボで、ヘッダ/フッダに限らず全ての設定が変更できなくなる。他の設定を保存するときでも、ヘッダの保存部分で失敗するから。(結局 tdiary.confの可読性を犠牲にして String#dumpを使用するしかないようだ)

 追記の追記: #{} の使用は避けるに限る

嫌い*だが tDiary本体をいじくるぐらいならしかたがない。今回のケースは結局 #{} を使わない形に書き換えた。無理矢理使っても面倒なだけだったので……。敗北。

<h1>
  <a href="<%=h @conf.index%>"><%=@conf.html_title%></a>
  <%=title_tag[Regexp.new('\\A<title>'+Regexp.quote(h( @conf.html_title))+'(.*?)</title>\\z', Regexp::IGNORECASE), 1]%>
</h1>

* Regexp.new()のこと。eval()や new Function()と同じくらい嫌い。※new演算子があるのは Rubyではない。


2008年03月04日 (火) 現在 9F。火トカゲを倒す必要がないことにしばらく気付かなかったよ。

[tDiary] fix_url() @ hikidoc.rb

ささいなことなのでここに書くわけだけど、HikiDocと tDiaryの許容する urlの違いから、tDiary同梱の hikidoc.rbの fix_url()が継ぎを当てられた結果、難解なことになっている。

    if /:/ =~ uri and %r!\A(https?|ftp|file|mailto):! !~ uri
      uri
    elsif %r|://| !~ uri and /\Amailto:/ !~ uri
      uri.sub(/\A\w+:/, "")
    else
      uri
    end
    uri.sub(/\A(?:https?|ftp|file):(?!\/\/)/, "")

上が 2008-03-04現在のコード。下は上と同じ意味になっていると思うが自信が持てない。それもひとえに前者が難解だから……。

 追記@2008-06-06: 何か勘違いしてるな

下の方のコードはほとんど hikidoc.rb(revision 93, 2008-02-16)のものだ。これを目にしていなかったはずがない。

[tDiary] tDiary UTF-8化

昨日(3日)の朝に、tDiary本体に migrate機能を付けたと読んだので、一日待って、今日アップデートしたのだけど、tarballは 2日のものだったみたい。(待った意味がない)

  1. いつも通りローカルの変更をマージ。
  2. 標準添付でない jdate.rbを UTF-8で保存し直し。
  3. tdiary.confを UTF-8で保存し直し。(しなくても動いているがコメントが悪さをしないとも限らない?)
  4. キャッシュ(yyyymm.rb, yyyymm.parser)を削除。(したけど migrate.rbが消してくれるみたい)
  5. misc/migrate.rbを index.rbフォルダに移動して実行。

どこかから拾ってきたプラグイン以外は、tDiaryを最新のものにして migrate.rbを実行するだけでいいみたい。

カテゴリキャッシュも、プラグインが(confに)保存したデータも、古いキャッシュも migrate.rbが面倒を見てくれる。


2008年01月12日 (土)

[tDiary] tDiaryの hikidoc.rbを VERSION 0.0.2(r87)に

中身ががらっと変わっていてびっくり。浦島太郎になっていた。Rubyist Magazine 出張版で hikidoc.rbが添削されていて、そのコードをベースに書き直したらしい。

tDiary-2.2.0同梱の hikidoc.rbにあった、複数行PREや複数行プラグイン記法が干渉する問題がなくなっている、というのがアップデートの目的。

Wikiスタイルでこういう本文を書くと

!_
<<<
{{{}}}
>>>
{{'test'}}

このような HTMLになっていた ( {{'test'}}は testになっているべき )が、

<h3><a name="p01" href="./20080111.html#p01"><span
class="sanchor">_</span></a> _</h3>
<pre>
{{{}}}
</pre>
<p>{{'test'}}</p>

VERSION 0.0.2では直っている。

 tdiary/wiki_style.rb の変更

HikiDocのアップデートに伴って、HikiDocを呼び出す部分を下のように変更する必要がある。

		def to_html( string )
			html = HikiDoc::to_html( string,
				:level => 3,
				:use_wiki_name => false,
				:allow_bracket_inline_image => false,
				:plugin_syntax => method(:valid_plugin_syntax?) ).strip
 use_wiki_nameオプション(false)を追加。

本文中にべた書きしたWikiNameをリンクにしてほしくない。

 allow_bracket_inline_imageオプション(false)を追加。

[[beautiful_stuff.png]]など角かっこに入れた画像っぽい URLを <img>に置換してほしくない。

 empty_element_suffixオプションを削除。

空タグの終了の仕方は HikiDoc.to_htmlと HikiDoc.to_xhtmlを呼び分けることで変更する。

 呼び出し方法を HikiDoc.new.to_htmlから HikiDoc.to_htmlに変更。

以前の形式のままでも大丈夫なように互換性が保たれているが、警告が出るので。

 エスケープ方法が変わった(というかなくなった?)

以前は二重のbrace(=プラグイン記法)など HikiDocにとって意味のある記号をそのまま表示したいときに

\{{plain text}}

とエスケープすることができたがそれが不可能になっている。今は代わりに

{{'{{'}}plain text}}

と書いている。他の書き方もあるかもしれないがプラグイン記法は将来も変更されないだろうから冗長でもこれが確実*。さもないとエスケープ方法が変わったときに過去の日記を書き換えてまわる羽目になる。(今の自分のように)

 行単位のPRE記法(空白インデントする方のPRE)で、強調、もっと強調、打ち消しするための変更。ついでにプラグイン記法も有効になる。

以前の hikidoc.rbに加えていた変更を新しい方にも。

「記法が有効になる ⇒ これらの文字をそのまま表示するためには一手間必要」

なんだけど、複数行PRE記法(<<<〜>>>)をそのままの状態(一切の修飾が無効)で残しているので必要なら(というか強調や打ち消しが不要なら)そちらを使える。

--- hikidoc.rb.r87	Mon Jan 14 09:43:25 2008
+++ hikidoc.rb	Mon Jan 14 09:57:35 2008
@@ -328,11 +328,11 @@
 
   INDENTED_PRE_RE = /\A[ \t]/
 
+  # ''em'', '''strong''', ==strike== and {{plugin}} are available.
   def compile_indented_pre(f)
     lines = f.span(INDENTED_PRE_RE)\
-        .map {|line| rstrip(line.sub(INDENTED_PRE_RE, "")) }\
-        .map {|line| @output.text(line) }
-    @output.preformatted restore_plugin_block(lines.join("\n"))
+        .map {|line| rstrip(line.sub(INDENTED_PRE_RE, "")) }
+    @output.preformatted compile_modifier(lines.join("\n"))
   end
 
   BLOCK_PRE_OPEN_RE = /\A<<<\s*(\w+)?/

強調は有効にするけどプラグインは無効がいいなら、追加分の最終行を

+    @output.preformatted compile_modifier(restore_plugin_block(lines.join("\n")))

こうすればよいが、そうするとモディファイア(''em'', '''strong''', ==strike==)をモディファイアでない、そのままのテキストとして書く方法がなくなる気がする。

 追記@2008-02-10: tDiary-2.2.0.20080119で hikidoc.rbが r87になった

 追記@2008-02-10: tDiary-2.2.0.20080210で 警告がでない呼び出し方法になった

* プラグイン記法の中身は HikiDocを利用するアプリケーション(Hikiや tDiaryなど)によって許可される内容が変わってくる。プラグインを利用して書かれた HikiDoc形式の文章は HikiDocを利用するアプリ間でポータブルでないかもしれない。全然確実ではなかった。実際この書き方は Hikiに、有効なプラグインの呼び出しではないとエラーにされてしまう。だってただの文字列だから……。


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月31日 (月)

[tDiary] RSSを出力するように。

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の場合)は面倒すぎるしぃ。

[tDiary][Ruby] evalは最終手段*。module ::TDiaryを使うんだ。

 plugin/counter.rb

 plugin/disp_referer.rb

 plugin/makelirs.rb

 plugin/makerss.rb

 plugin/navi_user.rb

 plugin/pb-show.rb

 plugin/recent_list.rb

 plugin/tb-show.rb

 plugin/title_tag.rb

eval(<<__END_OF_TOPLEVEL__,TOPLEVEL_BINDING)
module TDiary
end
__END_OF_TOPLEVEL__

に類する evalは

module ::TDiary
end

でいいじゃない。

 tdiary/defaultio.rb

(行間に、構造に関係しないコードが省略されています)

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 ) )

 tdiary.rb

 plugin\pingback\pb.rb

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を使う。プラグインは増やすことはできても減らすと過去の日記でエラーが出たりするから。

 追記@2008-01-06:grepは名前勝ち?

なじみがあって処理内容が明確だから使いやすいのかも。同じことを find_allでやろうとするよりも(any?や findの結果から考えて)倍近く高速だろうことも想像ができるので、使いどころが正しければ優秀なメソッド。

 追記@2008-01-06:ブロック付き Enumerable#any?(all?)は Ruby 1.8 feature

2.2.0までは Ruby 1.6もサポートしていたので any?の使用は考えられないのだった。でも findは Ruby 1.6からあるようなのでここまで書いたことが全否定されたというわけでもない。よかった。

* ほんとうに?なんで?


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で初期化とかしてるとはまる。


2007年12月29日 (土)

[tDiary] image.rbの引数っておかしいよね

最近ちょこちょこ日記に画像を貼ったりアップロードしたファイルにリンクを張ったりしてたのは image.rbに代えて、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ファイルをアップロードしようとしても拒否された。埋め込みはできなくてもアップロードとリンクくらいはしたかったし、ルールを追加することで SVGでも FLVでも適切に日記中に埋め込めるようにできるのが理想。

 ファイル名

べつにヘンじゃないけど、20071229_0.jpg のような名前というより IDのようなものではなく、ローカルでのファイル名を維持したいな、と思う。

 オレ仕様の image.rb (メソッド名は imageから fileになっている)

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"で)持たせた。縮小表示されてる画像を別窓で本来のサイズで表示するだけだけど割と便利。

 image.rbのしがらみを発見

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


2007年12月25日 (火) なんでもかんでも「相性」で片付けて原因究明を怠る(一部の)風潮が嫌い

[Ruby][tDiary] PStore, category.rb: 日記に変更があるたびにカテゴリキャッシュファイルが一斉に更新されるのをなんとかする。

日記を更新すると、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)を使う

読み出し専用であることが予めわかっている PStore#transactionはすべて trueを引数にして(readonly=trueの意)呼び出す。実はそういう transactionはすべて PStore#abortで終わっているのでこの対策は必要ないのだが、ファイルを排他ロックすることと MD5を計算する手間が省けるので一応。

 PStore#abortを使う

肝心の 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の順序を保存しているみたいだけど。


2007年12月16日 (日)

[tDiary] (特定の)日記が消える?

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がまだアップデートできてません)

 footnote.rb

セクションごとに表示されるようになったのはもちろんのこと、飛び先が同一ページ内になったことが嬉しい。プライベートな変更(→20040228p02, →20050401p01)は維持するのが面倒くさいし、アップデートの失敗例が直上にあるので。

[tDiary] タグクラウドでフォントの大きさを決めるときに出現数の二乗根を使うにはわけがある。

数日前からこちら(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を使ってフォントサイズを決めることにした。

 追記@2008:やっぱり root。

単純に出現数を使った場合、上で書いたように一つを除いて豆粒サイズになる問題があったが、logを使った場合は一部を除いほとんどが最大に近いサイズになってしまう問題が発覚した。あいだをとってルート。

 追記@2007-12-31:H指数なんてものも

http://www.machu.jp/diary/20070719.html#p02

はてブ指数というものに使われているという。定義は「N以上の数がN個以上含まれる場合の最大のN」

これなら一部のカテゴリがどれだけ突出していていようとも、「N以上である」としか評価されないわけだ。たとえば求めた H指数が 100だったとき、値が100の要素も10000の要素も「100以上である」とだけ評価されているわけで、重みは等しい。なるほど。でも H指数をフォントサイズの決定にどのように使おう。


2007年12月08日 (土) 知らなかった: ヘボン = Hepburn = ヘップバーン

[tDiary]カテゴリ表示を最新表示、月表示モード互換に変更

10月22日に書いたように「カテゴリ」表示モードは「最新」「月」表示と見た目が違って違和感があるので、カテゴリ表示を最新表示と月表示に近づけてみた。(この日記で実働中)。

 追記

  • 2007-12-17 不完全なキャッシュが作成される問題があったので、「4 tdiary/categorizedio.rb」を修正しました。
  • 2007-12-28 セクションURLが変わってしまう問題があったので、「4 tdiary/categorizedio.rb」を修正しました。
  • 2008-04-17 カテゴリインデックスのパスの求め方を最新の tDiaryと同じものにしました。(「4 tdiary/categorizedio.rb」。結果は以前の CGI.escapeを使ったものと同じ。20080417p01)
  • 2008-04-18 共通のコードを TDiaryView#load_categorizedioにまとめました。(「3 tdiary.rb」)

 方針

新しく表示モード(TDiaryViewを継承したクラス)を作るのは手間だし、TDiaryLatestや TDiaryMonthからのコピペばっかりになることが想像できるので IOのラッパを作って TDiaryLatestや TDiaryMonthを騙すことに。

 CategorizedIO

transactionメソッドを引っかけて元々の IO(DefaultIOか PStoreIO)が渡してきた diariesから特定のカテゴリを持たないセクションを取り除いて呼び出し元に渡す。

transactionの呼び出し元に不完全な日記データが渡る関係上、日記の変更は捨てる。(読み出し専用)

 変更・追加したファイル

  1. .htaccess
  2. index.rb
  3. tdiary.rb
  4. tdiary/categorizedio.rb
  5. plugin/00default.rb
  6. misc/plugin/category.rb

 1 .htaccess

個々の日記のURLを ?date=yyyymmdd から yyyymmdd.html に、mod_rewriteを使って書き換えてる場合、RewriteRuleの最後に [QSA] (query string append)フラグを付けて書き換えた URLにクエリストリングをくっつけてもらう必要がある。クエリストリングはカテゴリを指定するのに使う。mod_rewriteを使っていない場合は .htaccessの変更は必要ない。

RewriteEngine on
RewriteBase /ds14050/diary
RewriteRule ^([0-9]+)(-[0-9]+)?\.html$ index.rb?date=$1$2 [QSA]

 2 index.rb

カテゴリの指定に categoryという CGIパラメータを使うので、最新表示にカテゴリフィルタをかけたときの URLが index.rb?category=カテゴリ になり、最後の else まで落ちて最新表示が選択される前に、従来のカテゴリ表示が選択されてしまう。これまでのカテゴリ表示は使わないので該当する二行をコメントアウトする。

		if @cgi.valid?( 'comment' ) then
			……
		elsif @cgi.valid?( 'date' )
			……
#		elsif @cgi.valid?( 'category' )
#			tdiary = TDiary::TDiaryCategoryView::new( @cgi, "category.rhtml", conf )
		elsif @cgi.valid?( 'search' )
			……
		else
			……
		end

 3 tdiary.rb

騙すのは TDiaryLatestと TDiaryMonth。それぞれの initialize()で日記データを読み出す前に、@ioを置き換えるコードを追加する。CategorizedIOは読み出し専用なのでこれ以後 日記の変更はできなくなるが、どちらも日記の変更はしていない(と思う)ので影響はない。

また categoryパラメータが与えられたときにキャッシュを無効にするために cache_file() も変更している。

	class TDiaryView < TDiaryBase

(skip)

		def load_categorizedio( baseio ) # CategorizedIO is an IO wrapper and is readonly.
			return baseio if not @conf or not @cgi
			require 'tdiary/categorizedio'
			category_dir = File.join(@conf.data_path, 'category')
			categories = ( @cgi.params['category'] || [] )
			return CategorizedIO.new( baseio, category_dir, categories )
		end
	end

	#
	# class TDiaryDay
	#  show day mode view
	#
	class TDiaryDay < TDiaryView

(skip)

	class TDiaryMonth < TDiaryView
		def initialize( cgi, rhtml, conf )
			super

			@io = load_categorizedio( @io )

			begin

(skip)

	protected
		def cache_file( prefix )
			@cgi.valid?( 'category' ) ? nil : "#{prefix}#{@rhtml.sub( /month/, @date.strftime( '%Y%m' ) ).sub( /\.rhtml$/, '.rb' )}"
		end
	end # class TDiaryMonth

(skip)

	class TDiaryLatest < TDiaryView
		def initialize( cgi, rhtml, conf )
			super

			@io = load_categorizedio( @io )

(snip)

		def cache_file( prefix )
			if @cgi.params['date'][0] or @cgi.valid?( 'category' ) then
				nil
			else
				"#{prefix}#{@rhtml.sub( /\.rhtml$/, '.rb' )}"
			end
		end

(skip)

	end # class TDiaryLatest

 4 tdiary/categorizedio.rb

新規追加ファイル。貼り付けるには厳しい量だな。貼るけど。

既存の IOのメソッドを置き換えるのは transaction() と calendar() の二つ。

transaction() は該当するカテゴリを持たないセクションをはじく機能を持つ。

calendar() は該当するカテゴリを持つセクション の存在する年月のみを返す。calendarを作成するために category.rbプラグインが作成するキャッシュを利用するのでキャッシュが壊れていたり不完全だったりすると影響を受ける。

 予想される問題点

HOWTO-make-plugin.html には TDiary::Plugin@yearsには全日記の年月データが含まれていると明記されているので、@yearsの元になる calendarを上書きすると category.rbや squeeze.rbなど独自に日記を読み込むプラグインが期待通り動かない可能性がある。

@io.transactionで TDiary::TDiaryBase::DIRTY_NONEを返しても DefaultIOがデータを更新することがある。キャッシュが存在しない場合がそう。yieldを呼び出した後で、CategorizedIOがセクションを削除したりした diariesを元にキャッシュを作成してしまう。今は tdiary/defaultio.rbを書き換えて yieldを呼び出す前に必要ならキャッシュを作成してしまって、yieldの戻り値が DIRTY_NONE以外だった場合は、もう一度キャッシュを作成しなおすようにしているが、単純に tdiary/caretorizedio.rbで DefaultIO#transactionを breakで抜けてもいい。 DefaultIO#transactionを抜けてから diariesをいじるようにして解決済み。

表示しないセクションを削除してしまうとセクションナンバーが変わってしまい、それに伴って URLも変わってしまうので混乱を招くもとになっていた。 12月28日に tdiary/categorizedio.rbを修正して解決済み。

require 'delegate'
require 'pstore'

module TDiary

	class CategorizedIO < SimpleDelegator

		def initialize(baseio, category_dir, categories=nil)
			super(baseio);
			@dir = category_dir;
			set_categories(categories);
		end

		def set_categories(categories)
			@categories = (categories || []);
			@calendar = nil; # update calendar
		end

		def categorized?
			return(@categories and !@categories.empty?);
		end

		def calendar
			return __getobj__.calendar unless categorized?;
			@calendar = category_calendar unless @calendar;
			return @calendar;
		end

		# 読み出し専用。ブロック引数を変更しても元々の IO(DefaultIO or PStoreIO)には届きません。
		def transaction(date, &block)
			if(not categorized?)
				return __getobj__.transaction(date, &block);
			elsif(! block)
				# nothing to do.
			elsif(in_calendar?(date))
				diaries = nil;
				__getobj__.transaction(date){|diar_es|
					diaries = diar_es;
					TDiaryBase::DIRTY_NONE;
				}
				if(diaries)
					filtered_diaries = category_filter(diaries);
					dirty = yield(filtered_diaries);
				end
			else
				yield({});
			end
		end

	private

		def category_filter(diaries)
			filtered = {};
			diaries.each{|ymd, diary|
				next unless diary.categorizable?;
				diary_is_empty = true;
				diary.each_section{|section|
					if((@categories - section.categories).empty?)
						diary_is_empty = false;
					else
						hide_section(section);
					end
				}
				filtered[ymd] = diary unless diary_is_empty;
			}
			return filtered;
		end

		def hide_section(section)
			section.extend(HiddenSection);
		end

		def in_calendar?(date)
			y, m = "000#{date.year}"[-4,4], "0#{date.month}"[-2,2];
			cal = self.calendar;
			return(cal.has_key?(y) && cal[y].include?(m));
		end

		# returns intersection of all @categories' calendar.
		def category_calendar
			years = __getobj__.calendar;
			categories = (@categories || []);
			ymdp_array = nil;
			categorize(categories, years).each{|ctgr, ctgr_cache|
				a = [];
				ctgr_cache.each{|ymd, day_cache|
					day_cache.each{|cache| section_index, = *cache;
						a.push(ymd+'p'+section_index.to_s);
					}
				};
				ymdp_array = ymdp_array.nil? ? a : (ymdp_array & a);
				break if ymdp_array.empty?;
			};
			category_calendar = {};
			ymdp_array.each{|ymdp| y, m = ymdp[0,4], ymdp[4,2];
				months = (category_calendar[y] ||= []);
				months.push(m) unless months.member?(m);
			}
			return category_calendar;
		end

	# following code is from plugin/category.rb (--;) !DRY

		#
		# categorize sections of category of years
		#
		# {"category" => {"yyyymmdd" => [[idx, title, excerpt], ...], ...}, ...}
		#
		def categorize(category, years)
			categories = category - ['ALL']
			if categories.empty?
				categories = restore_categories
			else
				categories &= restore_categories
			end

			categorized = {}
			categories.each do |c|
				PStore.new(cache_file(c)).transaction do |db|
					categorized[c] = db['category']
					db.abort
				end
				categorized[c].keys.each do |ymd|
					y, m = ymd[0,4], ymd[4,2]
					if years[y].nil? or !years[y].include?(m)
						categorized[c].delete(ymd)
					end
				end
				categorized.delete(c) if categorized[c].empty?
			end

			categorized
		end

		#
		# restore category names
		# ["category1", "category2", ...]
		#
		def restore_categories
			list = nil
			PStore.new(cache_file).transaction do |db|
				list = db['category'] if db.root?('category')
				db.abort
			end
			list || []
		end

		def cache_file(category = nil)
			if category
				"#{@dir}/#{ERB::Util.u( category ).gsub(/%20/,'+')}".untaint
			else
				"#{@dir}/category_list"
			end
		end
	end

	module HiddenSection
		def html4(date, idx, opt)
			return <<-"HTML";
				<% section_enter_proc( Time::at( #{date.to_i} ) )%>
				<% section_leave_proc( Time::at( #{date.to_i} ) )%>
			HTML
		end
		def chtml(date, idx, opt)
			return html4(date, idx, opt);
		end
		def subtitle_to_html
			return '';
		end
		def body_to_html
			return '';
		end
		def stripped_subtitle_to_html
			return '';
		end
	end
end

 5 plugin/00default.rb

ナビリンクとカレンダーを categoryパラメータ対応に。c_anchor() がカテゴリに対応した anchor()で、この後で category.rbに追加する。

def navi_user_latest
	anchor = method(respond_to?(:c_anchor) ? :c_anchor : :anchor)
	result = ''
	result << navi_item( "#{@index}#{anchor.call( @conf['ndays.next'] + '-' + @conf.latest_limit.to_s )}", navi_next_ndays ) if @conf['ndays.next'] and not bot?
	result << navi_item( @index, navi_latest ) if @cgi.params['date'][0]
	result << navi_item( "#{@index}#{anchor.call( @conf['ndays.prev'] + '-' + @conf.latest_limit.to_s )}", navi_prev_ndays ) if @conf['ndays.prev'] and not bot?
	result
end
def navi_user_month

	……

	anchor = method(respond_to?(:c_anchor) ? :c_anchor : :anchor)

	result = ''
	result << navi_item( "#{@index}#{anchor.call( prev_month )}", navi_prev_month ) if prev_month and not bot?
	result << navi_item( @index, navi_latest )
	result << navi_item( "#{@index}#{anchor.call( next_month )}", navi_next_month ) if next_month and not bot?
	result
end
def calendar
	result = %Q[<div class="calendar">\n]
	@years.keys.sort.each do |year|
		result << %Q[<div class="year">#{year}|]
		@years[year.to_s].sort.each do |month|
			m = "#{year}#{month}"
			if(respond_to? :c_anchor)
				result << %Q[<a href="#{@index}#{c_anchor m}">#{month}</a>|]
			else
				result << %Q[<a href="#{@index}#{anchor m}">#{month}</a>|]
			end
		end
		result << "</div>\n"
	end
	result << "</div>"
end

 6 misc/plugin/category.rb

c_anchor プラグインメソッドを追加。カテゴリパラメータに対応した anchorメソッド。

def c_anchor(s)
	a = anchor(s)
	return a unless @category_info
	a << (a.index('?') ? ';' : '?')
	a << @category_info.query_string
	a.chomp!(';'); a.chomp!('?')
	return a
end

#
# misc
#

既存の make_anchor()メソッドの前半からクエリストリング作成部分を query_string()メソッドとして切り出し。上の c_anchor()で利用するために。

	def make_anchor(label = nil)
		if label
			case mode
			when :year
				label = label.gsub(/\$1/, @year)
			when :month, :quarter, :half
				label = label.gsub(/\$2/, @month)
				label = label.gsub(/\$1/, @year || '*')
			end
		else
			label = @category.map {|c| CGI.escapeHTML(c)}.join(':')
		end
		%Q|<a href="#{@conf.index}?#{query_string}">#{label}</a>|
	end

	def query_string
		qs = @category.map {|c| "category=#{CGI.escape(c)}"}.join(';')
		qs << ";year=#{@year}" if @year
		qs << ";month=#{@month}" if @month
		qs
	end

Category::Infoを毎回作るのが面倒なのでインスタンス変数として保持。

@categories = @category_cache.restore_categories
@category_info = Category::Info.new(@cgi, @years, @conf)
if @mode == 'categoryview'
	@categorized = @category_cache.categorize(@category_info.category, @category_info.years)
end

 まだやってないこと

  • カテゴリフィルタを外すリンク。やりました@2007-12-11。
  • フィルタがかかっていることの明示。やりました@2007-12-11。
  • カテゴリリストのソート (文字コード順でなく、最終更新日とエントリ数によるもの < タグクラウドじゃん。文字の大きさと濃さを変えてさ) やりました@2007-12-14 ページ上部に表示中

 思いついたこと

検索結果の表示も同じ方法でできる。スコア順に表示したりはできないけど、特定の語を含む日記を時系列順に閲覧することはできる。ワードフィルタ? IOの三段重ねでワードフィルタとカテゴリフィルタを両方適用することもできるね。

やってみた@2007-12-18。→http://vvvvvv.sakura.ne.jp/ds14050/diary/?word=windows

単語のハイライトとかナビゲーションリンクの対応とかカレンダーをどうするかとかは一切放置。本当にやってみただけ。word=WORD1というパラメータを付ければ「月別」「最新」「カテゴリ」表示のどれでもフィルタリングできるはず。

2008年の 1月頃から約 1年間、上記のリンク先が Internal Server Errorになっていたもよう。なんたる無様。それに気付くきっかけが連日のコメントスパムというのがなんともはや。


2007年11月29日 (木)

[tDiary] 検索フォームを付けてみた

  1. Rast < コンパイルで二度挫折。
  2. Namazu < インデックスの更新方法とか更新のコストとかよくわからず。
  3. msearch < Perlだし。スタンドアロンでなくライブラリのかたちの方が使いやすい。
  4. tdiarysearch < 共用サーバだから傍迷惑になりそう。
  5. tdiarygrep < ad hocだけど一番使い勝手がよさそう。

squeeze.rbによる HTMLファイルが既にあるし、ってことでただの grepの出力を HTMLに整形するだけのもの。タイトルとかサブタイトルを参照してもうちょっと見やすく整えたり、AND検索のときの出力に最後の検索語しか現れないのをなんとかしたりとか、そんなことの前に検索対象を間違えてることにいま気付いた。

td2html
2006127KB@12files981KB@130files
2007164KB@11files1.03MB@97files

td2を読まないと……(__;) Namazuのインデックスのサイズだって元のデータが小さいからたかがしれてるぞ。

 追記: 検索部分を grep呼び出しから tdiarygrepのものに変更した。

etDiary用のスプリッタ( 'etdiary' => /^<<(?!<)/ )を加えたのと、Wiki用のスプリッタを /^!/ から /^!(?!!)/ に変更したのと、本文のない日があるとその次の日を加えた二日分をまとめて読んで一日としてしまうことがあったので read_diaries(path) をごにょごにょと泥臭く修正した。


2007年10月22日 (月)

[tDiary] 最新≡月<<<>>>カテゴリ表示 | 最近のエントリは前後のエントリになればいいのに

tDiaryにはいくつかの表示モードがあって、最新と月の表示は一貫してるもののカテゴリ表示はなんか違う。検索結果については検索に何を使うかによって全く変わってくる。

表示形式表示制限フィルタソート
最新全文数件ずつ(なし)日付降順
全文すべて年月日付昇順
カテゴリサマリ*すべてカテゴリ + 期間(月、四半期、半期、年、全て)日付降順
検索結果検索エンジン依存

なにかというと、カテゴリ表示を

表示形式表示制限フィルタソート
カテゴリ全文数件ずつカテゴリ日付降順
カテゴリ全文すべてカテゴリ + 期間(年、年月)日付昇順

の選択制(選択するのは読者)にしたいな、と。最新表示と月表示にカテゴリフィルタを付けたようなかたち。違和感とかとまどいは減るんじゃないでしょか。

 最近のエントリや新着記事は前後のエントリになればいいのに

最近のエントリ(名前は違ってもそれに類するもの)は前後のエントリに置き換わればいいと思っている。

日記などで一番よく使うナビゲーションは「つぎの○件」。記事の独立性が高いブログなどでは、タイトルを見て中身を推測でき、読む読まないを事前に判断できる「最近のエントリ」も役に立つ。つぎの○件がないところではこれが唯一のナビゲーションだったりもする。アーカイブとかカレンダーとかは使わない。日記であれブログであれニュースサイト以外では記事が書かれた日時を気にすることがないから。

で、最近のエントリを利用して個別記事に飛んでから困るのが、次の移動先を同じ場所(最近のエントリ)から見つけられないこと。「次の記事」「前の記事」のリンクは全ての記事を読みたいわけではないので使いにくい。だから新着記事だけを閲覧したあと、(もうちょっと読みたいけど)サヨウナラということがたびたび起こる。ナビゲーションが途切れちゃってるのが問題。

最近のエントリでなく前後のエントリであれば、トップページでの表示は最近のエントリと同じものになるし、リンク先の個別の記事ではその前後の複数のエントリへのリンクを提供するので、興味のある記事だけを選んでとびとびに過去へ(あるいは未来へ)移動しつづけることができる。

個別記事へ直接とんできて最近のエントリが読みたくなった人はそのサイトのタイトル(大抵はトップページへのリンク)をクリックすればいいと思うよ。

(最初以外は tDiaryの話ではなかった)

* summaryの訳は要約なのでさわりと同じ意味。このサマリはさわりのよくある誤用と同じ誤用です。