/misc/plugin/my-sequel.rb
Ruby | 539 lines | 449 code | 40 blank | 50 comment | 17 complexity | 435de2a97372f3244eae184eb76a9daa MD5 | raw file
- #
- # my-sequel.rb
- #
- # show links to follow-up entries
- #
- # Copyright 2006 zunda <zunda at freeshell.org> and
- # NISHIMURA Takashi <nt at be.to>
- #
- # Permission is granted for use, copying, modification, distribution,
- # and distribution of modified versions of this work under the terms
- # of GPL version 2.
- #
- # Language resources can be found in the middle of thie 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(/</n, '<').gsub(/\n/n, '\n')
- end
- def self::handler_scriptlet
- return <<'_END'
- function unescape(string) {
- return string.replace(/</g, '<').replace(/>/g, '>').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, mobile = false)
- return @default_hash.keys.sort_by{|k| @default_hash[k][:index]}.map{|k|
- idattr = mobile ? '' : %Q| id="#{h k.to_s}"|
- idattr_reset = mobile ? '' : %Q| id="#{h k.to_s}.reset"|
- uncheck = mobile ? '' : ' onfocus="uncheck(this)"'
- restore = mobile ? '' : ' onchange="restore(this)" onclick="restore(this)"'
- r = %Q|\t<h3 class="subtitle">#{h @default_hash[k][:title]}</h3>\n|
- description = @default_hash[k][:description]
- r += %Q|\t<p>#{h description}</p>\n| if description and not mobile
- unless @default_hash[k][:textarea]
- r += %Q|\t<p><input name="#{h k.to_s}"#{idattr} type="text" value="#{h(Conf.to_native(self[k]))}"#{uncheck}>|
- 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<p><textarea name="#{h k.to_s}"#{idattr} cols="#{h cols}" rows="#{h rows}"#{uncheck}>#{h(Conf.to_native(self[k]))}</textarea>|
- end
- name = "#{h k.to_s}.reset"
- r += %Q| - <label for="#{name}"><input id="#{name}" name="#{name}"#{idattr_reset} type="checkbox" value="t"#{restore}>#{restore_default_label}</label></p>\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"
- <script type="text/javascript"><!--
- #{default_js_hash}#{Conf::handler_scriptlet}// --></script>
- _END
- end
- end
- # CSS sniplet for sequels
- def self::css(inner_css)
- unless inner_css.strip.empty?
- return <<"_END"
- \t<style type="text/css" media="all"><!--
- \tdiv.sequel {
- #{h(inner_css.gsub(/^\s*/, "\t\t").gsub(/\r?\n/, "\n"))}\t}
- \t--></style>
- _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].taint
- rescue SecurityError
- end
- end
- hash[key] << element
- hash
- end
- def initialize(cache_path)
- @link_srcs = Hash.new.taint # key:dst anchor value:Array of src anchors
- @current_dsts = Hash.new.taint # key:src anchor value:Array of dst anchors
- @cached_dsts = Hash.new.taint # for restore_dsts and clean_srcs
- @vanished_dsts = Hash.new.taint # 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|<div class="sequel">#{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 += "</div>\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].taint
- 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.taint
- else
- @link_srcs[anchor] += array.taint
- 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|
- array.taint
- @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.taint
- 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
- <p>Shows links to follow-up entries,
- which have `my' link to the entry in the past.</p>
- <p>Do not forget to push the OK button to store the changes.</p>
- _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 <code>div.sequel</code>.',
- :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}
- <h3>#{@my_sequel_plugin_name}</h3>
- #{@my_sequel_description}
- #{@my_sequel_conf.html(@my_sequel_restore_default_label, @conf.mobile_agent?).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
- if not @conf.bot? and not @conf.mobile_agent? then
- @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 @conf.bot? and not @conf.mobile_agent? 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
- if not @conf.bot? and not @conf.mobile_agent? 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
- 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: