review-retrovert 0.2.2 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) 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 +68 -2
  7. data/README.md +6 -3
  8. data/lib/review/retrovert/cli.rb +2 -0
  9. data/lib/review/retrovert/converter.rb +255 -16
  10. data/lib/review/retrovert/version.rb +1 -1
  11. data/review-retrovert.gemspec +1 -1
  12. data/testdata/mybook/.gitignore +7 -0
  13. data/testdata/mybook/README.md +43 -0
  14. data/testdata/mybook/Rakefile +16 -0
  15. data/testdata/mybook/catalog.yml +33 -0
  16. data/testdata/mybook/config-starter.yml +139 -0
  17. data/testdata/mybook/config.yml +375 -0
  18. data/testdata/mybook/contents/00-preface.re +83 -0
  19. data/testdata/mybook/contents/01-install.re +272 -0
  20. data/testdata/mybook/contents/02-tutorial.re +1008 -0
  21. data/testdata/mybook/contents/03-syntax.re +2613 -0
  22. data/testdata/mybook/contents/04-customize.re +728 -0
  23. data/testdata/mybook/contents/05-faq.re +328 -0
  24. data/testdata/mybook/contents/06-bestpractice.re +971 -0
  25. data/testdata/mybook/contents/91-compare.re +18 -0
  26. data/testdata/mybook/contents/92-filelist.re +119 -0
  27. data/testdata/mybook/contents/93-background.re +267 -0
  28. data/testdata/mybook/contents/99-postface.re +38 -0
  29. data/testdata/mybook/css/normalize.css +349 -0
  30. data/testdata/mybook/css/webstyle.css +514 -0
  31. data/testdata/mybook/images/03-syntax/favicon-16x16.png +0 -0
  32. data/testdata/mybook/images/03-syntax/figure_heretop.png +0 -0
  33. data/testdata/mybook/images/03-syntax/tw-icon1.jpg +0 -0
  34. data/testdata/mybook/images/03-syntax/tw-icon2.jpg +0 -0
  35. data/testdata/mybook/images/03-syntax/tw-icon3.jpg +0 -0
  36. data/testdata/mybook/images/03-syntax/tw-icon4.jpg +0 -0
  37. data/testdata/mybook/images/04-customize/caption_pagebreak.png +0 -0
  38. data/testdata/mybook/images/04-customize/chaptitlepage_sample.png +0 -0
  39. data/testdata/mybook/images/05-faq/codeblock_rpadding1.png +0 -0
  40. data/testdata/mybook/images/05-faq/codeblock_rpadding2.png +0 -0
  41. data/testdata/mybook/images/06-bestpractice/figure_heretop.png +0 -0
  42. data/testdata/mybook/images/06-bestpractice/font_beramono.png +0 -0
  43. data/testdata/mybook/images/06-bestpractice/heading_design1.png +0 -0
  44. data/testdata/mybook/images/06-bestpractice/heading_design2.png +0 -0
  45. data/testdata/mybook/images/06-bestpractice/margin_book.png +0 -0
  46. data/testdata/mybook/images/06-bestpractice/multiline-title.png +0 -0
  47. data/testdata/mybook/images/06-bestpractice/preface_numbered.png +0 -0
  48. data/testdata/mybook/images/06-bestpractice/program_border.png +0 -0
  49. data/testdata/mybook/images/06-bestpractice/sechead_design_4.png +0 -0
  50. data/testdata/mybook/images/06-bestpractice/titlepage-samples.png +0 -0
  51. data/testdata/mybook/images/93-background/bug913.png +0 -0
  52. data/testdata/mybook/images/93-background/slide2.png +0 -0
  53. data/testdata/mybook/images/cover_a5.pdf +0 -0
  54. data/testdata/mybook/images/cover_b5.pdf +0 -0
  55. data/testdata/mybook/images/tw-icon.jpg +0 -0
  56. data/testdata/mybook/layouts/layout.epub.erb +29 -0
  57. data/testdata/mybook/layouts/layout.html5.erb +106 -0
  58. data/testdata/mybook/layouts/layout.tex.erb +546 -0
  59. data/testdata/mybook/lib/hooks/beforetexcompile.rb +55 -0
  60. data/testdata/mybook/lib/ruby/review-builder.rb +503 -0
  61. data/testdata/mybook/lib/ruby/review-cli.rb +58 -0
  62. data/testdata/mybook/lib/ruby/review-compiler.rb +523 -0
  63. data/testdata/mybook/lib/ruby/review-epubmaker.rb +606 -0
  64. data/testdata/mybook/lib/ruby/review-htmlbuilder.rb +661 -0
  65. data/testdata/mybook/lib/ruby/review-latexbuilder.rb +782 -0
  66. data/testdata/mybook/lib/ruby/review-maker.rb +235 -0
  67. data/testdata/mybook/lib/ruby/review-monkeypatch.rb +91 -0
  68. data/testdata/mybook/lib/ruby/review-pdfmaker.rb +468 -0
  69. data/testdata/mybook/lib/ruby/review-textbuilder.rb +36 -0
  70. data/testdata/mybook/lib/ruby/review-tocparser.rb +285 -0
  71. data/testdata/mybook/lib/ruby/review-webmaker.rb +433 -0
  72. data/testdata/mybook/lib/tasks/mytasks.rake +31 -0
  73. data/testdata/mybook/lib/tasks/review.rake +142 -0
  74. data/testdata/mybook/lib/tasks/review.rake.orig +72 -0
  75. data/testdata/mybook/lib/tasks/starter.rake +326 -0
  76. data/testdata/mybook/locale.yml +6 -0
  77. data/testdata/mybook/review-ext.rb +206 -0
  78. data/testdata/mybook/sty/jumoline.sty +310 -0
  79. data/testdata/mybook/sty/mycolophon.sty +81 -0
  80. data/testdata/mybook/sty/mystyle.sty +8 -0
  81. data/testdata/mybook/sty/mytextsize.sty +61 -0
  82. data/testdata/mybook/sty/mytitlepage.sty +103 -0
  83. data/testdata/mybook/sty/reviewmacro.sty +60 -0
  84. data/testdata/mybook/sty/starter-codeblock.sty +332 -0
  85. data/testdata/mybook/sty/starter-color.sty +79 -0
  86. data/testdata/mybook/sty/starter-font.sty +112 -0
  87. data/testdata/mybook/sty/starter-heading.sty +514 -0
  88. data/testdata/mybook/sty/starter-note.sty +127 -0
  89. data/testdata/mybook/sty/starter-section.sty +262 -0
  90. data/testdata/mybook/sty/starter-toc.sty +72 -0
  91. data/testdata/mybook/sty/starter.sty +554 -0
  92. data/testdata/mybook/style.css +597 -0
  93. 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