# # my-sequel.rb # # show links to follow-up entries # # Copyright 2006 zunda and # NISHIMURA Takashi # # Permission is granted for use, copying, modification, distribution, # and distribution of modified versions of this work under the terms of GPL version 2 or later. # # Language resources can be found in the middle of this file. # Please search a line with `language resource' # require 'pstore' unless defined?(ERB) require 'erb' end class MySequel include ERB::Util extend ERB::Util class Conf include ERB::Util Prefix = 'my_sequel.' unless @conf then def self::to_native(str) return str end else def self::to_native(str) @conf.to_native(str) end end def self::handler_escape(string) string.gsub(/\r/n, '').gsub(/&/n, '&').gsub(/"/n, '"').gsub(/>/n, '>').gsub(/').replace(/"/g, '"').replace(/&/g, '&'); } function uncheck(element) { document.getElementById(element.id+".reset").checked = false; } function restore(element) { var text_id = element.id.replace(/\.reset$/, "") if (element.checked) { document.getElementById(text_id).value = unescape(default_values[text_id]); } } _END end def initialize(conf_hash) @default_hash = conf_hash @conf_hash = Hash.new end # takes configuration from @options trusting the input def merge_hash(hash) @default_hash.each_key do |key| prefixed = Prefix + key.to_s @conf_hash[key] = hash[prefixed] if hash.has_key?(prefixed) end end # takes configuration from @cgi.params def merge_params(params) @default_hash.each_key do |key| keystr = key.to_s if params[keystr+'.reset'] and params[keystr+'.reset'][0] then @conf_hash.delete(key) elsif params[keystr] then @conf_hash[key] = params[keystr][0] end end end # returns current configuration def [](key) if @conf_hash.has_key?(key) then return @conf_hash[key] else return @default_hash[key][:default] end end # returns hash of configured values def to_conf_hash(target_hash) @default_hash.each_key do |key| target_hash.delete(Prefix + key.to_s) end @conf_hash.each_pair do |key, value| target_hash[Prefix + key.to_s] = value end end # returns an HTML sniplet for configuration interface def html(restore_default_label) return @default_hash.keys.sort_by{|k| @default_hash[k][:index]}.map{|k| idattr = %Q| id="#{h k.to_s}"| idattr_reset = %Q| id="#{h k.to_s}.reset"| uncheck = ' onfocus="uncheck(this)"' restore = ' onchange="restore(this)" onclick="restore(this)"' r = %Q|\t

#{h @default_hash[k][:title]}

\n| description = @default_hash[k][:description] r += %Q|\t

#{h description}

\n| if description unless @default_hash[k][:textarea] r += %Q|\t

| else cols = 70 rows = 10 if @default_hash[k][:textarea].respond_to?(:[]) then cols = @default_hash[k][:textarea][:cols] || cols rows = @default_hash[k][:textarea][:rows] || rows end r += %Q|\t

| end name = "#{h k.to_s}.reset" r += %Q| - 

\n| r }.join end # Javascript hash literal for default values def default_js_hash r = "default_values = {\n" r += @default_hash.keys.sort_by{|k| @default_hash[k][:index]}.map{|k| %Q|\t"#{h k}": "#{Conf::handler_escape(@default_hash[k][:default])}"| }.join(",\n") r += "\n};\n" return r end def handler_block return <<"_END" _END end end # CSS sniplet for sequels def self::css(inner_css) unless inner_css.strip.empty? return <<"_END" \t _END else return '' end end # cache directory for this plguin def self::cache_dir(cache_path) return File.join(cache_path, 'my_sequel') end # cache file for a month: #{yyyy}/#{yyyymm}.#{src or dst}.dat def self::cache_file(cache_path, anchor, direction) return File.join(MySequel.cache_dir(cache_path), MySequel.year(anchor), "#{MySequel.month(anchor)}.#{direction}.dat") end # unique for each month def self::cache_key(anchor) return MySequel.month(anchor) end # for each cache key for dates def self::each_cache_key(dates) dates = dates.is_a?(String) ? [dates] : dates dates.map{|ymd| MySequel.cache_key(ymd)}.uniq.each do |cache_file| yield(cache_file) end end # yyyy def self::year(anchor) return anchor.scan(/\d{4,4}/)[0] end # yyyymm def self::month(anchor) return anchor.scan(/\d{6,6}/)[0] end # yyyymmdd def self::date(anchor) if anchor.respond_to?(:localtime) return anchor.localtime.strftime("%Y%m%d") else return anchor.scan(/\d{8,8}/)[0] end end # add an entry to Array value of hash, making new Array if needed def self::push_to_hash(hash, key, element) unless hash.has_key?(key) hash[key] = Array.new begin hash[key] rescue SecurityError end end hash[key] << element hash end def initialize(cache_path) @link_srcs = Hash.new # key:dst anchor value:Array of src anchors @current_dsts = Hash.new # key:src anchor value:Array of dst anchors @cached_dsts = Hash.new # for restore_dsts and clean_srcs @vanished_dsts = Hash.new # key:src date value:Array of dst anchors @cache_path = cache_path end def restore(dates) restore_srcs(dates) restore_dsts(dates) end # HTML sniplet for sequels def html(dst_anchor, date_format, label) anchors = srcs(dst_anchor) if anchors and not anchors.empty? then r = %Q|
#{h label}| r += anchors.map{|src_anchor| yield(src_anchor, Time.local(*(src_anchor.scan(/(\d{4,4})(\d\d)(\d\d)/)[0])).strftime(date_format)) }.join(', ') r += "
\n" return r else return '' end end # Array of source anchors for a destination anchor, nil if none def srcs(dst_anchor) a = @link_srcs[dst_anchor] return nil if not a or a.empty? return a.uniq.sort end # starts a day - get ready to scan the diary for the section def clean_dsts(date) datestr = MySequel.date(date) @current_dsts.keys.each do |src_anchor| next unless MySequel.date(src_anchor) == datestr @current_dsts[src_anchor] = Array.new begin @current_dsts[src_anchor] rescue SecurityError end end end # adds a link def add(src_anchor, dst_anchor) MySequel.push_to_hash(@link_srcs, dst_anchor, src_anchor) MySequel.push_to_hash(@current_dsts, src_anchor, dst_anchor) end # detect vanished links def clean_srcs (@cached_dsts.keys + @current_dsts.keys).uniq.each do |src_anchor| if @cached_dsts[src_anchor] then if @current_dsts[src_anchor] then @vanished_dsts[src_anchor] = @cached_dsts[src_anchor] - @current_dsts[src_anchor] else @vanished_dsts[src_anchor] = @cached_dsts[src_anchor] end end @cached_dsts[src_anchor] = @current_dsts[src_anchor].dup end end # restores cached data for a month # calls the block for each root giving key and value def each_cached(anchor, direction) path = MySequel.cache_file(@cache_path, anchor, direction) begin PStore.new(path).transaction(true) do |db| db.roots.each do |cached_anchor| yield(cached_anchor, db[cached_anchor]) end end rescue TypeError # corrupted PStore data File.unlink(path) rescue PStore::Error # corrupted PStore data begin File.unlink(path) rescue Errno::ENOENT end rescue Errno::ENOENT # no cache yet end end private :each_cached # restores cached sources for a month def restore_srcs(dates) @srcs_loaded ||= Hash.new MySequel.each_cache_key(dates) do |cache_key| unless @srcs_loaded[cache_key] then each_cached(cache_key, 'src') do |anchor, array| unless @link_srcs.has_key?(anchor) @link_srcs[anchor] = array else @link_srcs[anchor] += array end end @srcs_loaded[cache_key] = true end end end # restores cached destinations def restore_dsts(dates) @dsts_loaded ||= Hash.new MySequel.each_cache_key(dates) do |cache_key| unless @dsts_loaded[cache_key] then each_cached(cache_key, 'dst') do |anchor, array| @cached_dsts[anchor] = array @current_dsts[anchor] = array.dup end @dsts_loaded[cache_key] = true end end end # hash for storing cache # key: path to cache # value: Hash # key: anchor # value: compacted and uniqed Array of anchor on the other side of link def hash_for_cache(link_hash, direction) r = Hash.new link_hash.each_pair do |pivot_anchor, anchor_array| c = anchor_array.compact.uniq path = MySequel.cache_file(@cache_path, pivot_anchor, direction) r[path] ||= Hash.new r[path][pivot_anchor] = c end return r end private :hash_for_cache # stores the data def store(cache_hash) cache_hash.each_pair do |path, h| d = File.dirname(path) Dir.mkdir(d) unless File.exist?(d) PStore.new(path).transaction do |db| h.each_pair do |k, v| unless v.empty? then db[k] = v else db.delete(k) end end end end end private :store # commits on-memory results to files def commit d = MySequel.cache_dir(@cache_path) Dir.mkdir(d) unless File.exist?(d) restore_srcs(@link_srcs.keys) restore_srcs(@vanished_dsts.values.flatten) @vanished_dsts.each_pair do |src_anchor, dst_anchors| dst_anchors.uniq.each do |dst_anchor| @link_srcs[dst_anchor].reject!{|anchor| anchor == src_anchor} end end store(hash_for_cache(@link_srcs, 'src')) store(hash_for_cache(@current_dsts, 'dst')) @vanished_dsts = Hash.new end end # register this plguin to tDiary unless defined?(Test::Unit) # language resource and configuration @my_sequel_plugin_name ||= 'Link to follow ups' @my_sequel_description ||= <<_END

Shows links to follow-up entries, which have `my' link to the entry in the past.

Do not forget to push the OK button to store the changes.

_END @my_sequel_label_conf ||= 'Link label' @my_sequel_label ||= 'Follow up: ' @my_sequel_restore_default_label ||= 'Restore default' @my_sequel_default_hash ||= { :label => { :title => 'Link label', :default => 'Follow up: ', :description => 'Prefix for links to the follow-ups', :index => 1, }, :date_format => { :title => 'Link format', :default => @date_format, :description => 'Time format of links to the follow-ups. Sequences of % and a charactor are converted as follows: "%Y" to year, "%m" to month in number, "%b" to short name of month, "%B" to full name of month, "%d" to day of month, "%a" to short name of day of week, and "%A" to full name of day of week, for the follow-up.', :index => 2, }, :inner_css => { :title => 'CSS', :default => <<'_END', font-size: 75%; text-align: right; margin: 0px; _END :description => 'CSS for the links. The followoing is applied to div.sequel.', :index => 3, :textarea => {rows: 5}, }, } @my_sequel_conf = MySequel::Conf.new(@my_sequel_default_hash) @my_sequel_conf.merge_hash(@options) # configuration interface add_conf_proc( 'my-sequel', @my_sequel_plugin_name ) do if @mode == 'saveconf' then @my_sequel_conf.merge_params(@cgi.params) @my_sequel_conf.to_conf_hash(@conf) end <<"_HTML" #{@my_sequel_conf.handler_block}

#{@my_sequel_plugin_name}

#{@my_sequel_description} #{@my_sequel_conf.html(@my_sequel_restore_default_label).chomp} _HTML end @my_sequel = MySequel.new(@cache_path) @my_sequel_active = false # activate this plugin if header procs are called # - This avoids being called from makerss.rb add_header_proc do unless bot? @my_sequel_active = true @my_sequel.restore(@diaries.keys) MySequel.css(@my_sequel_conf[:inner_css]) end end # preparation for a day add_body_enter_proc do |date| if @my_sequel_active then if date then @my_sequel_date = MySequel.date(date) @my_sequel.clean_dsts(@my_sequel_date) else @my_sequel_date = nil end end '' end # preparation for a section add_section_enter_proc do |date, index| if @my_sequel_active and @my_sequel_date then @my_sequel_anchor = "#{@my_sequel_date}#p#{'%02d' % index}" end '' end # plugin function to be called from within sections alias :my_sequel_orig_my :my unless defined?(my_sequel_orig_my) def my(*args) if @my_sequel_active and @my_sequel_date and @my_sequel_anchor and @mode != 'preview' then dst_date, frag = args[0].scan(/(\d{8,8})(?:[^\d]*)(?:#?p(\d+))?$/)[0] if dst_date and dst_date < @my_sequel_date then dst_anchor = "#{dst_date}#{frag ? "#p%02d" % frag.to_i : ''}" @my_sequel.add(@my_sequel_anchor, dst_anchor) end end my_sequel_orig_my(*args) end # show sequels when leaving a section add_section_leave_proc do r = '' if @my_sequel_active and @my_sequel_date and @my_sequel_anchor and not bot? then r = @my_sequel.html(@my_sequel_anchor, @my_sequel_conf[:date_format], @my_sequel_conf[:label]){|src_anchor, anchor_str| my_sequel_orig_my(src_anchor, anchor_str) } end @my_sequel_anchor = nil r end # show sequels when leaving a day add_body_leave_proc do r = '' if @my_sequel_active and @my_sequel_date then unless bot? r = @my_sequel.html(@my_sequel_anchor, @my_sequel_conf[:date_format], @my_sequel_conf[:label]){|src_anchor, anchor_str| my_sequel_orig_my(src_anchor, anchor_str) } end end @my_sequel_date = nil r end # commit changes add_footer_proc do if @my_sequel_active then @my_sequel.clean_srcs @my_sequel.commit end '' end end # Local Variables: # mode: ruby # indent-tabs-mode: t # tab-width: 3 # ruby-indent-level: 3 # End: