review-retrovert 0.3.3 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/retrovert.yml +39 -0
  3. data/.gitignore +2 -2
  4. data/.ruby-version +1 -0
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +67 -1
  7. data/README.md +6 -3
  8. data/lib/review/retrovert/converter.rb +231 -6
  9. data/lib/review/retrovert/version.rb +1 -1
  10. data/review-retrovert.gemspec +1 -1
  11. data/testdata/mybook/.gitignore +7 -0
  12. data/testdata/mybook/README.md +43 -0
  13. data/testdata/mybook/Rakefile +16 -0
  14. data/testdata/mybook/catalog.yml +33 -0
  15. data/testdata/mybook/config-starter.yml +139 -0
  16. data/testdata/mybook/config.yml +375 -0
  17. data/testdata/mybook/contents/00-preface.re +83 -0
  18. data/testdata/mybook/contents/01-install.re +272 -0
  19. data/testdata/mybook/contents/02-tutorial.re +1008 -0
  20. data/testdata/mybook/contents/03-syntax.re +2613 -0
  21. data/testdata/mybook/contents/04-customize.re +728 -0
  22. data/testdata/mybook/contents/05-faq.re +328 -0
  23. data/testdata/mybook/contents/06-bestpractice.re +971 -0
  24. data/testdata/mybook/contents/91-compare.re +18 -0
  25. data/testdata/mybook/contents/92-filelist.re +119 -0
  26. data/testdata/mybook/contents/93-background.re +267 -0
  27. data/testdata/mybook/contents/99-postface.re +38 -0
  28. data/testdata/mybook/css/normalize.css +349 -0
  29. data/testdata/mybook/css/webstyle.css +514 -0
  30. data/testdata/mybook/images/03-syntax/favicon-16x16.png +0 -0
  31. data/testdata/mybook/images/03-syntax/figure_heretop.png +0 -0
  32. data/testdata/mybook/images/03-syntax/tw-icon1.jpg +0 -0
  33. data/testdata/mybook/images/03-syntax/tw-icon2.jpg +0 -0
  34. data/testdata/mybook/images/03-syntax/tw-icon3.jpg +0 -0
  35. data/testdata/mybook/images/03-syntax/tw-icon4.jpg +0 -0
  36. data/testdata/mybook/images/04-customize/caption_pagebreak.png +0 -0
  37. data/testdata/mybook/images/04-customize/chaptitlepage_sample.png +0 -0
  38. data/testdata/mybook/images/05-faq/codeblock_rpadding1.png +0 -0
  39. data/testdata/mybook/images/05-faq/codeblock_rpadding2.png +0 -0
  40. data/testdata/mybook/images/06-bestpractice/figure_heretop.png +0 -0
  41. data/testdata/mybook/images/06-bestpractice/font_beramono.png +0 -0
  42. data/testdata/mybook/images/06-bestpractice/heading_design1.png +0 -0
  43. data/testdata/mybook/images/06-bestpractice/heading_design2.png +0 -0
  44. data/testdata/mybook/images/06-bestpractice/margin_book.png +0 -0
  45. data/testdata/mybook/images/06-bestpractice/multiline-title.png +0 -0
  46. data/testdata/mybook/images/06-bestpractice/preface_numbered.png +0 -0
  47. data/testdata/mybook/images/06-bestpractice/program_border.png +0 -0
  48. data/testdata/mybook/images/06-bestpractice/sechead_design_4.png +0 -0
  49. data/testdata/mybook/images/06-bestpractice/titlepage-samples.png +0 -0
  50. data/testdata/mybook/images/93-background/bug913.png +0 -0
  51. data/testdata/mybook/images/93-background/slide2.png +0 -0
  52. data/testdata/mybook/images/cover_a5.pdf +0 -0
  53. data/testdata/mybook/images/cover_b5.pdf +0 -0
  54. data/testdata/mybook/images/tw-icon.jpg +0 -0
  55. data/testdata/mybook/layouts/layout.epub.erb +29 -0
  56. data/testdata/mybook/layouts/layout.html5.erb +106 -0
  57. data/testdata/mybook/layouts/layout.tex.erb +546 -0
  58. data/testdata/mybook/lib/hooks/beforetexcompile.rb +55 -0
  59. data/testdata/mybook/lib/ruby/review-builder.rb +503 -0
  60. data/testdata/mybook/lib/ruby/review-cli.rb +58 -0
  61. data/testdata/mybook/lib/ruby/review-compiler.rb +523 -0
  62. data/testdata/mybook/lib/ruby/review-epubmaker.rb +606 -0
  63. data/testdata/mybook/lib/ruby/review-htmlbuilder.rb +661 -0
  64. data/testdata/mybook/lib/ruby/review-latexbuilder.rb +782 -0
  65. data/testdata/mybook/lib/ruby/review-maker.rb +235 -0
  66. data/testdata/mybook/lib/ruby/review-monkeypatch.rb +91 -0
  67. data/testdata/mybook/lib/ruby/review-pdfmaker.rb +468 -0
  68. data/testdata/mybook/lib/ruby/review-textbuilder.rb +36 -0
  69. data/testdata/mybook/lib/ruby/review-tocparser.rb +285 -0
  70. data/testdata/mybook/lib/ruby/review-webmaker.rb +433 -0
  71. data/testdata/mybook/lib/tasks/mytasks.rake +31 -0
  72. data/testdata/mybook/lib/tasks/review.rake +142 -0
  73. data/testdata/mybook/lib/tasks/review.rake.orig +72 -0
  74. data/testdata/mybook/lib/tasks/starter.rake +326 -0
  75. data/testdata/mybook/locale.yml +6 -0
  76. data/testdata/mybook/review-ext.rb +206 -0
  77. data/testdata/mybook/sty/jumoline.sty +310 -0
  78. data/testdata/mybook/sty/mycolophon.sty +81 -0
  79. data/testdata/mybook/sty/mystyle.sty +8 -0
  80. data/testdata/mybook/sty/mytextsize.sty +61 -0
  81. data/testdata/mybook/sty/mytitlepage.sty +103 -0
  82. data/testdata/mybook/sty/reviewmacro.sty +60 -0
  83. data/testdata/mybook/sty/starter-codeblock.sty +332 -0
  84. data/testdata/mybook/sty/starter-color.sty +79 -0
  85. data/testdata/mybook/sty/starter-font.sty +112 -0
  86. data/testdata/mybook/sty/starter-heading.sty +514 -0
  87. data/testdata/mybook/sty/starter-note.sty +127 -0
  88. data/testdata/mybook/sty/starter-section.sty +262 -0
  89. data/testdata/mybook/sty/starter-toc.sty +72 -0
  90. data/testdata/mybook/sty/starter.sty +554 -0
  91. data/testdata/mybook/style.css +597 -0
  92. metadata +100 -3
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ ##
5
+ ## *.re を *.tex に変換したあとに実行されるスクリプト。
6
+ ## 第1引数:作業用展開ディレクトリ
7
+ ## 第2引数:呼び出しを実行したディレクトリ
8
+ ##
9
+
10
+
11
+ ##
12
+ ## @arg texdir 作業用展開ディレクトリ
13
+ ## @arg srcdir 呼び出しを実行したディレクトリ
14
+ ##
15
+ def main(texdir, srcdir)
16
+ Dir.glob("#{texdir}/*.tex").each do |filename|
17
+ s1 = File.open(filename, "rt:utf-8") {|f| f.read }
18
+ s2 = fix_reviewcolumn(s1) # コラムをいろいろ修正
19
+ if s1 != s2
20
+ origfile = filename + ".orig"
21
+ File.rename(filename, origfile) unless File.exist?(origfile)
22
+ File.open(filename, "w:utf-8") {|f| f.write(s2) }
23
+ end
24
+ end
25
+ end
26
+
27
+ ##
28
+ ## コラム(「==[column] タイトル」)を変換したあとのLaTeXコードを修正する
29
+ ##
30
+ def fix_reviewcolumn(content)
31
+ return content.gsub(/^\\begin\{reviewcolumn\}$.*?^\\end\{reviewcolumn\}$/m) {|str|
32
+ ##
33
+ ## これ↓だと脚注が消えてしまうので
34
+ ##
35
+ ## \begin{reviewcolumn}
36
+ ## 本文\footnotemark{}
37
+ ## \footnotetext[1]{脚注} % ←コラム内に脚注がある
38
+ ## \end{reviewcolumn}
39
+ ##
40
+ ## こう↓修正する
41
+ ##
42
+ ## \begin{reviewcolumn}
43
+ ## 本文\footnotemark{}
44
+ ## \end{reviewcolumn}
45
+ ## \footnotetext[1]{脚注} % ←コラム外に脚注を移動する
46
+ ##
47
+ fntexts = []
48
+ str = str.gsub(/^\\footnotetext\[\d+\]\{.*?\}\r?\n/m) {|s| fntexts << s; "" }
49
+ str << "\n" << fntexts.join() if fntexts
50
+ ##
51
+ str
52
+ }
53
+ end
54
+
55
+ main(*ARGV)
@@ -0,0 +1,503 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ###
4
+ ### ReVIEW::Builderクラスを拡張する
5
+ ###
6
+
7
+ require 'review/builder'
8
+
9
+
10
+ module ReVIEW
11
+
12
+ defined?(Builder) or raise "internal error: Builder not found."
13
+
14
+
15
+ class Builder
16
+
17
+ ## Re:VIEW3で追加されたもの
18
+ def on_inline_balloon(arg)
19
+ return "← #{yield}"
20
+ end
21
+
22
+ ## ul_item_begin() だけあって ol_item_begin() がないのはどうかと思う。
23
+ ## ol の入れ子がないからといって、こういう非対称な設計はやめてほしい。
24
+ def ol_item_begin(lines, _num)
25
+ ol_item(lines, _num)
26
+ end
27
+ def ol_item_end()
28
+ end
29
+
30
+ protected
31
+
32
+ def truncate_if_endwith?(str)
33
+ sio = @output # StringIO object
34
+ if sio.string.end_with?(str)
35
+ pos = sio.pos - str.length
36
+ sio.seek(pos)
37
+ sio.truncate(pos)
38
+ true
39
+ else
40
+ false
41
+ end
42
+ end
43
+
44
+ def enter_context(key)
45
+ @doc_status[key] = true
46
+ end
47
+
48
+ def exit_context(key)
49
+ @doc_status[key] = nil
50
+ end
51
+
52
+ def with_context(key)
53
+ enter_context(key)
54
+ return yield
55
+ ensure
56
+ exit_context(key)
57
+ end
58
+
59
+ def within_context?(key)
60
+ return @doc_status[key]
61
+ end
62
+
63
+ def within_codeblock?
64
+ d = @doc_status
65
+ d[:program] || d[:terminal] \
66
+ || d[:list] || d[:emlist] || d[:listnum] || d[:emlistnum] \
67
+ || d[:cmd] || d[:source]
68
+ end
69
+
70
+ ## 入れ子可能なブロック命令
71
+
72
+ public
73
+
74
+ def on_note_block caption=nil, &b; on_minicolumn :note , caption, &b; end
75
+ def on_memo_block caption=nil, &b; on_minicolumn :memo , caption, &b; end
76
+ def on_tip_block caption=nil, &b; on_minicolumn :tip , caption, &b; end
77
+ def on_info_block caption=nil, &b; on_minicolumn :info , caption, &b; end
78
+ def on_warning_block caption=nil, &b; on_minicolumn :warning , caption, &b; end
79
+ def on_important_block caption=nil, &b; on_minicolumn :important, caption, &b; end
80
+ def on_caution_block caption=nil, &b; on_minicolumn :caution , caption, &b; end
81
+ def on_notice_block caption=nil, &b; on_minicolumn :notice , caption, &b; end
82
+
83
+ def on_minicolumn(type, caption=nil, &b)
84
+ raise NotImplementedError.new("#{self.class.name}#on_minicolumn(): not implemented yet.")
85
+ end
86
+ protected :on_minicolumn
87
+
88
+ def on_sideimage_block(imagefile, imagewidth, option_str=nil, &b)
89
+ raise NotImplementedError.new("#{self.class.name}#on_sideimage_block(): not implemented yet.")
90
+ end
91
+
92
+ def validate_sideimage_args(imagefile, imagewidth, option_str)
93
+ opts = {}
94
+ if option_str.present?
95
+ option_str.split(',').each do |kv|
96
+ kv.strip!
97
+ next if kv.empty?
98
+ kv =~ /(\w[-\w]*)=(.*)/ or
99
+ error "//sideimage: [#{option_str}]: invalid option string."
100
+ opts[$1] = $2
101
+ end
102
+ end
103
+ #
104
+ opts.each do |k, v|
105
+ case k
106
+ when 'side'
107
+ v == 'L' || v == 'R' or
108
+ error "//sideimage: [#{option_str}]: 'side=' should be 'L' or 'R'."
109
+ when 'boxwidth'
110
+ v =~ /\A\d+(\.\d+)?(%|mm|cm|zw)\z/ or
111
+ error "//sideimage: [#{option_str}]: 'boxwidth=' invalid (expected such as 10%, 30mm, 3.0cm, or 5zw)"
112
+ when 'sep'
113
+ v =~ /\A\d+(\.\d+)?(%|mm|cm|zw)\z/ or
114
+ error "//sideimage: [#{option_str}]: 'sep=' invalid (expected such as 2%, 5mm, 0.5cm, or 1zw)"
115
+ when 'border'
116
+ v =~ /\A(on|off)\z/ or
117
+ error "//sideimage: [#{option_str}]: 'border=' should be 'on' or 'off'"
118
+ opts[k] = v == 'on' ? true : false
119
+ else
120
+ error "//sideimage: [#{option_str}]: unknown option '#{k}=#{v}'."
121
+ end
122
+ end
123
+ #
124
+ imagefile.present? or
125
+ error "//sideimage: 1st option (image file) required."
126
+ imagewidth.present? or
127
+ error "//sideimage: 2nd option (image width) required."
128
+ imagewidth =~ /\A\d+(\.\d+)?(%|mm|cm|zw|pt)\z/ or
129
+ error "//sideimage: [#{imagewidth}]: invalid image width (expected such as: 30mm, 3.0cm, 5zw, or 100pt)"
130
+ #
131
+ return imagefile, imagewidth, opts
132
+ end
133
+ protected :validate_sideimage_args
134
+
135
+ ## コードブロック(//program, //terminal)
136
+
137
+ CODEBLOCK_OPTIONS = {
138
+ 'fold' => true,
139
+ 'lineno' => false,
140
+ 'linenowidth' => -1,
141
+ 'eolmark' => false,
142
+ 'foldmark' => true,
143
+ 'lang' => nil,
144
+ }
145
+
146
+ ## プログラム用ブロック命令
147
+ ## ・//list と似ているが、長い行を自動的に折り返す
148
+ ## ・seqsplit.styの「\seqsplit{...}」コマンドを使っている
149
+ def program(lines, id=nil, caption=nil, optionstr=nil)
150
+ _codeblock('program', lines, id, caption, optionstr)
151
+ end
152
+
153
+ ## ターミナル用ブロック命令
154
+ ## ・//cmd と似ているが、長い行を自動的に折り返す
155
+ ## ・seqsplit.styの「\seqsplit{...}」コマンドを使っている
156
+ def terminal(lines, id=nil, caption=nil, optionstr=nil)
157
+ _codeblock('terminal', lines, id, caption, optionstr)
158
+ end
159
+
160
+ protected
161
+
162
+ def _codeblock(blockname, lines, id, caption, optionstr)
163
+ raise NotImplementedError.new("#{self.class.name}#_codeblock(): not implemented yet.")
164
+ end
165
+
166
+ def _each_block_option(option_str)
167
+ option_str.split(',').each do |kvs|
168
+ k, v = kvs.split('=', 2)
169
+ yield k, v
170
+ end if option_str && !option_str.empty?
171
+ end
172
+
173
+ def _parse_codeblock_optionstr(optionstr, blockname) # parse 'fold={on|off},...'
174
+ opts = {}
175
+ return opts if optionstr.nil? || optionstr.empty?
176
+ vals = {nil=>true, 'on'=>true, 'off'=>false}
177
+ optionstr.split(',').each_with_index do |x, i|
178
+ x = x.strip()
179
+ ## //list[][][1] は //list[][][lineno=1] とみなす
180
+ if x =~ /\A[0-9]+\z/
181
+ opts['lineno'] = x.to_i
182
+ next
183
+ end
184
+ #
185
+ x =~ /\A([-\w]+)(?:=(.*))?\z/ or
186
+ raise "//#{blockname}[][][#{x}]: invalid option format."
187
+ k, v = $1, $2
188
+ case k
189
+ when 'fold', 'eolmark', 'foldmark'
190
+ if vals.key?(v)
191
+ opts[k] = vals[v]
192
+ else
193
+ raise "//#{blockname}[][][#{x}]: expected 'on' or 'off'."
194
+ end
195
+ when 'lineno'
196
+ if vals.key?(v)
197
+ opts[k] = vals[v]
198
+ elsif v =~ /\A\d+\z/
199
+ opts[k] = v.to_i
200
+ elsif v =~ /\A\d+-?\d*(?:\&+\d+-?\d*)*\z/
201
+ opts[k] = v
202
+ else
203
+ raise "//#{blockname}[][][#{x}]: expected line number pattern."
204
+ end
205
+ when 'linenowidth'
206
+ if v =~ /\A-?\d+\z/
207
+ opts[k] = v.to_i
208
+ else
209
+ raise "//#{blockname}[][][#{x}]: expected integer value."
210
+ end
211
+ when 'fontsize'
212
+ if v =~ /\A((x-|xx-)?small|(x-|xx-)?large)\z/
213
+ opts[k] = v
214
+ else
215
+ raise "//#{blockname}[][][#{x}]: expected small/x-small/xx-small."
216
+ end
217
+ when 'indentwidth'
218
+ if v =~ /\A\d+\z/
219
+ opts[k] = v.to_i
220
+ else
221
+ raise "//#{blockname}[][][#{x}]: expected integer (>=0)."
222
+ end
223
+ when 'lang'
224
+ if v
225
+ opts[k] = v
226
+ else
227
+ raise "//#{blockname}[][][#{x}]: requires option value."
228
+ end
229
+ else
230
+ if i == 0
231
+ opts['lang'] = v
232
+ else
233
+ raise "//#{blockname}[][][#{x}]: unknown option."
234
+ end
235
+ end
236
+ end
237
+ return opts
238
+ end
239
+
240
+ def _build_caption_str(id, caption)
241
+ str = ""
242
+ with_context(:caption) do
243
+ str = compile_inline(caption) if caption.present?
244
+ end
245
+ if id.present?
246
+ number = _build_caption_number(id)
247
+ prefix = "#{I18n.t('list')}#{number}#{I18n.t('caption_prefix')}"
248
+ str = "#{prefix}#{str}"
249
+ end
250
+ return str
251
+ end
252
+
253
+ def _build_caption_number(id)
254
+ chapter = get_chap()
255
+ number = @chapter.list(id).number
256
+ return chapter.nil? \
257
+ ? I18n.t('format_number_header_without_chapter', [number]) \
258
+ : I18n.t('format_number_header', [chapter, number])
259
+ rescue KeyError
260
+ error "no such list: #{id}"
261
+ end
262
+
263
+ public
264
+
265
+ ## インライン命令のバグ修正
266
+
267
+ def inline_raw(str)
268
+ name = target_name()
269
+ if str =~ /\A\|(.*?)\|/
270
+ str = $'
271
+ return "" unless $1.split(',').any? {|s| s.strip == name }
272
+ end
273
+ return str.gsub('\\n', "\n")
274
+ end
275
+
276
+ def inline_embed(str)
277
+ name = target_name()
278
+ if str =~ /\A\|(.*?)\|/
279
+ str = $'
280
+ return "" unless $1.split(',').any? {|s| s.strip == name }
281
+ end
282
+ return str
283
+ end
284
+
285
+ ## インライン命令を入れ子に対応させる
286
+
287
+ def on_inline_href
288
+ escaped_str, items = yield true
289
+ url = label = nil
290
+ separator1 = /, /
291
+ separator2 = /(?<=[^\\}]),/ # 「\\,」はセパレータと見なさない
292
+ [separator1, separator2].each do |sep|
293
+ pair = items[0].split(sep, 2)
294
+ if pair.length == 2
295
+ url = pair[0]
296
+ label = escaped_str.split(sep, 2)[-1] # 「,」がエスケープされない前提
297
+ break
298
+ end
299
+ end
300
+ url ||= items[0]
301
+ url = url.gsub(/\\,/, ',') # 「\\,」を「,」に置換
302
+ return build_inline_href(url, label)
303
+ end
304
+
305
+ def on_inline_ruby
306
+ escaped_str = yield
307
+ arr = escaped_str.split(', ')
308
+ if arr.length > 1 # ex: @<ruby>{小鳥遊, たかなし}
309
+ yomi = arr.pop().strip()
310
+ word = arr.join(', ')
311
+ elsif escaped_str =~ /,([^,]*)\z/ # ex: @<ruby>{小鳥遊,たかなし}
312
+ word, yomi = $`, $1.strip()
313
+ else
314
+ error "@<ruby>: missing yomi, should be '@<ruby>{word, yomi}' style."
315
+ end
316
+ return build_inline_ruby(word, yomi)
317
+ end
318
+
319
+ ## 節 (Section) や項 (Subsection) を参照する。
320
+ ## 引数 id が節や項のラベルでないなら、エラー。
321
+ ## 使い方: @<secref>{label}
322
+ def inline_secref(id) # 参考:ReVIEW::Builder#inline_hd(id)
323
+ ## 本来、こういった処理はParserで行うべきであり、Builderで行うのはおかしい。
324
+ ## しかしRe:VIEWのアーキテクチャがよくないせいで、こうせざるを得ない。無念。
325
+ sec_id = id
326
+ chapter = nil
327
+ if id =~ /\A([^|]+)\|(.+)/
328
+ chap_id = $1; sec_id = $2
329
+ chapter = @book.contents.detect {|chap| chap.id == chap_id } or
330
+ error "@<secref>{#{id}}: chapter '#{chap_id}' not found."
331
+ end
332
+ begin
333
+ _inline_secref(chapter || @chapter, sec_id)
334
+ rescue KeyError
335
+ error "@<secref>{#{id}}: section (or subsection) not found."
336
+ end
337
+ end
338
+
339
+ private
340
+
341
+ def _inline_secref(chap, id)
342
+ sec_id = chap.headline(id).id
343
+ num, title = _get_secinfo(chap, sec_id)
344
+ level = num.split('.').length
345
+ #
346
+ secnolevel = @book.config['secnolevel']
347
+ if secnolevel + 1 < level
348
+ error "'secnolevel: #{secnolevel}' should be >= #{level-1} in config.yml"
349
+ ## config.ymlの「secnolevel:」が3以上なら、項 (Subsection) にも
350
+ ## 番号がつく。なので、節 (Section) のタイトルは必要ない。
351
+ elsif secnolevel + 1 > level
352
+ parent_title = nil
353
+ ## そうではない場合は、節 (Section) と項 (Subsection) とを組み合わせる。
354
+ ## たとえば、"「1.1 イントロダクション」内の「はじめに」" のように。
355
+ elsif secnolevel + 1 == level
356
+ parent_id = sec_id.sub(/\|[^|]+\z/, '')
357
+ _, parent_title = _get_secinfo(chap, parent_id)
358
+ else
359
+ raise "not reachable"
360
+ end
361
+ #
362
+ return _build_secref(chap, num, title, parent_title)
363
+ end
364
+
365
+ def _get_secinfo(chap, id) # 参考:ReVIEW::LATEXBuilder#inline_hd_chap()
366
+ num = chap.headline_index.number(id)
367
+ caption = compile_inline(chap.headline(id).caption)
368
+ if chap.number && @book.config['secnolevel'] >= num.split('.').size
369
+ caption = "#{chap.headline_index.number(id)} #{caption}"
370
+ end
371
+ #title = I18n.t('chapter_quote', caption)
372
+ title = caption
373
+ return num, title
374
+ end
375
+
376
+ def _build_secref(chap, num, title, parent_title)
377
+ raise NotImplementedError.new("#{self.class.name}#_build_secref(): not implemented yet.")
378
+ end
379
+
380
+ ##
381
+
382
+ public
383
+
384
+ ## ノートを参照する
385
+ def inline_noteref(label)
386
+ begin
387
+ chapter, label = parse_reflabel(label)
388
+ rescue KeyError => ex
389
+ error "@<noteref>{#{label}}: #{ex.message}"
390
+ end
391
+ begin
392
+ item = (chapter || @chapter).note(label)
393
+ rescue KeyError => ex
394
+ error "@<noteref>{#{label}}: note not found."
395
+ end
396
+ build_noteref(chapter, label, item.caption)
397
+ end
398
+
399
+ ## 数式を参照する
400
+ def inline_eq(label)
401
+ begin
402
+ chapter, label = parse_reflabel(label)
403
+ rescue KeyError => ex
404
+ error "@<eq>{#{label}}: #{ex.message}"
405
+ end
406
+ begin
407
+ item = (chapter || @chapter).equation(label)
408
+ rescue KeyError => ex
409
+ error "@<eq>{#{label}}: equation not found."
410
+ end
411
+ build_eq(chapter || @chapter, label, item.number)
412
+ end
413
+
414
+ protected
415
+
416
+ def build_noteref(chapter, label, caption)
417
+ raise NotImplementedError.new("#{self.class.name}#build_noteref(): not implemented yet.")
418
+ end
419
+
420
+ def build_eq(chapter, label, caption)
421
+ raise NotImplementedError.new("#{self.class.name}#build_noteref(): not implemented yet.")
422
+ end
423
+
424
+ def parse_reflabel(label)
425
+ ## 本来ならParserで行うべきだけど仕方ない。
426
+ chapter = nil
427
+ if label =~ /\A([^|]+)\|(.+)/
428
+ chap_id = $1; label = $2
429
+ chapter = @book.contents.detect {|chap| chap.id == chap_id } or
430
+ raise KeyError.new("chapter '#{chap_id}' not found.")
431
+ return chapter, label
432
+ end
433
+ return chapter, label
434
+ end
435
+
436
+ ##
437
+
438
+ protected
439
+
440
+ def find_image_filepath(image_id)
441
+ finder = get_image_finder()
442
+ filepath = finder.find_path(image_id)
443
+ return filepath
444
+ end
445
+
446
+ def get_image_finder()
447
+ imagedir = "#{@book.basedir}/#{@book.config['imagedir']}"
448
+ types = @book.image_types
449
+ builder = @book.config['builder']
450
+ chap_id = @chapter.id
451
+ return ReVIEW::Book::ImageFinder.new(imagedir, chap_id, builder, types)
452
+ end
453
+
454
+ end
455
+
456
+
457
+ ##
458
+ ## 行番号を生成するクラス。
459
+ ##
460
+ ## gen = LineNumberGenerator.new("1-3&8-10&15-")
461
+ ## p gen.each.take(15).to_a
462
+ ## #=> [1, 2, 3, nil, 8, 9, 10, nil, 15, 16, 17, 18, 19, 20, 21]
463
+ ##
464
+ class LineNumberGenerator
465
+
466
+ def initialize(arg)
467
+ @ranges = []
468
+ inf = Float::INFINITY
469
+ case arg
470
+ when true ; @ranges << (1 .. inf)
471
+ when Integer ; @ranges << (arg .. inf)
472
+ when /\A(\d+)\z/ ; @ranges << (arg.to_i .. inf)
473
+ else
474
+ arg.split('&', -1).each do |str|
475
+ case str
476
+ when /\A\z/
477
+ @ranges << nil
478
+ when /\A(\d+)\z/
479
+ @ranges << ($1.to_i .. $1.to_i)
480
+ when /\A(\d+)\-(\d+)?\z/
481
+ start = $1.to_i
482
+ end_ = $2 ? $2.to_i : inf
483
+ @ranges << (start..end_)
484
+ else
485
+ raise ArgumentError.new("'#{strpat}': invalid lineno format")
486
+ end
487
+ end
488
+ end
489
+ end
490
+
491
+ def each(&block)
492
+ return enum_for(:each) unless block_given?
493
+ for range in @ranges
494
+ range.each(&block) if range
495
+ yield nil
496
+ end
497
+ nil
498
+ end
499
+
500
+ end
501
+
502
+
503
+ end