OSDN Git Service

simply use autotools macro to set version numbers in *.el
[howm/howm.git] / ext / howm2
1 #!/usr/bin/ruby -s
2 # -*- coding: euc-jp -*-
3 # -*- Ruby -*-
4 # $Id: howm2,v 1.10 2010-12-31 12:12:47 hira Exp $
5
6 # Convert ~/howm/ to HTML or other formats.
7 # Only RD format is supported unless you will give me patches. :p
8
9 require 'cgi'
10
11 def usage
12   name = File::basename $0
13   print <<EOU
14 #{name}: howm ¥á¥â¤ò¥Õ¥©¡¼¥Þ¥Ã¥È
15 ¡¦¤ä¤Ð¤¤Ê¸»ú¤ò¥¨¥¹¥±¡¼¥×
16 ¡¦¥­¡¼¥ï¡¼¥É¤ò¥ê¥ó¥¯¤ËÊÑ´¹
17 ¡¦¥Ø¥Ã¥À¤È¥Õ¥Ã¥¿¤ò¤Ä¤±¤ë
18 (Îã)
19   #{name} ~/howm/ ~/converted/
20   #{name} -type=rd ~/howm/ ~/converted/
21   ls ~/howm/*/*/*7-*.txt | #{name} -list ~/converted/
22   grep -rl '¤Û¤²' ~/howm/ | #{name} -list ~/converted/
23 (¥ª¥×¥·¥ç¥óÎã)
24   -type=rd ¤Þ¤¿¤Ï -t=rd ¢ª ¥Õ¥©¡¼¥Þ¥Ã¥È¤Î¼ïÎà¤ò»ØÄê
25     html   ¡Ä ¥Ç¥Õ¥©¥ë¥È
26     rd     ¡Ä see http://www2.pos.to/~tosh/ruby/rdtool/ja/
27     rdbody ¡Ä rd ¤Î =begin ¤È =end ¤¬¤Ê¤¤¤â¤Î¤òÆþÎÏ. ¤³¤ì¤é¤òÊä¤Ã¤ÆÀ¸À®.
28   -list                     ¢ª ¥á¥â¥Õ¥¡¥¤¥ë¤Î¥ê¥¹¥È¤òɸ½àÆþÎϤ«¤éÆɤà
29   -exclude='^[.]|CVS'       ¢ª Âоݳ°¤Î¥Õ¥¡¥¤¥ë¤òÀµµ¬É½¸½¤Ç»ØÄê
30   -r                        ¢ª ¥á¥â°ìÍ÷¤ò¿·¤·¤¤½ç¤Ëʤ٤ë
31   -i                        ¢ª <<< ¤ÇÂçʸ»ú¾®Ê¸»ú¤ò¶èÊ̤·¤Ê¤¤
32   -title='Index'            ¢ª index ¥Ú¡¼¥¸¤ÎÂê̾
33   -silent ¤Þ¤¿¤Ï -s         ¢ª ¿ÊĽɽ¼¨¤ò¤·¤Ê¤¤
34   -goto='>>>'               ¢ª goto link ¤Î½ñ¼°
35   -comefrom='<<<'           ¢ª come-from link ¤Î½ñ¼°
36   -no_alias                 ¢ª come-from ¥­¡¼¥ï¡¼¥É¤Î alias ¤ò̵»ë
37   -help ¤Þ¤¿¤Ï -h           ¢ª ¤³¤Î¥á¥Ã¥»¡¼¥¸¤òɽ¼¨
38   (-debug                   ¢ª ¥Ç¥Ð¥Ã¥°ÍѽÐÎÏ)
39 EOU
40 end
41
42 argv_len = $list ? 1 : 2
43 if ($help || $h || ARGV.length != argv_len)
44   usage
45   exit 0
46 end
47
48 #####################################
49
50 $type ||= $t || 'html'
51 $exclude ||= "^[.\#]|CVS|~$"
52 $silent ||= $s
53 $title ||= 'Index'
54 #$r_text_width = 40
55 $progress = '.'
56 $goto = '>>>'
57 $comefrom = '<<<'
58 $url_regexp = %r!((http|file)://\S+)!
59
60 def come_go_match(str)
61   case str
62   when /#$comefrom|#$goto/
63     s = str
64     r = []
65     while s =~ /((#$comefrom|#$goto) *(.+?)) *($|(#$comefrom|#$goto).*)/
66       raw = $1
67       type = ($2 == $comefrom) ? :comefrom : :goto
68       key = $3
69       s = $4
70       r.push [type, key, raw]
71     end
72     return r
73   else
74     return false
75   end
76 end
77
78 def title_match(str)
79   if str =~ /^= +(.+)$/
80     return $1
81   else
82     return false
83   end
84 end
85
86 #####################################
87
88 def empty(); lambda{|*dummy| ""}; end
89 def constant(str); lambda{|*dummy| str}; end
90 def appender(str); lambda{|x| x + str}; end
91 def no_change(); lambda{|*x| x[0]}; end
92
93 $formatter = Hash::new
94
95 $formatter['html'] = {
96   :escaper => lambda{|str| CGI::escapeHTML str},
97   :unescaper => lambda{|str| CGI::unescapeHTML str},
98   # body page
99   :namer => appender('.b.html'),
100   :header => lambda{|file|
101     %!<HTML><TITLE>#{file}</TITLE><BODY><H1>#{file}</A></H1><HR><PRE>\n!
102   },
103   :come_tag => lambda{|a|
104     # Fix me.
105     %!<A NAME="#{a[:occur][0][:anch]}"></A><A HREF="#{a[:rpath]}\##{a[:anch_n]}" NAME="#{a[:anch]}">#{a[:orig]}</A>!
106   },
107   :come_jump => lambda{|a|
108     %!<A HREF="#{a[:path]}\##{a[:anch]}">#{a[:orig]}</A>!
109   },
110   :come_anchor => lambda{|a|
111     %!<A NAME="#{a[:occur][0][:anch]}"></A>!
112   },
113   :go_tag => lambda{|a|
114     %!<A NAME="#{a[:occur][0][:anch]}"></A><A HREF="#{a[:rpath]}\##{a[:anch]}" NAME="#{a[:anch]}">#{a[:orig]}</A>!
115   },
116   :go_anchor => lambda{|a|
117     %!<A NAME="#{a[:occur][0][:anch]}"></A>!
118 #     %!<A NAME="#{a[:occur][0][:anch]}">#{a[:key]}</A>!
119   },
120   :url => lambda{|a|
121     %!<A HREF="#{a[:url]}">#{CGI::unescapeHTML a[:url]}</A>!
122   },
123   :footer => lambda{|file|
124     %!</PRE><HR><A HREF="#{to_index file}">index</A></BODY></HTML>\n!
125   },
126   # reference page
127   :ref_namer => appender('.r.html'),
128   :ref_header => lambda{|file|
129     "<HTML><TITLE>#{file}</TITLE><BODY><H1>References: #{file}</H1>\n"
130   },
131   :ref_itemer => lambda{|a|
132     go = a[:goto_file]
133     url = go ? "file://#{a[:goto_file]}" : "#{a[:path]}\##{a[:anch]}"
134     ocs = a[:occur]
135     %!<A HREF="#{url}" NAME="#{a[:anch]}"><H2>#{a[:key]} (#{ocs.length})</H2></A>\n<OL>\n! +
136     ocs.map{|oc|
137       %!<LI><A HREF="#{oc[:path]}\##{oc[:anch]}">#{oc[:file]}</A> #{oc[:text]}\n!
138     }.join +
139     "</OL>\n"
140   },
141   :ref_footer => constant("</BODY></HTML>\n"),
142   # index page
143   :index_namer => constant('index.html'),
144   :index_header => constant("<HTML><TITLE>#{$title}</TITLE><BODY><H1>#{$title}</H1>\n"),
145   :index_keyworder => lambda{|as|
146     "<H2>Keywords (#{as.length})</H2>\n" +
147     as.map{|a| %!<A HREF="#{a[:dest]}\##{a[:anch]}">#{a[:key]}</A>!}.join(" /\n") +
148     "\n"
149   },
150   :index_filer => lambda{|as|
151     "<H2>Files (#{as.length})</H2>\n<OL>\n" +
152     as.map{|a| %!<LI><A HREF="#{a[:dest]}">#{a[:file]}</A>: #{a[:title]}\n!}.join +
153     "</OL>\n"
154   },
155   :index_footer => constant("</BODY></HTML>\n"),
156 }
157
158 $formatter['rd'] = {  # RD doesn't have anchor?
159   :escaper => no_change,
160   :unescaper => no_change,
161   # body page
162   :namer => appender('.b.rd'),
163   :header => empty,
164   :come_tag => lambda{|a| %!((<"#{a[:orig]}"|URL:#{a[:rpath]}>))!},
165   :come_jump => lambda{|a| %!((<"#{a[:orig]}"|URL:#{a[:path]}>))!},
166   :come_anchor => constant(''),
167   :footer => empty,
168   :go_tag => lambda{|a| %!((<"#{a[:orig]}"|URL:#{a[:rpath]}>))!},
169   :go_anchor => constant(''),
170   :url => lambda{|a| %!((<"#{a[:url]}"|URL:#{a[:url]}>))!},
171   # reference page
172   :ref_namer => appender('.r.rd'),
173   :ref_header => lambda{|file| "=begin\n= References: #{file}\n"},
174   :ref_itemer => lambda{|a|
175     go = a[:goto_file]
176     url = go ? "file://#{go}" : "#{a[:path]}"
177     %!== ((<"#{a[:key]}"|URL:#{url}>))\n! +
178     a[:occur].map{|oc|
179       %!* ((<"#{oc[:file]}"|URL:#{oc[:path]}>)) #{oc[:text]}\n!
180     }.join +
181     "\n"
182   },
183   :ref_footer => constant("=end\n"),
184   # index page
185   :index_namer => constant('index.rd'),
186   :index_header => constant("=begin\n= #{$title}\n"),
187   :index_keyworder => lambda{|as|
188     "== Keywords (#{as.length})\n" +
189     as.map{|a| %!((<"#{a[:key]}"|URL:#{a[:dest]}>))!}.join(" /\n") +
190     "\n"
191   },
192   :index_filer => lambda{|as|
193     "== Files (#{as.length})\n" +
194     as.map{|a| %!* ((<"#{a[:file]}"|URL:#{a[:dest]}>)): #{a[:title]}\n!}.join
195   },
196   :index_footer => constant("=end\n"),
197 }
198
199 b = $formatter['rd'].dup
200 b[:header] = constant "=begin\n"
201 b[:footer] = constant "=end\n"
202 $formatter['rdbody'] = b
203
204 #####################################
205
206 class String
207   def indices(substr)
208     a = Array::new
209     pos = 0
210     while (i = index substr, pos)
211       a.push(substr.is_a?(Regexp) ? [i, $&] : i)
212       pos = i + 1
213     end
214     return a
215   end
216 end
217
218 class HashList < Hash
219   def cons(key, val)
220     self[key] ||= Array::new
221     self[key].push val
222   end
223 end
224
225 def ls_R(dir)
226   a = Array::new
227   Dir::open(dir){|d| d.each{|f| a.push f}}  # map doesn't work??
228   b = Array::new
229   a.each{|f|
230     next if f =~ /#$exclude/
231     path = File::expand_path f, dir
232     b.push f if FileTest::file? path
233     b += ls_R(path).map{|g| "#{f}/#{g}"} if FileTest::directory? path
234   }
235   return b
236 end
237
238 # FixMe :-(
239 def bundle(file_list)
240   fs = file_list.map{|f| File::expand_path f}
241   ds = fs.map{|f| File::dirname f}
242   common = ds[0] || ''
243   ds.each{|d|
244     while common != d
245       if common.length <= d.length
246         d = File::dirname d
247       else
248         common = File::dirname common
249       end
250     end
251   }
252   rs = fs.map{|f| f[(common.length + 1)..-1]}  # +1 for '/'
253   return [common, rs]
254 end
255
256 # Fixme :-(
257 def mkdir_p(path)
258   parent = File::dirname path
259   return true if parent == path  # root dir
260   mkdir_p parent
261   if !FileTest::exist? path
262     Dir::mkdir path
263   end
264 end
265
266 # Fixme :-(
267 def relative_path(target, origin)
268   return target if origin == '.'
269   sep = '/'
270   parent = '..'
271   root = origin.split(sep).map{|any| parent}.join(sep)
272   return root + sep + target
273 end
274
275 def to_index(origin)
276   relative_path $formatter[$type][:index_namer].call, File::dirname(origin)
277 end
278
279 $unique_id = ':000000'
280 def unique_name(base)
281   base + $unique_id.succ!.dup
282 end
283
284 #####################################
285
286 $titles_in_file = HashList::new  # dirty!
287 def come_go_master(files, dir, formatter)
288   h = HashList::new  # key => master files
289   aliases = []
290   files.each{|f|
291     open(File::expand_path(f, dir)){|io|
292       io.each_line{|line| 
293         if (t = title_match line)
294           $titles_in_file.cons f, t
295         end
296         if (found = come_go_match line)
297           equiv_key = []
298           equiv_raw = []
299           found.each{|m|
300             type, key, raw = m
301             s = formatter[:escaper].call raw
302             k = formatter[:escaper].call key
303             g = formatter[:namer].call f
304             r = formatter[:ref_namer].call f
305             a = CGI::escape(k)
306             arg = {
307               :type => type,
308               :raw => s,
309               :key => k,
310               :occur => Array::new,
311               :file => f,
312               :dest => g,
313               :ref => r,
314               :anch => a,
315             }
316             case type
317             when :comefrom
318               h.cons s, [:come_tag, arg]
319               h.cons k, [:come_jump, arg]
320               h.cons k, [:come_anchor, arg]
321               equiv_key.push k
322               equiv_raw.push s
323             when :goto
324               h.cons s, [:go_tag, arg]
325               h.cons k, [:go_anchor, arg]
326             end
327           }
328            if equiv_key.length > 1
329              aliases += [equiv_key, equiv_raw]
330            end
331         end
332       }
333     }
334   }
335   return h, aliases
336 end
337
338 def format_line(line, prog)
339   match = HashList::new  # pos => key
340   prog.each{|rule|
341     regexp, func, greedy = [:regexp, :func, :greedy].map{|k| rule[k]}
342     line.indices(regexp).each{|r|
343       i, k = r
344       match.cons i, [k, func, greedy]
345     }
346   }
347   p match if $debug
348   cursor = 0
349   done = ""
350   match.keys.sort.each{|i|
351     skipping = (i < cursor)
352     if !skipping
353       done += line[cursor..(i - 1)] if i > 0 # 'foobar'[0..-1] is 'foobar'
354       cursor = i
355     end
356     match[i].each{|com|
357       key, func, greedy = com
358       next if greedy && skipping
359       done += func.call(key, line)
360       if greedy
361         cursor = i + key.length
362         break
363       end
364     }
365   }
366   if (cursor <= (len = line.length))
367     done += line[cursor..len]
368   end
369   return done
370 end
371
372 def format_io(input, output, prog_src, compiler, escaper)
373   a = input.readlines.map{|s| escaper.call s}
374   whole = a.join
375   matched_rules = prog_src.select{|rule|
376     whole =~ rule[:regexp]
377   }
378   prog = matched_rules.map{|r| compiler.call r}
379   a.each{|line|
380     output.print format_line(line, prog)
381   }
382 end
383
384 #####################################
385
386 def notice(str)
387   STDERR.print str if !$silent
388 end
389
390 if $list
391   dest_dir = ARGV.shift
392   src_dir, files = bundle(STDIN.readlines.map{|s| s.chomp})
393 else
394   src_dir, dest_dir = ARGV
395   files = ls_R src_dir
396 end
397 notice "#{files.length} files "
398 fmt = $formatter[$type]
399 k2m, aliases = come_go_master files, src_dir, fmt
400 aliases = [] if $no_alias
401 notice "(#{k2m.length} entries)\n"
402 p k2m if $debug
403
404 aliases.each{|equiv|
405   key0 = equiv.shift
406   type0, arg0 = k2m[key0][0]
407   equiv.each{|key|
408     k2m[key].each{|m|
409       type, arg = m
410       [:occur, :file, :dest, :ref].each{|x|
411         arg[x] = arg0[x]
412       }
413       arg[:anch_alias] = arg0[:anch] if type == :come_tag
414     }
415   }
416 }
417
418 notice 'body pages: '
419 greedy = Array::new
420 nongreedy = Array::new
421 k2m.each_pair{|k, v|
422   v.each{|m|
423     type, arg = m
424     r = /#{Regexp::escape k}/
425     g = [:come_tag, :come_jump, :go_tag].member?(type)
426     z = g ? greedy : nongreedy
427     h = {:raw => k, :regexp => r, :type => type, :arg => arg, :greedy => g}
428     z.push h
429   }
430 }
431 greedy.sort!{|x, y| x[:raw].length <=> y[:raw].length}
432 greedy.reverse!
433 p greedy if $debug
434 p nongreedy if $debug
435 u = {:regexp => $url_regexp, :type => :url, :arg => Hash::new, :greedy => true}
436 prog_src = nongreedy + [u] + greedy
437 files.each{|f|
438   notice $progress
439   g = fmt[:namer].call f
440   r = fmt[:ref_namer].call f
441   spath = File::expand_path f, src_dir
442   dpath, rpath = [g, r].map{|x| File::expand_path x, dest_dir}
443   mkdir_p File::dirname(dpath)
444   compiler = lambda{|h|
445     func = lambda{|k, s|
446       type = h[:type]
447       arg = h[:arg]
448       if type == :url
449         arg[:url] = k
450       else
451         dir = File::dirname(f)
452         arg[:path] = relative_path arg[:dest], dir
453         arg[:rpath] = relative_path arg[:ref], dir
454         a = unique_name arg[:anch]
455         path = relative_path(g, dir)
456         occur = {
457           :file => f, :path => path, :text => s.chop,
458           :anch => a, :type => type,
459         }
460         arg[:occur].unshift occur
461         arg[:orig] = k
462         arg[:anch_n] = arg[:anch_alias] || arg[:anch]
463       end
464       fmt[type].call arg
465     }
466     reg = h[:regexp]
467     ignore_case = [:come_tag, :come_jump, :come_anchor].member?(h[:type]) && $i
468     reg = /#{reg.source}/i if ignore_case
469     {:regexp => reg, :func => func, :greedy => h[:greedy]}
470   }
471   open(spath){|input|
472     open(dpath, 'w'){|output|
473       output.print fmt[:header].call(f)
474       format_io input, output, prog_src, compiler, fmt[:escaper]
475       output.print fmt[:footer].call(f)
476     }
477   }
478 }
479 notice "\n"
480
481 notice 'reference pages: '
482 m2a = HashList::new
483 k2m.each_pair{|k, v|
484   v.each{|z|
485     type, arg = z
486     next if arg[:anch_alias]
487     m2a.cons arg[:file], arg if [:come_anchor, :go_anchor].member? type
488   }
489 }
490 m2a.each_pair{|f, v|
491   notice $progress
492   body = fmt[:namer].call f
493   ref = fmt[:ref_namer].call f
494   rpath = File::expand_path ref, dest_dir
495   mkdir_p File::dirname(rpath)
496   open(rpath, 'w'){|output|
497     output.print fmt[:ref_header].call(f)
498     v.each{|arg|
499       g = fmt[:unescaper].call arg[:key]
500       arg[:goto_file] = g if arg[:type] == :goto && FileTest::exist?(g)
501       arg[:occur].reject!{|oc| ![:come_anchor, :go_anchor].member? oc[:type]}
502       arg[:occur].sort!{|a,b| - (a[:file] <=> b[:file])}
503       output.print fmt[:ref_itemer].call(arg)
504     }
505     output.print fmt[:ref_footer].call(f)
506   }
507 }
508 notice "\n"
509
510 notice 'index page: '
511 path = File::expand_path fmt[:index_namer].call(), dest_dir
512 open(path, 'w'){|output|
513   output.print fmt[:index_header].call
514   output.print fmt[:index_keyworder].call(k2m.keys.sort.map{|k|
515     k2m[k].map{|m|
516       type, arg = m
517       [:come_anchor].member?(type) ? arg : nil
518     }.select{|a| a}
519   }.flatten)
520   # alphabet files precede numerical files
521   z = files.sort{|f, g|
522     a, b = [f, g].map{|h| (h =~ /^[0-9]/ ? 'z' : 'a') + h}
523     a <=> b
524   }
525   z.reverse! if $r
526   output.print fmt[:index_filer].call(z.map{|f|
527     g = fmt[:namer].call f
528 #    ts = $titles_in_file[f].reject{|t| t =~ /^\s*$/} || []
529     ts = $titles_in_file[f] || []
530     {:file => f, :dest => g, :title => ts.join(' / ')}
531   })
532   output.print fmt[:index_footer].call
533 }
534 notice ".\n"