/ 最近 .rdf 追記 設定 本棚

脳log[tDiary: 2007-12-29~]



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の訳は要約なのでさわりと同じ意味。このサマリはさわりのよくある誤用と同じ誤用です。


2007年04月13日 (金)

[tDiary] amazon.rb $Revision: 1.62 $ のオプション関連がちょっと変。

 @conf['amazon.nodefault'] がどこからも参照されてないよ

画像が見つからないときにテキストリンクを表示する(true)か noimage画像を表示する(false)かの設定。

 画像の代替テキストが真っ白

class="amazon"の時に画像の横に表示されるテキストと、画像の代替テキストなど(alt, title)は区別して、altや titleは常に設定されるようにして欲しい。

この辺のことです。

	if with_image and @conf['amazon.hidename'] || pos != 'amazon' then
		label = ''
	elsif not label
		label = %Q|#{amazon_title( item )}#{author}|
	end

両方とも自分でなんとかしてみる。

def amazon_image( item )
	image = {}
	begin
		size = case @conf['amazon.imgsize']
		when 0; 'Large'
		when 2; 'Small'
		else;   'Medium'
		end
		image[:src] = item.elements.to_a( "#{size}Image/URL" )[0].text
		image[:height] = item.elements.to_a( "#{size}Image/Height" )[0].text
		image[:width] = item.elements.to_a( "#{size}Image/Width" )[0].text
	rescue
		image[:src] = image[:height] = image[:width] = nil
	end
	image
end

def amazon_default_image
	image = {}
	base = @conf['amazon.default_image_base'] || 'http://www.tdiary.org/images/amazondefaults/'
	case @conf['amazon.imgsize']
	when 0
		image[:src] = "#{base}large.png"
		image[:height] = 500
		image[:width] = 380
	when 2
		image[:src] = "#{base}small.png"
		image[:height] = 75
		image[:width] = 57
	else
		image[:src] = "#{base}medium.png"
		image[:height] = 160
		image[:width] = 122
	end
	image
end

def amazon_to_html( item, with_image = true, label = nil, pos = 'amazon' )
	with_image = false if @mode == 'categoryview'

	author = amazon_author( item )
	author = "(#{author})" unless author.empty?

	label = %Q|#{amazon_title( item )}#{author}| unless label

	if with_image
		image = amazon_image( item )
		image = amazon_default_image if not image[:src] and not @conf['amazon.nodefault']
		if image[:src] then
			img = <<-HTML
			<img class="#{h pos}" src="#{h image[:src]}"
			height="#{h image[:height]}" width="#{h image[:width]}"
			alt="#{h label}" title="#{h label}">
			HTML
			img.gsub!( /\t/, '' )
			label = '' if @conf['amazon.hidename'] || pos != 'amazon'
		else
			img = ''
		end
	end

	url = amazon_url( item )
	%Q|<a href="#{h url}">#{img}#{h label}</a>|
end

動いているようだ。labelという変数に二つの役割が与えられているのが微かに気になるが。

 (おまけ) エラーメッセージの表示部分

def amazon_get( asin, with_image = true, label = nil, pos = 'amazon' )
	……(省略)……
		rescue Timeout::Error
			asin
		rescue NoMethodError
			if item == nil then
				message = doc.elements.to_a( 'Items/Request/Errors/Error/Message' )[0].text
				"#{label ? label : asin}<!--#{h @conf.to_native( message, 'utf-8' )}-->"
			else
				"#{label ? label : asin}<!--#{h $!}\n#{h $@.join( ' / ' )}-->"
			end
		end

labelは他の場所で HTMLエスケープ前の文字列として扱われているのでエスケープするのがよい。asinだって中身が英数字に違いなかろうがなんだろうがエスケープすればいい。

コメント(<!---->)の中身は h(ERB::Util.html_escape) する必要はなく .gsub(/--+/, '-') した方が良い、とまで言うとパラノイア・原理主義者っぽい気がするのは何故? コメントに起因する不具合に遭ったことがないから?


2006年06月10日 (土)

[tDiary] セクション毎にフットノートが付けられる。(やろうと思えば)

tDiary-2.1.4からは section_enter_procと section_leave_procが用意されてるので、現在 body_leave_procを使って一日毎に行ってることを、section_leave_procでセクション毎に行うだけ。

[tDiary] index.rb: UAにキャッシュを許可。If-Modified-Sinceをチェック。

構わないと思うのですよ、リロードの度にプラグインを評価しなくても。

--- index.rb~	2005-06-13 14:05:11.000000000 +0900
+++ index.rb	2006-06-11 00:52:18.203125000 +0900
@@ -60,11 +60,15 @@
 		body = ''
 		head['Last-Modified'] = CGI::rfc1123_date( tdiary.last_modified )

+		require 'time'
+		ims = ENV['HTTP_IF_MODIFIED_SINCE']; ims = ims ? Time.httpdate(ims) : Time.at(0); # ENV?
+		diary_changed = (tdiary.last_modified - ims) > 30; # 30?
+
 		# ETag testing code
 		#require 'md5'
 		#head['ETag'] = MD5::md5( body )

-		if /HEAD/i !~ @cgi.request_method then
+		if /HEAD/i !~ @cgi.request_method and diary_changed then
 			if @cgi.mobile_agent? then
 				body = conf.to_mobile( tdiary.eval_rhtml( 'i.' ) )
 				head['charset'] = conf.mobile_encoding
@@ -73,15 +77,15 @@
 				body = tdiary.eval_rhtml
 				head['charset'] = conf.encoding
 				head['Content-Length'] = body.size.to_s
-				head['Pragma'] = 'no-cache'
-				head['Cache-Control'] = 'no-cache'
+#				head['Pragma'] = 'no-cache'
+#				head['Cache-Control'] = 'no-cache'
 			end
 			head['cookie'] = tdiary.cookies if tdiary.cookies.size > 0
 			print @cgi.header( head )
 			print body
 		else
-			head['Pragma'] = 'no-cache'
-			head['Cache-Control'] = 'no-cache'
+#			head['Pragma'] = 'no-cache'
+#			head['Cache-Control'] = 'no-cache'
 			print @cgi.header( head )
 		end
 	rescue TDiary::ForceRedirect

2006年06月04日 (日)

[tDiary]テーマ:横幅を制限してみた。

以前から横にだらだら延びた段落は醜いと思っていた。

このようなページを見つけた。C O U L D:固定か可変かそれが問題だ

早速 cssに下記の一行を追加した。

body { max-width: 50em }

emってな曖昧な単位に少し不安があるが Firefox1.5と IE7.0で同じように見えるので良しとする。


2006年05月30日 (火)

[tDiary] tdiary/hikidoc.rb: <pre>の中でも強調したい

<pre>の中だからってタグが書けないわけじゃなし。インライン要素なら OKのはず。

C:\Documents and Settings\ds14050\デスクトップ>diff -u hikidoc.rb~ hikidoc.rb
--- hikidoc.rb~ 2005-10-06 16:42:35.000000000 +0900
+++ hikidoc.rb  2006-05-30 06:34:32.265625000 +0900
@@ -142,8 +142,9 @@
   end

   def restore_pre( text )
-    ret = unescape_meta_char( text, true )
-    ret = restore_plugin_block( ret, true )
+    text = inline_parser( text )
+#    ret = unescape_meta_char( text, true )
+#    ret = restore_plugin_block( ret, true )
   end

   ######################################################################
  • inline_parser()が ''強調'', '''もっと強調''', ==打消==, [[WikiName]]や URLっぽい本文を解釈する。
  • unescape_meta_char( text, true )をコメントアウトすることでメタ文字({, }, :, ', ", |)のエスケープを有効にしてるのは inline_parser()によってシングルクォートなどが解釈されるようになった結果、<pre>の中にシングルクォートそのものを書く方法がなくなったから。
  • restore_plugin_block(ret, true)をコメントアウトしてるのは <pre>の中でもプラグインを使いたいから。
  • メタ文字をリストアップしていて気付いたけどイコールとブラケットがメタ文字に含まれてなくてエスケープができないから、こんなふうに ==打ち消し== とか [[WikiName]] の書き方の例示ができなくね? 左のは {{'=='}} みたいにプラグイン記法でなんとかしたけど。

 <pre>の中と外でマークアップが変わらないことのテスト

 [Wikiソース]
''test''
 ''test''
''test&'test\'''
 ''test&'test\'''
 [変換後のHTML]
<p><em>test</em></p>
<pre>
<em>test</em>
</pre>
<p><em>test&amp;'test'</em></p>
<pre>
<em>test&amp;'test'</em>
</pre>

2006年01月17日 (火)

[tDiary] 新Wikiスタイル(HikiDoc):引用の中で<pre>。できるやん

20050929p02で引用の中で <pre>を使いたい言うてましたが、どうやら最初から可能だったご様子。

hikidoc.rbからの引用。

 ######################################################################
 # blockquote

 BLOCKQUOTE_RE = /^""[ \t]?/
 BLOCKQUOTES_RE = /(#{BLOCKQUOTE_RE}.*\n?)+/

 def parse_blockquote( text )
   text.gsub( BLOCKQUOTES_RE ) do |str|
     str.chomp!
     str.gsub!( BLOCKQUOTE_RE, '' )
     "\n<blockquote>\n%s\n</blockquote>\n\n" % block_parser(str)
   end
 end
  1. BLOCKQUOTES_RE(行頭が""と 1つか0個の空白・タブ文字で始まる行の連続)にマッチする文字列が見つかったら
  2. BLOCKQUOTE_RE(各行頭の ""とそれに続く 1つか0個の空白・タブ文字) を取り除いた後
  3. block_parser()を呼び
  4. その結果を <blockquote>タブで囲む。
 ######################################################################
 # block parser
 ######################################################################

 def block_parser( text )
   ret = text
   ret = parse_plugin( ret )
   ret = parse_pre( ret )
   ret = parse_comment( ret )
   ret = parse_header( ret )
   ret = parse_hrules( ret )
   ret = parse_list( ret )
   ret = parse_definition( ret )
   ret = parse_blockquote( ret )
   ret = parse_table( ret )
   ret = parse_paragraph( ret )
   ret.lstrip
 end

block_parser()は見ての通り、整形済みテキスト(pre)、見出し、リスト、引用、テーブル、段落などを解釈する。

つまり、引用の中には (<pre>も含めて) block_parserが解釈するブロック要素が全て書ける。

何故今日まで気付かなかったのかというとコレ↓

 BLOCKQUOTE_RE = /^""[ \t]?/

ドキュメントでは一般的に、「行頭に "" を付ければ引用になります」と書かれるけど、hikidoc.rbの実装では それに続く空白・タブ文字も引用を表す記号の一部として扱われている。行頭の空白は整形済みテキストを表すりっぱな Wiki記法の一部なのに。

そういうわけで

 引用の中で整形済みテキストを書くときは 行頭を「""  」(注:スペースは 2つ)で始めます。

2005年11月11日 (金)

[tDiary]レーベルが取得できないときは出版社を表示してお茶を濁してみました。

現在のフォーマット:

'#{author} 【#{title}】 #{label.empty? ? publisher : label}'

20051108#p06のやり方では、『9S(3)』を使って「電撃文庫」を取得できるのは試したし、「ジャンプコミックス」と「角川スニーカー文庫」も多分取得できる。でもこれら一部大手を除くと、富士見ミステリーもファミ通文庫も、その他殆どのレーベルで取得できない。<ItemAttributes>拡張してよ、アマゾンさん。


2005年09月29日 (木)

最終更新: 2011-02-13T06:07+0900

[tDiary]新Wikiスタイル(HikiDoc)の拡張案:引用の中で<pre>

引用文の表現力が上がってるのだけど、もうひとつ、引用の中で整形済みテキスト(pre)を使いたい。

すぐ上のセクション(↑)で、改行を維持するために引用の中のコード部分だけを<pre>で囲ってるのだけど、そのソースはちょっと汚い。(上から二行目と下から二行目で)直接HTMLを埋め込んでるから後々 tDiaryをXHTML化したりするのが難しくなる。HTML化はWikiパーサに全て任せたい。tdiary/hikidoc.rbを拡張して引用文中の<pre>が可能にならないものか。

""7×7の二次元配列aがあるとする。たとえば、以下のようなものである。
"" int a[7,7] ={
""   {1,1,1,1,1,1,1},
""   {1,0,0,0,0,0,1},
""   {1,0,1,1,1,0,1},
""   {1,0,0,0,1,1,1},
""   {1,0,0,1,0,0,1},
""   {1,0,0,0,1,0,1},
""   {1,1,1,1,1,1,1}
"" };
""いま、aを迷路と見立てる。a[x,y]が0の箇所は歩けて、1の箇所は歩けな

 [2006-01-17] (最初から)できるみたいよ _|‾|○

20060117p04