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,58 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ##
4
+ ## ReVIEW::PDFMaker から、CLIに関する機能を分離する
5
+ ##
6
+
7
+ require 'optparse'
8
+
9
+
10
+ module ReVIEW
11
+
12
+
13
+ class CLI
14
+
15
+ def initialize(script_name)
16
+ @script_name = script_name
17
+ end
18
+
19
+ def parse_opts(args)
20
+ parser = OptionParser.new
21
+ parser.banner = "Usage: #{@script_name} <config.yaml>"
22
+ parser.version = ReVIEW::VERSION
23
+ cmdopts = {}
24
+ parser.on('-h', '--help', 'Prints this message and quit.') do
25
+ cmdopts['help'] = true
26
+ end
27
+ parser.on('--[no-]debug', 'Keep temporary files.') do |debug|
28
+ cmdopts['debug'] = debug
29
+ end
30
+ parser.on('--ignore-errors', 'Ignore review-compile errors.') do
31
+ cmdopts['ignore-errors'] = true
32
+ end
33
+ yield parser, cmdopts if block_given?
34
+ @_cmdopt_parser = parser
35
+ parser.parse!(args)
36
+ #
37
+ return cmdopts, args if cmdopts['help']
38
+ #
39
+ if args.length < 1
40
+ raise OptionParser::InvalidOption.new("Config filename required.")
41
+ elsif args.length > 1
42
+ raise OptionParser::InvalidOption.new("Too many arguments.")
43
+ end
44
+ config_filename = args[0]
45
+ File.exist?(config_filename) or
46
+ raise OptionParser::InvalidOption.new("file '#{config_filename}' not found.")
47
+ #
48
+ return cmdopts, config_filename
49
+ end
50
+
51
+ def help_message
52
+ return @_cmdopt_parser.help
53
+ end
54
+
55
+ end
56
+
57
+
58
+ end
@@ -0,0 +1,523 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ ###
4
+ ### ReVIEW::Compilerクラスとその関連クラスを拡張する
5
+ ###
6
+
7
+ require 'set'
8
+ require 'review/compiler'
9
+
10
+
11
+ module ReVIEW
12
+
13
+ defined?(Compiler) or raise "internal error: Compiler not found."
14
+
15
+
16
+ class Compiler
17
+
18
+ ## ブロック命令
19
+ defblock :program, 0..3 ## プログラム
20
+ defblock :terminal, 0..3 ## ターミナル
21
+ defblock :sideimage, 2..3 ## テキストの横に画像を表示
22
+ defblock :abstract, 0 ## 章の概要
23
+ defblock :list, 0..3 ## (上書き)
24
+ defblock :listnum, 0..3 ## (上書き)
25
+ defblock :note, 0..2 ## (上書き)
26
+ defblock :texequation, 0..2 ## (上書き)
27
+
28
+ defsingle :makechaptitlepage, 0..1 ## 章扉をつける
29
+ defsingle :needvspace, 2 ## 縦方向のスペースがなければ改ページ
30
+ defsingle :paragraphend, 0 ## 段の終わりにスペースを入れる
31
+ defsingle :subparagraphend, 0## 小段の終わりにスペースを入れる(あれば)
32
+
33
+ ## インライン命令
34
+ definline :balloon ## コード内でのふきだし説明(Re:VIEW3から追加)
35
+ definline :eq ## 数式を参照
36
+ definline :secref ## 節(Section)や項(Subsection)を参照
37
+ definline :noteref ## ノートを参照
38
+ definline :hlink ## @<href>{}の代わり
39
+ definline :file ## ファイル名
40
+ definline :userinput ## ユーザ入力
41
+ definline :nop ## 引数をそのまま表示 (No Operation)
42
+ definline :letitgo ## (nopのエイリアス名)
43
+ definline :foldhere ## 折り返し箇所を手動で指定
44
+ definline :cursor ## ターミナルでのカーソル
45
+ definline :weak ## 目立たせない(@<strong>{} の反対)
46
+ definline :small ## 文字サイズを小さく
47
+ definline :xsmall ## 文字サイズをもっと小さく
48
+ definline :xxsmall ## 文字サイズをもっともっと小さく
49
+ definline :large ## 文字サイズを大きく
50
+ definline :xlarge ## 文字サイズをもっと大きく
51
+ definline :xxlarge ## 文字サイズをもっともっと大きく
52
+ definline :xstrong ## 文字を大きくした@<strong>{}
53
+ definline :xxstrong ## 文字をもっと大きくした@<strong>{}
54
+
55
+ private
56
+
57
+ ## パーサを再帰呼び出しに対応させる
58
+
59
+ def do_compile
60
+ f = LineInput.new(StringIO.new(@chapter.content))
61
+ @strategy.bind self, @chapter, Location.new(@chapter.basename, f)
62
+ tagged_section_init
63
+ parse_document(f, false)
64
+ close_all_tagged_section
65
+ end
66
+
67
+ def parse_document(f, block_cmd)
68
+ while f.next?
69
+ case f.peek
70
+ when /\A\#@/
71
+ f.gets # Nothing to do
72
+ when /\A=+[\[\s\{]/
73
+ if block_cmd #+
74
+ line = f.gets #+
75
+ error "'#{line.strip}': should close '//#{block_cmd}' block before sectioning." #+
76
+ end #+
77
+ compile_headline f.gets
78
+ #when /\A\s+\*/ #-
79
+ # compile_ulist f #-
80
+ when LIST_ITEM_REXP #+
81
+ compile_list(f) #+
82
+ when /\A\s+\d+\./
83
+ compile_olist f
84
+ when /\A\s*:\s/
85
+ compile_dlist f
86
+ when %r{\A//\}}
87
+ return if block_cmd #+
88
+ f.gets
89
+ #error 'block end seen but not opened' #-
90
+ error "'//}': block-end found, but no block command opened." #+
91
+ #when %r{\A//[a-z]+} #-
92
+ # name, args, lines = read_command(f) #-
93
+ # syntax = syntax_descriptor(name) #-
94
+ # unless syntax #-
95
+ # error "unknown command: //#{name}" #-
96
+ # compile_unknown_command args, lines #-
97
+ # next #-
98
+ # end #-
99
+ # compile_command syntax, args, lines #-
100
+ when /\A\/\/\w+/ #+
101
+ parse_block_command(f) #+
102
+ when %r{\A//}
103
+ line = f.gets
104
+ warn "`//' seen but is not valid command: #{line.strip.inspect}"
105
+ if block_open?(line)
106
+ warn 'skipping block...'
107
+ read_block(f, false)
108
+ end
109
+ else
110
+ if f.peek.strip.empty?
111
+ f.gets
112
+ next
113
+ end
114
+ compile_paragraph f
115
+ end
116
+ end
117
+ end
118
+
119
+ ## コードブロックのタブ展開を、LaTeXコマンドの展開より先に行うよう変更。
120
+ ##
121
+ ## ・たとえば '\\' を '\\textbackslash{}' に展開してからタブを空白文字に
122
+ ## 展開しても、正しい展開にはならないことは明らか。先にタブを空白文字に
123
+ ## 置き換えてから、'\\' を '\\textbackslash{}' に展開すべき。
124
+ ## ・またタブ文字の展開は、本来はBuilderではなくCompilerで行うべきだが、
125
+ ## Re:VIEWの設計がまずいのでそうなっていない。
126
+ ## ・'//table' と '//embed' ではタブ文字の展開は行わない。
127
+ def read_block_for(cmdname, f) # 追加
128
+ disable_comment = cmdname == :embed # '//embed' では行コメントを読み飛ばさない
129
+ ignore_inline = cmdname == :embed # '//embed' ではインライン命令を解釈しない
130
+ enable_detab = cmdname !~ /\A(?:em)?table\z/ # '//table' ではタブ展開しない
131
+ f.enable_comment(false) if disable_comment
132
+ lines = read_block(f, ignore_inline, enable_detab)
133
+ f.enable_comment(true) if disable_comment
134
+ return lines
135
+ end
136
+ def read_block(f, ignore_inline, enable_detab=true) # 上書き
137
+ head = f.lineno
138
+ buf = []
139
+ builder = @strategy #+
140
+ f.until_match(%r{\A//\}}) do |line|
141
+ if ignore_inline
142
+ buf.push line
143
+ elsif line !~ /\A\#@/
144
+ #buf.push text(line.rstrip) #-
145
+ line = line.rstrip #+
146
+ line = builder.detab(line) if enable_detab #+
147
+ buf << text(line) #+
148
+ end
149
+ end
150
+ unless %r{\A//\}} =~ f.peek
151
+ error "unexpected EOF (block begins at: #{head})"
152
+ return buf
153
+ end
154
+ f.gets # discard terminator
155
+ buf
156
+ end
157
+
158
+ ## ブロック命令を入れ子可能に変更('//note' と '//quote')
159
+
160
+ def parse_block_command(f)
161
+ line = f.gets()
162
+ lineno = f.lineno
163
+ line =~ /\A\/\/(\w+)(\[.*\])?(\{)?$/ or
164
+ error "'#{line.strip}': invalid block command format."
165
+ cmdname = $1.intern; argstr = $2; curly = $3
166
+ ##
167
+ prev = @strategy.doc_status[cmdname]
168
+ @strategy.doc_status[cmdname] = true
169
+ ## 引数を取り出す
170
+ syntax = syntax_descriptor(cmdname) or
171
+ error "'//#{cmdname}': unknown command"
172
+ args = parse_args(argstr || "", cmdname)
173
+ begin
174
+ syntax.check_args args
175
+ rescue CompileError => err
176
+ error err.message
177
+ end
178
+ ## ブロックをとらないコマンドにブロックが指定されていたらエラー
179
+ if curly && !syntax.block_allowed?
180
+ error "'//#{cmdname}': this command should not take block (but given)."
181
+ end
182
+ ## ブロックの入れ子をサポートしてあれば、再帰的にパースする
183
+ handler = "on_#{cmdname}_block"
184
+ builder = @strategy
185
+ if builder.respond_to?(handler)
186
+ if curly
187
+ builder.__send__(handler, *args) do
188
+ parse_document(f, cmdname)
189
+ end
190
+ s = f.peek()
191
+ f.peek() =~ /\A\/\/}/ or
192
+ error "'//#{cmdname}': not closed (reached to EOF)"
193
+ f.gets() ## '//}' を読み捨てる
194
+ else
195
+ builder.__send__(handler, *args)
196
+ end
197
+ ## そうでなければ、従来と同じようにパースする
198
+ elsif builder.respond_to?(cmdname)
199
+ if !syntax.block_allowed?
200
+ builder.__send__(cmdname, *args)
201
+ elsif curly
202
+ lines = read_block_for(cmdname, f)
203
+ builder.__send__(cmdname, lines, *args)
204
+ else
205
+ lines = default_block(syntax)
206
+ builder.__send__(cmdname, lines, *args)
207
+ end
208
+ else
209
+ error "'//#{cmdname}': #{builder.class.name} not support this command"
210
+ end
211
+ ##
212
+ @strategy.doc_status[cmdname] = prev
213
+ end
214
+
215
+ ## 箇条書きの文法を拡張
216
+
217
+ LIST_ITEM_REXP = /\A( +)(\*+|\-+) +/ # '*' は unordred list、'-' は ordered list
218
+
219
+ def compile_list(f)
220
+ line = f.gets()
221
+ line =~ LIST_ITEM_REXP
222
+ indent = $1
223
+ char = $2[0]
224
+ $2.length == 1 or
225
+ error "#{$2[0]=='*'?'un':''}ordered list should start with level 1"
226
+ line = parse_list(f, line, indent, char, 1)
227
+ f.ungets(line)
228
+ end
229
+
230
+ def parse_list(f, line, indent, char, level)
231
+ if char != '*' && line =~ LIST_ITEM_REXP
232
+ start_num, _ = $'.lstrip().split(/\s+/, 2)
233
+ end
234
+ st = @strategy
235
+ char == '*' ? st.ul_begin { level } : st.ol_begin(start_num) { level }
236
+ while line =~ LIST_ITEM_REXP # /\A( +)(\*+|\-+) +/
237
+ $1 == indent or
238
+ error "mismatched indentation of #{$2[0]=='*'?'un':''}ordered list"
239
+ mark = $2
240
+ text = $'
241
+ if mark.length == level
242
+ break unless mark[0] == char
243
+ line = parse_item(f, text.lstrip(), indent, char, level)
244
+ elsif mark.length < level
245
+ break
246
+ else
247
+ raise "internal error"
248
+ end
249
+ end
250
+ char == '*' ? st.ul_end { level } : st.ol_end { level }
251
+ return line
252
+ end
253
+
254
+ def parse_item(f, text, indent, char, level)
255
+ if char != '*'
256
+ num, text = text.split(/\s+/, 2)
257
+ text ||= ''
258
+ end
259
+ #
260
+ buf = [parse_text(text)]
261
+ while (line = f.gets()) && line =~ /\A( +)/ && $1.length > indent.length
262
+ buf << parse_text(line)
263
+ end
264
+ #
265
+ st = @strategy
266
+ char == '*' ? st.ul_item_begin(buf) : st.ol_item_begin(buf, num)
267
+ rexp = LIST_ITEM_REXP # /\A( +)(\*+|\-+) +/
268
+ while line =~ rexp && $2.length > level
269
+ $2.length == level + 1 or
270
+ error "invalid indentation level of (un)ordred list"
271
+ line = parse_list(f, line, indent, $2[0], $2.length)
272
+ end
273
+ char == '*' ? st.ul_item_end() : st.ol_item_end()
274
+ #
275
+ return line
276
+ end
277
+
278
+ public
279
+
280
+ ## 入れ子のインライン命令をパースできるよう上書き
281
+ def parse_text(line)
282
+ stack = []
283
+ tag_name = nil
284
+ close_char = nil
285
+ items = [""]
286
+ nestable = true
287
+ scan_inline_command(line) do |text, s1, s2, s3|
288
+ if s1 # ex: '@<code>{', '@<b>{', '@<m>$'
289
+ if nestable
290
+ items << text
291
+ stack.push([tag_name, close_char, items])
292
+ s1 =~ /\A@<(\w+)>([{$|])\z/ or raise "internal error"
293
+ tag_name = $1
294
+ close_char = $2 == '{' ? '}' : $2
295
+ items = [""]
296
+ nestable = false if ignore_nested_inline_command?(tag_name)
297
+ else
298
+ items[-1] << text << s1
299
+ end
300
+ elsif s2 # '\}' or '\\' (not '\$' nor '\|')
301
+ text << (close_char == '}' ? s2[1] : s2)
302
+ items[-1] << text
303
+ elsif s3 # '}', '$', or '|'
304
+ items[-1] << text
305
+ if close_char == s3
306
+ items.delete_if {|x| x.empty? }
307
+ elem = [tag_name, {}, items]
308
+ tag_name, close_char, items = stack.pop()
309
+ items << elem << ""
310
+ nestable = true
311
+ else
312
+ items[-1] << s3
313
+ end
314
+ else
315
+ if items.length == 1 && items[-1].empty?
316
+ items[-1] = text
317
+ else
318
+ items[-1] << text
319
+ end
320
+ end
321
+ end
322
+ if tag_name
323
+ error "inline command '@<#{tag_name}>' not closed."
324
+ end
325
+ items.delete_if {|x| x.empty? }
326
+ #
327
+ return compile_inline_command(items)
328
+ end
329
+
330
+ alias text parse_text
331
+
332
+ private
333
+
334
+ def scan_inline_command(line)
335
+ pos = 0
336
+ line.scan(/(\@<\w+>[{$|])|(\\[\\}])|([}$|])/) do
337
+ m = Regexp.last_match
338
+ text = line[pos, m.begin(0)-pos]
339
+ pos = m.end(0)
340
+ yield text, $1, $2, $3
341
+ end
342
+ remained = pos == 0 ? line : line[pos..-1]
343
+ yield remained, nil, nil, nil
344
+ end
345
+
346
+ def compile_inline_command(items)
347
+ buf = ""
348
+ strategy = @strategy
349
+ items.each do |x|
350
+ case x
351
+ when String
352
+ buf << strategy.nofunc_text(x)
353
+ when Array
354
+ tag_name, attrs, children = x
355
+ op = tag_name
356
+ inline_defined?(op) or
357
+ raise CompileError, "no such inline op: #{op}"
358
+ if strategy.respond_to?("on_inline_#{op}")
359
+ buf << strategy.__send__("on_inline_#{op}") {|both_p|
360
+ if !both_p
361
+ compile_inline_command(children)
362
+ else
363
+ [compile_inline_command(children), children]
364
+ end
365
+ }
366
+ elsif strategy.respond_to?("inline_#{op}")
367
+ children.empty? || children.all? {|x| x.is_a?(String) } or
368
+ error "'@<#{op}>' does not support nested inline commands."
369
+
370
+ buf << strategy.__send__("inline_#{op}", children[0])
371
+ else
372
+ error "strategy does not support inline op: @<#{op}>"
373
+ end
374
+ else
375
+ raise "internal error: x=#{x.inspect}"
376
+ end
377
+ end
378
+ buf
379
+ end
380
+
381
+ def ignore_nested_inline_command?(tag_name)
382
+ return IGNORE_NESTED_INLINE_COMMANDS.include?(tag_name)
383
+ end
384
+
385
+ IGNORE_NESTED_INLINE_COMMANDS = Set.new(['m', 'raw', 'embed'])
386
+
387
+ end
388
+
389
+
390
+ ## コメント「#@#」を読み飛ばす(ただし //embed では読み飛ばさない)
391
+ class LineInput
392
+
393
+ def initialize(f)
394
+ super
395
+ @enable_comment = true
396
+ end
397
+
398
+ def enable_comment(flag)
399
+ @enable_comment = flag
400
+ end
401
+
402
+ def gets
403
+ line = super
404
+ if @enable_comment
405
+ while line && line =~ /\A\#\@\#/
406
+ line = super
407
+ end
408
+ end
409
+ return line
410
+ end
411
+
412
+ end
413
+
414
+
415
+ class Catalog
416
+
417
+ def parts_with_chaps
418
+ ## catalog.ymlの「CHAPS:」がnullのときエラーになるのを防ぐ
419
+ (@yaml['CHAPS'] || []).flatten.compact
420
+ end
421
+
422
+ end
423
+
424
+
425
+ class Book::ListIndex
426
+
427
+ ## '//program' と '//terminal' をサポートするよう拡張
428
+ def self.item_type # override
429
+ #'(list|listnum)' # original
430
+ '(list|listnum|program|terminal)'
431
+ end
432
+
433
+ ## '//list' や '//terminal' のラベル(第1引数)を省略できるよう拡張
434
+ def self.parse(src, *args) # override
435
+ items = []
436
+ seq = 1
437
+ src.grep(%r{\A//#{item_type}}) do |line|
438
+ if id = line.slice(/\[(.*?)\]/, 1)
439
+ next if id.empty? # 追加
440
+ items.push item_class.new(id, seq)
441
+ seq += 1
442
+ ReVIEW.logger.warn "warning: no ID of #{item_type} in #{line}" if id.empty?
443
+ end
444
+ end
445
+ new(items, *args)
446
+ end
447
+
448
+ end
449
+
450
+
451
+ ## ノートブロック(//note)のラベル用
452
+ class Book::NoteIndex < Book::Index # create new class for '//note'
453
+ Item = Struct.new(:id, :caption)
454
+
455
+ def self.parse(src_lines)
456
+ rexp = /\A\/\/note\[([^\]]+)\]\[(.*?[^\\])\]/ # $1: label, $2: caption
457
+ items = src_lines.grep(rexp) {|line|
458
+ label = $1.strip(); caption = $2.gsub(/\\\]/, ']')
459
+ next if label.empty?
460
+ label =~ /\A[-_a-zA-Z0-9]+\z/ or
461
+ error "'#{line}': invalid label (only alphabet, number, '_' or '-' available)"
462
+ Item.new(label, caption)
463
+ }.compact()
464
+ self.new(items)
465
+ end
466
+
467
+ end
468
+
469
+
470
+ ## 数式(//texequation)のラベル用
471
+ class Book::EquationIndex < Book::Index
472
+ def self.item_type
473
+ '(texequation)'
474
+ end
475
+ end
476
+
477
+
478
+ module Book::Compilable
479
+
480
+ def note(id)
481
+ note_index()[id]
482
+ end
483
+
484
+ def note_index
485
+ @note_index ||= Book::NoteIndex.parse(lines())
486
+ @note_index
487
+ end
488
+
489
+ def equation(id)
490
+ equation_index()[id]
491
+ end
492
+
493
+ def equation_index
494
+ @equation_index ||= Book::EquationIndex.parse(lines)
495
+ @equation_index
496
+ end
497
+
498
+ def content # override
499
+ ## //list[?] や //terminal[?] の '?' をランダム文字列に置き換える。
500
+ ## こうすると、重複しないラベルをいちいち指定しなくても、ソースコードや
501
+ ## ターミナルにリスト番号がつく。ただし @<list>{} での参照はできない。
502
+ unless @_done
503
+ pat = Book::ListIndex.item_type # == '(list|listnum|program|terminal)'
504
+ @content = @content.gsub(/^\/\/#{pat}\[\?\]/) { "//#{$1}[#{_random_label()}]" }
505
+ ## 改行コードを「\n」に統一する
506
+ @content = @content.gsub(/\r\n/, "\n")
507
+ ## (experimental) 範囲コメント('#@+++' '#@---')を行コメント('#@#')に変換
508
+ @content = @content.gsub(/^\#\@\+\+\+$.*?^\#\@\-\-\-$/m) { $&.gsub(/^/, '#@#') }
509
+ @_done = true
510
+ end
511
+ @content
512
+ end
513
+
514
+ module_function
515
+
516
+ def _random_label
517
+ "_" + rand().to_s[2..10]
518
+ end
519
+
520
+ end
521
+
522
+
523
+ end