PageRenderTime 57ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/misc/plugin/my-sequel.rb

https://github.com/kayakaya/tdiary-core
Ruby | 539 lines | 449 code | 40 blank | 50 comment | 17 complexity | 435de2a97372f3244eae184eb76a9daa MD5 | raw file
  1. #
  2. # my-sequel.rb
  3. #
  4. # show links to follow-up entries
  5. #
  6. # Copyright 2006 zunda <zunda at freeshell.org> and
  7. # NISHIMURA Takashi <nt at be.to>
  8. #
  9. # Permission is granted for use, copying, modification, distribution,
  10. # and distribution of modified versions of this work under the terms
  11. # of GPL version 2.
  12. #
  13. # Language resources can be found in the middle of thie file.
  14. # Please search a line with `language resource'
  15. #
  16. require 'pstore'
  17. unless defined?(ERB)
  18. require 'erb'
  19. end
  20. class MySequel
  21. include ERB::Util
  22. extend ERB::Util
  23. class Conf
  24. include ERB::Util
  25. Prefix = 'my_sequel.'
  26. unless @conf then
  27. def self::to_native(str)
  28. return str
  29. end
  30. else
  31. def self::to_native(str)
  32. @conf.to_native(str)
  33. end
  34. end
  35. def self::handler_escape(string)
  36. string.gsub(/\r/n, '').gsub(/&/n, '&amp;').gsub(/"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;').gsub(/\n/n, '\n')
  37. end
  38. def self::handler_scriptlet
  39. return <<'_END'
  40. function unescape(string) {
  41. return string.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&amp;/g, '&');
  42. }
  43. function uncheck(element) {
  44. document.getElementById(element.id+".reset").checked = false;
  45. }
  46. function restore(element) {
  47. var text_id = element.id.replace(/\.reset$/, "")
  48. if (element.checked) {
  49. document.getElementById(text_id).value = unescape(default_values[text_id]);
  50. }
  51. }
  52. _END
  53. end
  54. def initialize(conf_hash)
  55. @default_hash = conf_hash
  56. @conf_hash = Hash.new
  57. end
  58. # takes configuration from @options trusting the input
  59. def merge_hash(hash)
  60. @default_hash.each_key do |key|
  61. prefixed = Prefix + key.to_s
  62. @conf_hash[key] = hash[prefixed] if hash.has_key?(prefixed)
  63. end
  64. end
  65. # takes configuration from @cgi.params
  66. def merge_params(params)
  67. @default_hash.each_key do |key|
  68. keystr = key.to_s
  69. if params[keystr+'.reset'] and params[keystr+'.reset'][0] then
  70. @conf_hash.delete(key)
  71. elsif params[keystr] then
  72. @conf_hash[key] = params[keystr][0]
  73. end
  74. end
  75. end
  76. # returns current configuration
  77. def [](key)
  78. if @conf_hash.has_key?(key) then
  79. return @conf_hash[key]
  80. else
  81. return @default_hash[key][:default]
  82. end
  83. end
  84. # returns hash of configured values
  85. def to_conf_hash(target_hash)
  86. @default_hash.each_key do |key|
  87. target_hash.delete(Prefix + key.to_s)
  88. end
  89. @conf_hash.each_pair do |key, value|
  90. target_hash[Prefix + key.to_s] = value
  91. end
  92. end
  93. # returns an HTML sniplet for configuration interface
  94. def html(restore_default_label, mobile = false)
  95. return @default_hash.keys.sort_by{|k| @default_hash[k][:index]}.map{|k|
  96. idattr = mobile ? '' : %Q| id="#{h k.to_s}"|
  97. idattr_reset = mobile ? '' : %Q| id="#{h k.to_s}.reset"|
  98. uncheck = mobile ? '' : ' onfocus="uncheck(this)"'
  99. restore = mobile ? '' : ' onchange="restore(this)" onclick="restore(this)"'
  100. r = %Q|\t<h3 class="subtitle">#{h @default_hash[k][:title]}</h3>\n|
  101. description = @default_hash[k][:description]
  102. r += %Q|\t<p>#{h description}</p>\n| if description and not mobile
  103. unless @default_hash[k][:textarea]
  104. r += %Q|\t<p><input name="#{h k.to_s}"#{idattr} type="text" value="#{h(Conf.to_native(self[k]))}"#{uncheck}>|
  105. else
  106. cols = 70
  107. rows = 10
  108. if @default_hash[k][:textarea].respond_to?(:[]) then
  109. cols = @default_hash[k][:textarea][:cols] || cols
  110. rows = @default_hash[k][:textarea][:rows] || rows
  111. end
  112. 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>|
  113. end
  114. name = "#{h k.to_s}.reset"
  115. r += %Q|&nbsp;-&nbsp;<label for="#{name}"><input id="#{name}" name="#{name}"#{idattr_reset} type="checkbox" value="t"#{restore}>#{restore_default_label}</label></p>\n|
  116. r
  117. }.join
  118. end
  119. # Javascript hash literal for default values
  120. def default_js_hash
  121. r = "default_values = {\n"
  122. r += @default_hash.keys.sort_by{|k| @default_hash[k][:index]}.map{|k|
  123. %Q|\t"#{h k}": "#{Conf::handler_escape(@default_hash[k][:default])}"|
  124. }.join(",\n")
  125. r += "\n};\n"
  126. return r
  127. end
  128. def handler_block
  129. return <<"_END"
  130. <script type="text/javascript"><!--
  131. #{default_js_hash}#{Conf::handler_scriptlet}// --></script>
  132. _END
  133. end
  134. end
  135. # CSS sniplet for sequels
  136. def self::css(inner_css)
  137. unless inner_css.strip.empty?
  138. return <<"_END"
  139. \t<style type="text/css" media="all"><!--
  140. \tdiv.sequel {
  141. #{h(inner_css.gsub(/^\s*/, "\t\t").gsub(/\r?\n/, "\n"))}\t}
  142. \t--></style>
  143. _END
  144. else
  145. return ''
  146. end
  147. end
  148. # cache directory for this plguin
  149. def self::cache_dir(cache_path)
  150. return File.join(cache_path, 'my_sequel')
  151. end
  152. # cache file for a month: #{yyyy}/#{yyyymm}.#{src or dst}.dat
  153. def self::cache_file(cache_path, anchor, direction)
  154. return File.join(MySequel.cache_dir(cache_path), MySequel.year(anchor), "#{MySequel.month(anchor)}.#{direction}.dat")
  155. end
  156. # unique for each month
  157. def self::cache_key(anchor)
  158. return MySequel.month(anchor)
  159. end
  160. # for each cache key for dates
  161. def self::each_cache_key(dates)
  162. dates = dates.is_a?(String) ? [dates] : dates
  163. dates.map{|ymd| MySequel.cache_key(ymd)}.uniq.each do |cache_file|
  164. yield(cache_file)
  165. end
  166. end
  167. # yyyy
  168. def self::year(anchor)
  169. return anchor.scan(/\d{4,4}/)[0]
  170. end
  171. # yyyymm
  172. def self::month(anchor)
  173. return anchor.scan(/\d{6,6}/)[0]
  174. end
  175. # yyyymmdd
  176. def self::date(anchor)
  177. if anchor.respond_to?(:localtime)
  178. return anchor.localtime.strftime("%Y%m%d")
  179. else
  180. return anchor.scan(/\d{8,8}/)[0]
  181. end
  182. end
  183. # add an entry to Array value of hash, making new Array if needed
  184. def self::push_to_hash(hash, key, element)
  185. unless hash.has_key?(key)
  186. hash[key] = Array.new
  187. begin
  188. hash[key].taint
  189. rescue SecurityError
  190. end
  191. end
  192. hash[key] << element
  193. hash
  194. end
  195. def initialize(cache_path)
  196. @link_srcs = Hash.new.taint # key:dst anchor value:Array of src anchors
  197. @current_dsts = Hash.new.taint # key:src anchor value:Array of dst anchors
  198. @cached_dsts = Hash.new.taint # for restore_dsts and clean_srcs
  199. @vanished_dsts = Hash.new.taint # key:src date value:Array of dst anchors
  200. @cache_path = cache_path
  201. end
  202. def restore(dates)
  203. restore_srcs(dates)
  204. restore_dsts(dates)
  205. end
  206. # HTML sniplet for sequels
  207. def html(dst_anchor, date_format, label)
  208. anchors = srcs(dst_anchor)
  209. if anchors and not anchors.empty? then
  210. r = %Q|<div class="sequel">#{h label}|
  211. r += anchors.map{|src_anchor|
  212. yield(src_anchor, Time.local(*(src_anchor.scan(/(\d{4,4})(\d\d)(\d\d)/)[0])).strftime(date_format))
  213. }.join(', ')
  214. r += "</div>\n"
  215. return r
  216. else
  217. return ''
  218. end
  219. end
  220. # Array of source anchors for a destination anchor, nil if none
  221. def srcs(dst_anchor)
  222. a = @link_srcs[dst_anchor]
  223. return nil if not a or a.empty?
  224. return a.uniq.sort
  225. end
  226. # starts a day - get ready to scan the diary for the section
  227. def clean_dsts(date)
  228. datestr = MySequel.date(date)
  229. @current_dsts.keys.each do |src_anchor|
  230. next unless MySequel.date(src_anchor) == datestr
  231. @current_dsts[src_anchor] = Array.new
  232. begin
  233. @current_dsts[src_anchor].taint
  234. rescue SecurityError
  235. end
  236. end
  237. end
  238. # adds a link
  239. def add(src_anchor, dst_anchor)
  240. MySequel.push_to_hash(@link_srcs, dst_anchor, src_anchor)
  241. MySequel.push_to_hash(@current_dsts, src_anchor, dst_anchor)
  242. end
  243. # detect vanished links
  244. def clean_srcs
  245. (@cached_dsts.keys + @current_dsts.keys).uniq.each do |src_anchor|
  246. if @cached_dsts[src_anchor] then
  247. if @current_dsts[src_anchor] then
  248. @vanished_dsts[src_anchor] = @cached_dsts[src_anchor] - @current_dsts[src_anchor]
  249. else
  250. @vanished_dsts[src_anchor] = @cached_dsts[src_anchor]
  251. end
  252. end
  253. @cached_dsts[src_anchor] = @current_dsts[src_anchor].dup
  254. end
  255. end
  256. # restores cached data for a month
  257. # calls the block for each root giving key and value
  258. def each_cached(anchor, direction)
  259. path = MySequel.cache_file(@cache_path, anchor, direction)
  260. begin
  261. PStore.new(path).transaction(true) do |db|
  262. db.roots.each do |cached_anchor|
  263. yield(cached_anchor, db[cached_anchor])
  264. end
  265. end
  266. rescue TypeError # corrupted PStore data
  267. File.unlink(path)
  268. rescue PStore::Error # corrupted PStore data
  269. begin
  270. File.unlink(path)
  271. rescue Errno::ENOENT
  272. end
  273. rescue Errno::ENOENT # no cache yet
  274. end
  275. end
  276. private :each_cached
  277. # restores cached sources for a month
  278. def restore_srcs(dates)
  279. @srcs_loaded ||= Hash.new
  280. MySequel.each_cache_key(dates) do |cache_key|
  281. unless @srcs_loaded[cache_key] then
  282. each_cached(cache_key, 'src') do |anchor, array|
  283. unless @link_srcs.has_key?(anchor)
  284. @link_srcs[anchor] = array.taint
  285. else
  286. @link_srcs[anchor] += array.taint
  287. end
  288. end
  289. @srcs_loaded[cache_key] = true
  290. end
  291. end
  292. end
  293. # restores cached destinations
  294. def restore_dsts(dates)
  295. @dsts_loaded ||= Hash.new
  296. MySequel.each_cache_key(dates) do |cache_key|
  297. unless @dsts_loaded[cache_key] then
  298. each_cached(cache_key, 'dst') do |anchor, array|
  299. array.taint
  300. @cached_dsts[anchor] = array
  301. @current_dsts[anchor] = array.dup
  302. end
  303. @dsts_loaded[cache_key] = true
  304. end
  305. end
  306. end
  307. # hash for storing cache
  308. # key: path to cache
  309. # value: Hash
  310. # key: anchor
  311. # value: compacted and uniqed Array of anchor on the other side of link
  312. def hash_for_cache(link_hash, direction)
  313. r = Hash.new
  314. link_hash.each_pair do |pivot_anchor, anchor_array|
  315. c = anchor_array.compact.uniq
  316. path = MySequel.cache_file(@cache_path, pivot_anchor, direction)
  317. r[path] ||= Hash.new
  318. r[path][pivot_anchor] = c
  319. end
  320. return r
  321. end
  322. private :hash_for_cache
  323. # stores the data
  324. def store(cache_hash)
  325. cache_hash.each_pair do |path, h|
  326. d = File.dirname(path)
  327. Dir.mkdir(d) unless File.exist?(d)
  328. PStore.new(path).transaction do |db|
  329. h.each_pair do |k, v|
  330. unless v.empty? then
  331. db[k] = v
  332. else
  333. db.delete(k)
  334. end
  335. end
  336. end
  337. end
  338. end
  339. private :store
  340. # commits on-memory results to files
  341. def commit
  342. d = MySequel.cache_dir(@cache_path)
  343. Dir.mkdir(d) unless File.exist?(d)
  344. restore_srcs(@link_srcs.keys)
  345. restore_srcs(@vanished_dsts.values.flatten)
  346. @vanished_dsts.each_pair do |src_anchor, dst_anchors|
  347. dst_anchors.uniq.each do |dst_anchor|
  348. @link_srcs[dst_anchor].reject!{|anchor| anchor == src_anchor}
  349. end
  350. end
  351. store(hash_for_cache(@link_srcs, 'src'))
  352. store(hash_for_cache(@current_dsts, 'dst'))
  353. @vanished_dsts = Hash.new.taint
  354. end
  355. end
  356. # register this plguin to tDiary
  357. unless defined?(Test::Unit)
  358. # language resource and configuration
  359. @my_sequel_plugin_name ||= 'Link to follow ups'
  360. @my_sequel_description ||= <<_END
  361. <p>Shows links to follow-up entries,
  362. which have `my' link to the entry in the past.</p>
  363. <p>Do not forget to push the OK button to store the changes.</p>
  364. _END
  365. @my_sequel_label_conf ||= 'Link label'
  366. @my_sequel_label ||= 'Follow up: '
  367. @my_sequel_restore_default_label ||= 'Restore default'
  368. @my_sequel_default_hash ||= {
  369. :label => {
  370. :title => 'Link label',
  371. :default => 'Follow up: ',
  372. :description => 'Prefix for links to the follow-ups',
  373. :index => 1,
  374. },
  375. :date_format => {
  376. :title => 'Link format',
  377. :default => @date_format,
  378. :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.',
  379. :index => 2,
  380. },
  381. :inner_css => {
  382. :title => 'CSS',
  383. :default => <<'_END',
  384. font-size: 75%;
  385. text-align: right;
  386. margin: 0px;
  387. _END
  388. :description => 'CSS for the links. The followoing is applied to <code>div.sequel</code>.',
  389. :index => 3,
  390. :textarea => {:rows => 5},
  391. },
  392. }
  393. @my_sequel_conf = MySequel::Conf.new(@my_sequel_default_hash)
  394. @my_sequel_conf.merge_hash(@options)
  395. # configuration interface
  396. add_conf_proc( 'my-sequel', @my_sequel_plugin_name ) do
  397. if @mode == 'saveconf' then
  398. @my_sequel_conf.merge_params(@cgi.params)
  399. @my_sequel_conf.to_conf_hash(@conf)
  400. end
  401. <<"_HTML"
  402. #{@my_sequel_conf.handler_block}
  403. <h3>#{@my_sequel_plugin_name}</h3>
  404. #{@my_sequel_description}
  405. #{@my_sequel_conf.html(@my_sequel_restore_default_label, @conf.mobile_agent?).chomp}
  406. _HTML
  407. end
  408. @my_sequel = MySequel.new(@cache_path)
  409. @my_sequel_active = false
  410. # activate this plugin if header procs are called
  411. # - This avoids being called from makerss.rb
  412. add_header_proc do
  413. if not @conf.bot? and not @conf.mobile_agent? then
  414. @my_sequel_active = true
  415. @my_sequel.restore(@diaries.keys)
  416. MySequel.css(@my_sequel_conf[:inner_css])
  417. end
  418. end
  419. # preparation for a day
  420. add_body_enter_proc do |date|
  421. if @my_sequel_active then
  422. if date then
  423. @my_sequel_date = MySequel.date(date)
  424. @my_sequel.clean_dsts(@my_sequel_date)
  425. else
  426. @my_sequel_date = nil
  427. end
  428. end
  429. ''
  430. end
  431. # preparation for a section
  432. add_section_enter_proc do |date, index|
  433. if @my_sequel_active and @my_sequel_date then
  434. @my_sequel_anchor = "#{@my_sequel_date}#p#{'%02d' % index}"
  435. end
  436. ''
  437. end
  438. # plugin function to be called from within sections
  439. alias :my_sequel_orig_my :my unless defined?(my_sequel_orig_my)
  440. def my(*args)
  441. if @my_sequel_active and @my_sequel_date and @my_sequel_anchor and @mode != 'preview' then
  442. dst_date, frag = args[0].scan(/(\d{8,8})(?:[^\d]*)(?:#?p(\d+))?$/)[0]
  443. if dst_date and dst_date < @my_sequel_date then
  444. dst_anchor = "#{dst_date}#{frag ? "#p%02d" % frag.to_i : ''}"
  445. @my_sequel.add(@my_sequel_anchor, dst_anchor)
  446. end
  447. end
  448. my_sequel_orig_my(*args)
  449. end
  450. # show sequels when leaving a section
  451. add_section_leave_proc do
  452. r = ''
  453. if @my_sequel_active and @my_sequel_date and @my_sequel_anchor and not @conf.bot? and not @conf.mobile_agent? then
  454. r = @my_sequel.html(@my_sequel_anchor, @my_sequel_conf[:date_format], @my_sequel_conf[:label]){|src_anchor, anchor_str|
  455. my_sequel_orig_my(src_anchor, anchor_str)
  456. }
  457. end
  458. @my_sequel_anchor = nil
  459. r
  460. end
  461. # show sequels when leaving a day
  462. add_body_leave_proc do
  463. r = ''
  464. if @my_sequel_active and @my_sequel_date then
  465. if not @conf.bot? and not @conf.mobile_agent? then
  466. r = @my_sequel.html(@my_sequel_anchor, @my_sequel_conf[:date_format], @my_sequel_conf[:label]){|src_anchor, anchor_str|
  467. my_sequel_orig_my(src_anchor, anchor_str)
  468. }
  469. end
  470. end
  471. @my_sequel_date = nil
  472. r
  473. end
  474. # commit changes
  475. add_footer_proc do
  476. if @my_sequel_active then
  477. @my_sequel.clean_srcs
  478. @my_sequel.commit
  479. end
  480. ''
  481. end
  482. end
  483. # Local Variables:
  484. # mode: ruby
  485. # indent-tabs-mode: t
  486. # tab-width: 3
  487. # ruby-indent-level: 3
  488. # End: