html 作成に必要なファイルを追加
@@ -0,0 +1,515 @@ | ||
1 | +#!/usr/bin/ruby | |
2 | +# Doxygen で出力したページを SourceForge 用に調整するツール | |
3 | +# 併せて、RSS, 更新履歴の生成も行う | |
4 | +# Satofumi KAMIMURA | |
5 | +# $Id$ | |
6 | +# | |
7 | +# \todo RSS のリンクに改行が入るのを修正する | |
8 | + | |
9 | +require 'find' | |
10 | +require 'kconv' | |
11 | +require "rss" | |
12 | +$KCODE = 'SJIS' | |
13 | + | |
14 | + | |
15 | +# RSS 用の項目データ | |
16 | +$articles = Array.new | |
17 | + | |
18 | + | |
19 | +# 設定ファイルがないときの処理 | |
20 | +if ARGV.size < 1 | |
21 | + print "usage:\n\t" + __FILE__ + " <config file>\n\n" | |
22 | + exit(1) | |
23 | +end | |
24 | +config_file = ARGV[0] | |
25 | + | |
26 | +# 更新履歴のフレーム幅 | |
27 | +HistoryWidth = 250 | |
28 | + | |
29 | +# 設定ファイルの内容読み出し | |
30 | +class ConfigInformation | |
31 | + attr_reader :index_page, :history_max, :replace_words, :target_page, :strip_path, :history_x, :history_y, :line_size_max, :log_file, :rss_10_file, :rss_20_file, :rss_base, :rss_title, :rss_description, :rss_insert, :find_dox_directory | |
32 | + | |
33 | + def loadReplaceWords(io) | |
34 | + while line = io.gets | |
35 | + case line.chomp | |
36 | + when /^\s*$/ | |
37 | + # 空行までを読み出す | |
38 | + return | |
39 | + | |
40 | + when /^(.+?)\s(.+)$/ | |
41 | + @replace_words[$1] = $2 | |
42 | + end | |
43 | + end | |
44 | + end | |
45 | + | |
46 | + def initialize(file) | |
47 | + @target_page = nil | |
48 | + @index_page = nil | |
49 | + @replace_words = Hash.new | |
50 | + @strip_path = '' | |
51 | + @line_size_max = 20 | |
52 | + @log_file = 'history_log.txt' | |
53 | + @rss_10_file = nil | |
54 | + @rss_20_file = nil | |
55 | + @rss_base = 'rss_base_address' | |
56 | + @rss_title = 'rss title' | |
57 | + @rss_description = 'rss description' | |
58 | + @rss_insert = false | |
59 | + @find_dox_directory = "./" | |
60 | + | |
61 | + File.open(file) { |io| | |
62 | + while line = io.gets | |
63 | + case line.chomp | |
64 | + when /TargetHtml\s(.+)/ | |
65 | + @target_page = $1.split | |
66 | + | |
67 | + when /IndexHtml\s(.+)/ | |
68 | + @index_page = $1 | |
69 | + | |
70 | + when /StripFromExamplesPath\s(.+)/ | |
71 | + @strip_path = $1 | |
72 | + | |
73 | + when /IndexHistoryMax\s(\d+)/ | |
74 | + @history_max = $1.to_i | |
75 | + | |
76 | + when /IndexPosition\s(\d+)x(\d+)/ | |
77 | + @history_x = $1.to_i | |
78 | + @history_y = $2.to_i | |
79 | + | |
80 | + when /ReplaceList/ | |
81 | + loadReplaceWords(io) | |
82 | + | |
83 | + when /HistoryLineMax\s(\d+)/ | |
84 | + @line_size_max = $1.to_i | |
85 | + | |
86 | + when /HistoryLogFile\s(.+)/ | |
87 | + @log_file = $1 | |
88 | + | |
89 | + when /OutputRss10\s(.+)/ | |
90 | + @rss_10_file = $1 | |
91 | + | |
92 | + when /OutputRss20\s(.+)/ | |
93 | + @rss_20_file = $1 | |
94 | + | |
95 | + when /RssBaseUrl\s(.+)/ | |
96 | + @rss_base = $1 | |
97 | + | |
98 | + when /RssTitle\s(.+)/ | |
99 | + @rss_title = Kconv.kconv($1, Kconv::UTF8, Kconv::SJIS) | |
100 | + | |
101 | + when /RssDescription\s(.+)/ | |
102 | + @rss_description = Kconv.kconv($1, Kconv::UTF8, Kconv::SJIS) | |
103 | + | |
104 | + when /InsertRss\s[Yy][Ee][Ss]/ | |
105 | + @rss_insert = true | |
106 | + | |
107 | + when /FindDoxDirectory\s(.+)/ | |
108 | + @find_dox_directory = $1 | |
109 | + end | |
110 | + end | |
111 | + } | |
112 | + @target_page = (@target_page == nil) ? Array.new : @target_page | |
113 | + @index_page = (@index_page == nil) ? '' : @index_page | |
114 | + end | |
115 | +end | |
116 | +configs = ConfigInformation.new(config_file) | |
117 | + | |
118 | + | |
119 | +# ファイルの置換 | |
120 | +def updatefile(page, lines) | |
121 | + File.open(page, 'w') { |io| | |
122 | + io.write(lines) | |
123 | + } | |
124 | +end | |
125 | + | |
126 | + | |
127 | +# 文字毎のエンコード | |
128 | +def hexEncode(line, type) | |
129 | + | |
130 | + encoded = '' | |
131 | + line.each_byte { |ch| | |
132 | + if ch.chr =~ /[a-zA-Z:]/ | |
133 | + case type | |
134 | + when 0 | |
135 | + encoded += '&#' + ch.to_s(10) + ';' | |
136 | + | |
137 | + when 1 | |
138 | + encoded += '%' + ch.to_s(16) | |
139 | + | |
140 | + when 2 | |
141 | + encoded += '&#x' + ch.to_s(16) + ';' | |
142 | + end | |
143 | + else | |
144 | + encoded += ch.chr | |
145 | + end | |
146 | + } | |
147 | + encoded | |
148 | +end | |
149 | + | |
150 | + | |
151 | +# メールアドレスのエンコード | |
152 | +def encodeMailAddress(line) | |
153 | + | |
154 | + if line =~ /(mailto:)(.+)">(.+)/ | |
155 | + hexEncode($1, 0) + hexEncode($2, 1) + '">' + hexEncode($3, 2) | |
156 | + #$1 + hexEncode($2, 1) + '" >' + hexEncode($3, 2) | |
157 | + else | |
158 | + line | |
159 | + end | |
160 | +end | |
161 | + | |
162 | + | |
163 | +# ページ毎に置換処理を行う | |
164 | +configs.target_page.each { |page| | |
165 | + updated = false | |
166 | + | |
167 | + # ファイルの読み出し | |
168 | + lines = File.read(page) | |
169 | + | |
170 | + # メールアドレスの置換 | |
171 | + match_pattern = /<a href="(mailto:[^%].+)<\/a>/ | |
172 | + while lines.match(match_pattern) | |
173 | + replace = '<a href="' + encodeMailAddress($1) + '</a>' | |
174 | + lines.sub!(match_pattern, replace) | |
175 | + updated = true | |
176 | + end | |
177 | + | |
178 | + # 任意タグの置換 | |
179 | + configs.replace_words.each { |key, words| | |
180 | + if lines.match(key) | |
181 | + lines.gsub!(key, words) | |
182 | + updated = true | |
183 | + end | |
184 | + } | |
185 | + | |
186 | + # ファイルの置換 | |
187 | + if updated | |
188 | + updatefile(page, lines); | |
189 | + print 'update "' + page + "\"\n" | |
190 | + end | |
191 | +} | |
192 | + | |
193 | + | |
194 | +# ページ情報の処理クラス | |
195 | +class PageInformation | |
196 | + attr_reader :name, :mtime | |
197 | + | |
198 | + def initialize(fname) | |
199 | + @name = fname | |
200 | + @mtime = File.mtime(fname).tv_sec | |
201 | + end | |
202 | +end | |
203 | + | |
204 | + | |
205 | +# RSS ファイルの生成 | |
206 | +def createRss(type, fname, rss_settings) | |
207 | + | |
208 | + rss = RSS::Maker.make(type) do |maker| | |
209 | + # !!! ひどいな...。fname, basename の扱いを確認すべき | |
210 | + maker.channel.about = rss_settings['base'] + File.basename(fname) | |
211 | + maker.channel.title = rss_settings['title'] | |
212 | + maker.channel.description = rss_settings['description'] | |
213 | + maker.channel.link = rss_settings['base'] | |
214 | + | |
215 | + maker.items.do_sort = true | |
216 | + | |
217 | + items_num = 0; | |
218 | + $articles.each { |article| | |
219 | + maker.items.new_item do |item| | |
220 | + # !!! article を代入するように変更する | |
221 | + item.link = rss_settings['base'] + article['link'] | |
222 | + item.title = article['title'] | |
223 | + item.date = article['date'] | |
224 | + item.description = article['description'] | |
225 | + end | |
226 | + | |
227 | + items_num += 1 | |
228 | + if items_num >= 15 | |
229 | + break | |
230 | + end | |
231 | + } | |
232 | + end | |
233 | + | |
234 | + File.open(fname, 'w') { |io| | |
235 | + io << rss.to_s | |
236 | + } | |
237 | +end | |
238 | + | |
239 | +# 更新履歴メモの作成 | |
240 | +def insertUpdateMemo(logfile, line_size_max) | |
241 | + | |
242 | + if !File.exist?(logfile) | |
243 | + return ''; | |
244 | + end | |
245 | + | |
246 | + out = '' | |
247 | + File.open(logfile) { |io| | |
248 | + # パースフォーマット | |
249 | + # -----+----- | |
250 | + # リンク | |
251 | + # タイトル | |
252 | + # 詳細 | |
253 | + # "\n" | |
254 | + | |
255 | + # !!! 実装、適当すぎだ! | |
256 | + last_date = Time.now | |
257 | + parse_index = 0 | |
258 | + link = nil | |
259 | + title = nil | |
260 | + messages = '' | |
261 | + while line = io.gets | |
262 | + case line | |
263 | + when /---+---/ | |
264 | + out += '<hr align="left" size="1" width="90%">' | |
265 | + | |
266 | + when /^(\d\d\d\d)[\/-](\d\d)[\/-](\d\d)$/ | |
267 | + last_date = Time.local($1, $2, $3) | |
268 | + if out != '' | |
269 | + out += '<br>' | |
270 | + end | |
271 | + out += $1 + '-' + $2 + '-' + $3 | |
272 | + else | |
273 | + | |
274 | + case parse_index | |
275 | + when 0 | |
276 | + # リンク | |
277 | + link = line.chomp | |
278 | + parse_index += 1 | |
279 | + | |
280 | + when 1 | |
281 | + # タイトル | |
282 | + title = Kconv.kconv(line, Kconv::UTF8, Kconv::SJIS).chomp | |
283 | + parse_index += 1 | |
284 | + | |
285 | + else | |
286 | + # 詳細 | |
287 | + messages += Kconv.kconv(line, Kconv::UTF8, Kconv::SJIS).chomp | |
288 | + end | |
289 | + | |
290 | + # !!! 現状は、詳細が改行で終わっているときは、処理されない | |
291 | + if line.chomp == '' | |
292 | + # 項目のリンク作成 | |
293 | + if link && title | |
294 | + if messages.chomp == '' | |
295 | + messages = Kconv.kconv('説明なし', Kconv::UTF8, Kconv::SJIS) | |
296 | + end | |
297 | + out += '- <a class="el" href="' + link + '">' + title + '</a><br>' | |
298 | + out += '<div class="log_space">' + messages + '</div><br>' | |
299 | + | |
300 | + # !!! ここでグローバル変数を使うんかい! | |
301 | + date = last_date | |
302 | + $articles << { 'link' => link, 'title' => title, 'date' => date, 'description' => messages } | |
303 | + end | |
304 | + | |
305 | + parse_index = 0 | |
306 | + link = nil | |
307 | + title = nil | |
308 | + messages = '' | |
309 | + end | |
310 | + end | |
311 | + end | |
312 | + } | |
313 | + | |
314 | + '<table class="main" border="0" cellpadding="0" cellspacing="0" width="100%">' + "\n" + '<tbody><tr><td><strong>' + Kconv.kconv('更新履歴', Kconv::UTF8, Kconv::SJIS) + '</strong><div class="log_frame">' + out + '<br><br><br><br><br><br><br><br><br><br><br><br> </td></tr></tbody></table>' | |
315 | +end | |
316 | + | |
317 | + | |
318 | +# 更新ページ履歴の作成 | |
319 | +def insertUpdateHistory(history_max, find_path) | |
320 | + | |
321 | + # 対象ページの検索を行う | |
322 | + files = Array.new | |
323 | + Find.find(find_path) { |name| | |
324 | + Find.prune if (name =~ /.svn/ || name =~ /output_html/ || name =~ /~$/) | |
325 | + if ! File.file?(name) | |
326 | + next | |
327 | + end | |
328 | + | |
329 | + files.push(PageInformation.new(name)) | |
330 | + } | |
331 | + files.sort! { |a, b| a.mtime <=> b.mtime } | |
332 | + files.reverse! | |
333 | + | |
334 | + # page 情報を新しい順に処理する | |
335 | + pages_counter = 0 | |
336 | + update_days = Array.new | |
337 | + files.each { |info| | |
338 | + # ファイル毎に \page の情報を取得する | |
339 | + lines = File.read(info.name); | |
340 | + lines.each_line { |line| | |
341 | + if line =~ /^\s+\\page\s+(.+?)\s+(.+)/ | |
342 | + link = $1 | |
343 | + #title = Kconv.kconv($2, Kconv::UTF8, Kconv::SJIS) | |
344 | + title = $2 | |
345 | + pages_counter += 1 | |
346 | + | |
347 | + # 更新履歴のタグを作る | |
348 | + date = Time.at(info.mtime).strftime("%Y-%m-%d") | |
349 | + href = '<a class=el href="' + link + '.html">' + title + '</a>' | |
350 | + update_days.push([date, href]) | |
351 | + end | |
352 | + } | |
353 | + break if pages_counter >= history_max | |
354 | + } | |
355 | + | |
356 | + # 整形して返す | |
357 | + last_date = '' | |
358 | + output = '' | |
359 | + update_days.each { |date, href| | |
360 | + if last_date != date | |
361 | + if output != '' | |
362 | + output += '<br>'; | |
363 | + end | |
364 | + output += date + '<hr align="left" size="1" width="90%">' | |
365 | + last_date = date | |
366 | + end | |
367 | + output += ' - ' + href + '<br>' | |
368 | + } | |
369 | + | |
370 | + return '<strong>' + Kconv.kconv('最近変更されたページ', Kconv::UTF8, Kconv::SJIS) + '</strong><br><div class="history_frame">' + output + '</div>' | |
371 | +end | |
372 | + | |
373 | + | |
374 | +# トップページに更新履歴を追加する | |
375 | +configs.index_page.each { |page| | |
376 | + lines = File.read(page) | |
377 | + | |
378 | + # 本文中に "TWOCOLUMN" があれば、それを検出するようにする | |
379 | + # !!! | |
380 | + | |
381 | + first_detected = false | |
382 | + replace_lines = '' | |
383 | + lines.each_line { |line| | |
384 | + # 本文中に、"TWOCOLUMN" があれば、それを2段組の開始位置とする | |
385 | + # Doxygen 処理後は、"<twocolumn><ul>" になっている | |
386 | + # !!! あると仮定して実装する | |
387 | + if (! first_detected) && (line =~ /TWOCOLUMN/) | |
388 | + first_detected = true | |
389 | + replace_lines += "<table width=\"100%\"><tbody><tr valign=\"top\"><td valign=\"top\" width=\"60%\">\n" | |
390 | + line = ''; | |
391 | + end | |
392 | + | |
393 | + # TWOCOLUMN_END までの行を生成ページの末尾と判断する | |
394 | + if line =~ /TWOCOLUMN_END/ && first_detected | |
395 | + line.sub!(/TWOCOLUMN_END/, '') | |
396 | + replace_lines += "</td><td valign=\"top\" width=\"40%\">\n" | |
397 | + | |
398 | + # 更新メッセージの追加 | |
399 | + replace_lines += insertUpdateMemo(configs.log_file, configs.line_size_max) + "\n" | |
400 | + | |
401 | + # ページ更新履歴の追加 | |
402 | + if configs.history_max > 0 | |
403 | + # 項目を表示する可能性があるならば、表示させる | |
404 | + replace_lines += '<br><br>' | |
405 | + replace_lines += insertUpdateHistory(configs.history_max, configs.find_dox_directory) + "\n" | |
406 | + end | |
407 | + replace_lines += "</td></tr></tbody></table>\n" | |
408 | + end | |
409 | + replace_lines += line | |
410 | + | |
411 | + # 指定がなければ、最初の ^</div> を、2段組の開始位置と判断する | |
412 | + #if (! first_detected) && (line =~ /^<\/div>/) | |
413 | + #first_detected = true | |
414 | + #replace_lines += "<table width=100%><tr><td width=60%>\n" | |
415 | + #end | |
416 | + } | |
417 | + | |
418 | + # TWOCOLUMN でない場合に、RSS を追加するために、insertUpdateMemo を呼び出す | |
419 | + # !!! ひどすぎる... | |
420 | + if ! first_detected | |
421 | + insertUpdateMemo(configs.log_file, configs.line_size_max) | |
422 | + end | |
423 | + | |
424 | + # !!! つか、実装が適当すぎだろ! | |
425 | + # 指定があれば、RSS ファイルを出力する | |
426 | + rss_settings = { 'base' => configs.rss_base, 'title' => configs.rss_title, 'description' => configs.rss_description } | |
427 | + | |
428 | + if configs.rss_10_file | |
429 | + createRss('1.0', configs.rss_10_file, rss_settings) | |
430 | + if configs.rss_insert | |
431 | + replace_lines.sub!(/<\/title>\n/, "</title>\n" + '<link rel="alternate" type="application/rss+xml" title="RSS 1.0" href="' + rss_settings['base'] + File.basename(configs.rss_10_file) + '">' + "\n") | |
432 | + end | |
433 | + end | |
434 | + if configs.rss_20_file | |
435 | + createRss('2.0', configs.rss_20_file, rss_settings) | |
436 | + if configs.rss_insert | |
437 | + replace_lines.sub!(/<\/title>\n/, "</title>\n" + '<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="' + rss_settings['base'] + File.basename(configs.rss_20_file) + '">' + "\n") | |
438 | + end | |
439 | + end | |
440 | + | |
441 | + # ファイルの置換 | |
442 | + File.open(page, 'w') { |io| | |
443 | + io.write(replace_lines) | |
444 | + } | |
445 | +} | |
446 | + | |
447 | + | |
448 | +# Examples から、共通 PATH を取り除く | |
449 | +# !!! 要、エラー処理。場所の指定がなかった場合について | |
450 | +examples_file = File.dirname(configs.index_page) + '/examples.html' | |
451 | +begin | |
452 | + lines = File.read(examples_file); | |
453 | +rescue => exc | |
454 | +ensure | |
455 | + lines = '' | |
456 | +end | |
457 | + | |
458 | +match_pattern = '">' + configs.strip_path | |
459 | +update = false | |
460 | +if lines.match(match_pattern) | |
461 | + replace = '">' | |
462 | + lines.gsub!(match_pattern, replace) | |
463 | + updated = true | |
464 | +end | |
465 | +# ファイルの更新 | |
466 | +if updated | |
467 | + updatefile(examples_file, lines); | |
468 | + print 'update "' + examples_file + "\"\n" | |
469 | +end | |
470 | + | |
471 | +# *-examples.html から、共通パスを取り除く | |
472 | +# 対象ページの検索を行う | |
473 | +match_pattern = configs.strip_path | |
474 | +Find.find('./doxygen_html/') { |name| | |
475 | + if name =~ /.+\.html$/ | |
476 | + # 置換処理 | |
477 | + lines = File.read(name) | |
478 | + # !!! 改行を読み飛ばす正規表現に置き換える | |
479 | + while lines.match(match_pattern) | |
480 | + lines.sub!(match_pattern, '') | |
481 | + updated = true | |
482 | + end | |
483 | + | |
484 | + # ファイルの更新 | |
485 | + if updated | |
486 | + updatefile(name, lines); | |
487 | + print 'update "' + name + "\"\n" | |
488 | + end | |
489 | + end | |
490 | +} | |
491 | + | |
492 | + | |
493 | +# doxygen.css に設定を追加する | |
494 | +css_file = File.dirname(configs.index_page) + '/doxygen.css' | |
495 | +File.open(css_file, 'a') { |io| | |
496 | +io.print <<-"EOB" | |
497 | +.history_frame { | |
498 | + clear:both; | |
499 | + line-height:19px; | |
500 | + padding:4px; | |
501 | + border:solid 2px MediumSlateBlue; | |
502 | +} | |
503 | + | |
504 | +.log_frame { | |
505 | + height:250px; | |
506 | + overflow:auto; | |
507 | + border:solid 2px MediumSlateBlue; | |
508 | + padding:4px; | |
509 | +} | |
510 | + | |
511 | +.log_space { | |
512 | + margin-left: 18px; | |
513 | +} | |
514 | +EOB | |
515 | +} |