#!/usr/bin/env ruby # # tdiarysearch # # Copyright (C) 2003,2004 Minero Aoki # # This program is free software. # You can distribute/modify this program under the terms of # the GNU GPL, General Public License version 2. # # $Id: search.rb,v 1.12 2004/05/22 18:31:47 aamine Exp $ # # Project home page: http://i.loveruby.net/w/tdiarysearch.html # # # Static Configurations # LOGGING = true LOGFILE_NAME = 'search.log' DEBUG = $DEBUG # # HTML Templates # def unindent(str, n) str.gsub(/^ {0,#{n}}/, '') end HEADER = unindent(<<-'EOS', 2)
<%= short_html(component) %>
<%= toomanyhits ? 'too many hits.' : nhits.to_s+' hits.' %>
#{SEARCH_FORM} EOS SEARCH_ERROR = unindent(<<"EOS", 2) #{SEARCH_FORM} <%= escape(reason) %>. EOS HISTORY = unindent(<<"EOS", 2)error
' begin html = generate_page(cgi) ensure send_html cgi, html end exit 0 end def generate_page(cgi) query = nil begin theme = @config.theme if LOGGING and File.file?(query_log()) and cgi.valid?('history') return history_page(theme) end begin return search_form_page(theme) unless cgi.valid?('q') initialize_tdiary_plugins cgi query = @config.to_native([cgi.params['q']].flatten.compact.join(' ')) patterns = setup_patterns(query) html = search_result_page(theme, patterns) save_query(query, query_log()) if LOGGING return html rescue WrongQuery => err return search_error_page(theme, (patterns || []), err.message) end rescue Exception => err html = '' html << HEADER html << "\n" html << 'q=' << escape(query) << "\n" if query html << escape(err.class.name) << "\n" if DEBUG html << escape(err.message) << "\n" html << err.backtrace.map {|i| escape(i) }.join("\n") if DEBUG html << "\n" html << FOOTER return html end end def send_html(cgi, html) print cgi.header('status' => '200 OK', 'type' => 'text/html', 'charset' => 'euc-jp', 'Content-Length' => html.length.to_s, 'Cache-Control' => 'no-cache', 'Pragma' => 'no-cache') print html unless cgi.request_method == 'HEAD' end def setup_patterns(query) patterns = split_string(query).map {|pat| check_pattern pat /#{Regexp.quote(pat)}/ie } raise WrongQuery, 'no pattern' if patterns.empty? raise WrongQuery, 'too many sub patterns' if patterns.length > 8 patterns end def check_pattern(pat) raise WrongQuery, 'no pattern' unless pat raise WrongQuery, 'empty pattern' if pat.empty? raise WrongQuery, "pattern too short: #{pat}" if pat.length < 2 raise WrongQuery, 'pattern too long' if pat.length > 128 end def split_string(str) str.split(/[\s#{Z_SPACE}]+/oe).reject {|w| w.empty? } end def save_query(query, file) File.open(file, 'a') {|f| begin f.flock(File::LOCK_EX) f.puts "#{Time.now.to_i}: #{query.dump}" ensure f.flock(File::LOCK_UN) end } end # # eRuby Dispatchers and Helper Routines # def search_form_page(theme) patterns = [] ERB.new(HEADER + SEARCH_FORM + FOOTER).result(binding()) end def search_result_page(theme, patterns) ERB.new(HEADER + SEARCH_RESULT + FOOTER).result(binding()) end def search_error_page(theme, patterns, reason) ERB.new(HEADER + SEARCH_ERROR + FOOTER).result(binding()) end def history_page(theme) patterns = [] ERB.new(HEADER + HISTORY + FOOTER).result(binding()) end def query_log "#{@config.data_path}#{LOGFILE_NAME}" end N_SHOW_QUERY_MAX = 20 def recent_queries return unless File.file?(query_log()) File.readlines(query_log()).reverse[0, N_SHOW_QUERY_MAX].map {|line| time, q = *line.split(/:/, 2) [Time.at(time.to_i), eval(q)] } end INF = 1 / 0.0 def match_components(patterns) foreach_diary_from_latest do |diary| next unless diary.visible? num = 1 diary.each_section do |sec| if patterns.all? {|re| re =~ sec.to_src } yield diary, fragment('p', num), sec end num += 1 end diary.each_visible_comment(INF) do |cmt, num| if patterns.all? {|re| re =~ cmt.body } yield diary, fragment('c', num), cmt end end end end def fragment(type, num) sprintf('%s%02d', type, num) end # # tDiary Implementation Dependent # def foreach_diary_from_latest(&block) foreach_data_file(@config.data_path.sub(%r+\z>, '')) do |path| read_diaries(path).sort_by {|diary| diary.date }.reverse_each(&block) end end def foreach_data_file(data_path, &block) Dir.glob("#{data_path}/[0-9]*/*.td2").sort.reverse_each do |path| yield path.untaint end end def read_diaries(path) d = nil diaries = {} load_tdiary_textdb(path) do |header, body| d = diary_class(header['Format']).new(header['Date'], '', body) d.show(header['Visible'] != 'false') diaries[d.ymd] = d end (Years[d.y] ||= []).push(d.m) if d load_comments diaries, path diaries.values end DIARY_CLASS_CACHE = {} def diary_class(style) c = DIARY_CLASS_CACHE[style] return c if c require "tdiary/#{style.downcase}_style.rb" c = eval("TDiary::#{style.capitalize}Diary") c.__send__(:include, DiaryClassDelta) DIARY_CLASS_CACHE[style] = c c end module DiaryClassDelta def ymd date().strftime('%Y%m%d') end def y_m_d date().strftime('%Y-%m-%d') end def y '%04d' % date().year end def m '%02d' % date().month end end def load_comments(diaries, path) cmtfile = path.sub(/2\z/, 'c') return unless File.file?(cmtfile) load_tdiary_textdb(cmtfile) do |header, body| c = TDiary::Comment.new(header['Name'], header['Mail'], body, Time.at(header['Last-Modified'].to_i)) c.show = (header['Visible'] != 'false') d = diaries[header['Date']] d.add_comment c if d end end def load_tdiary_textdb(path) File.open(path) {|f| ver = f.gets.strip raise "unkwnown format: #{ver}" unless ver == 'TDIARY2.00.00' f.each('') do |header| h = {} header.untaint.strip.each do |line| n, v = *line.split(':', 2) if v == nil next end h[n.strip] = v.strip end yield h, f.gets("\n.\n").chomp(".\n").untaint end } end def short_html(component) # Section classes do not have common superclass, we can't use class here. case component.class.name when /Section/ section = component if section.subtitle sprintf('%s