10月22日に書いたように「カテゴリ」表示モードは「最新」「月」表示と見た目が違って違和感があるので、カテゴリ表示を最新表示と月表示に近づけてみた。(この日記で実働中)。
新しく表示モード(TDiaryViewを継承したクラス)を作るのは手間だし、TDiaryLatestや TDiaryMonthからのコピペばっかりになることが想像できるので IOのラッパを作って TDiaryLatestや TDiaryMonthを騙すことに。
transactionメソッドを引っかけて元々の IO(DefaultIOか PStoreIO)が渡してきた diariesから特定のカテゴリを持たないセクションを取り除いて呼び出し元に渡す。
transactionの呼び出し元に不完全な日記データが渡る関係上、日記の変更は捨てる。(読み出し専用)
個々の日記の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]
カテゴリの指定に 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
騙すのは 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
新規追加ファイル。貼り付けるには厳しい量だな。貼るけど。
既存の 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
ナビリンクとカレンダーを 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
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
検索結果の表示も同じ方法でできる。スコア順に表示したりはできないけど、特定の語を含む日記を時系列順に閲覧することはできる。ワードフィルタ? IOの三段重ねでワードフィルタとカテゴリフィルタを両方適用することもできるね。
やってみた@2007-12-18。→http://vvvvvv.sakura.ne.jp/ds14050/diary/?word=windows
単語のハイライトとかナビゲーションリンクの対応とかカレンダーをどうするかとかは一切放置。本当にやってみただけ。word=WORD1というパラメータを付ければ「月別」「最新」「カテゴリ」表示のどれでもフィルタリングできるはず。
2008年の 1月頃から約 1年間、上記のリンク先が Internal Server Errorになっていたもよう。なんたる無様。それに気付くきっかけが連日のコメントスパムというのがなんともはや。