review-retrovert 0.9.11 → 0.10.0

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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +1 -1
  3. data/.pinact.yaml +12 -0
  4. data/.ruby-version +1 -1
  5. data/Dockerfile +2 -1
  6. data/Dockerfile.local +13 -0
  7. data/Gemfile.lock +62 -35
  8. data/Rakefile +9 -0
  9. data/aqua.yaml +20 -0
  10. data/hooks/post_push +14 -1
  11. data/lib/review/retrovert/cli.rb +6 -0
  12. data/lib/review/retrovert/converter.rb +38 -0
  13. data/lib/review/retrovert/version.rb +1 -1
  14. data/lib/review/retrovert/yamlconfig.rb +38 -1
  15. data/review-retrovert.gemspec +18 -4
  16. data/zizmor.yml +5 -0
  17. metadata +53 -127
  18. data/.github/FUNDING.yml +0 -4
  19. data/.github/workflows/docker-build.yml +0 -26
  20. data/.github/workflows/release.yml +0 -26
  21. data/.github/workflows/retrovert.yml +0 -91
  22. data/testdata/mybook/.gitignore +0 -13
  23. data/testdata/mybook/.textlintrc +0 -11
  24. data/testdata/mybook/README.md +0 -43
  25. data/testdata/mybook/Rakefile +0 -24
  26. data/testdata/mybook/catalog.yml +0 -60
  27. data/testdata/mybook/config-noretrovert.yml +0 -404
  28. data/testdata/mybook/config-retrovert.yml +0 -16
  29. data/testdata/mybook/config-starter.yml +0 -264
  30. data/testdata/mybook/config.yml +0 -404
  31. data/testdata/mybook/contents/00-preface.re +0 -129
  32. data/testdata/mybook/contents/01-install.re +0 -305
  33. data/testdata/mybook/contents/02-tutorial.re +0 -1228
  34. data/testdata/mybook/contents/03-syntax.re +0 -4610
  35. data/testdata/mybook/contents/04-customize.re +0 -1064
  36. data/testdata/mybook/contents/05-faq.re +0 -606
  37. data/testdata/mybook/contents/06-bestpractice.re +0 -1343
  38. data/testdata/mybook/contents/91-compare.re +0 -418
  39. data/testdata/mybook/contents/92-filelist.re +0 -125
  40. data/testdata/mybook/contents/93-background.re +0 -267
  41. data/testdata/mybook/contents/99-postface.re +0 -39
  42. data/testdata/mybook/contents/r0-inner.re +0 -2
  43. data/testdata/mybook/contents/r0-root.re +0 -49
  44. data/testdata/mybook/contents/table.csv +0 -4
  45. data/testdata/mybook/contents/test.txt +0 -1
  46. data/testdata/mybook/contents/ut.re +0 -5
  47. data/testdata/mybook/css/normalize.css +0 -349
  48. data/testdata/mybook/css/webstyle.css +0 -692
  49. data/testdata/mybook/data/terms.txt +0 -3
  50. data/testdata/mybook/data/words.txt +0 -15
  51. data/testdata/mybook/images/03-syntax/favicon-16x16.png +0 -0
  52. data/testdata/mybook/images/03-syntax/figure_heretop.png +0 -0
  53. data/testdata/mybook/images/03-syntax/index-page.png +0 -0
  54. data/testdata/mybook/images/03-syntax/order-detail.png +0 -0
  55. data/testdata/mybook/images/03-syntax/tw-icon1.jpg +0 -0
  56. data/testdata/mybook/images/03-syntax/tw-icon2.jpg +0 -0
  57. data/testdata/mybook/images/03-syntax/tw-icon3.jpg +0 -0
  58. data/testdata/mybook/images/03-syntax/tw-icon4.jpg +0 -0
  59. data/testdata/mybook/images/04-customize/caption_pagebreak.png +0 -0
  60. data/testdata/mybook/images/04-customize/chaptitlepage_sample.png +0 -0
  61. data/testdata/mybook/images/04-customize/section_decoration_samples.png +0 -0
  62. data/testdata/mybook/images/05-faq/codeblock_rpadding1.png +0 -0
  63. data/testdata/mybook/images/05-faq/codeblock_rpadding2.png +0 -0
  64. data/testdata/mybook/images/05-faq/dummy-image.png +0 -0
  65. data/testdata/mybook/images/06-bestpractice/figure_heretop.png +0 -0
  66. data/testdata/mybook/images/06-bestpractice/font_beramono.png +0 -0
  67. data/testdata/mybook/images/06-bestpractice/heading_design1.png +0 -0
  68. data/testdata/mybook/images/06-bestpractice/heading_design2.png +0 -0
  69. data/testdata/mybook/images/06-bestpractice/margin_book.png +0 -0
  70. data/testdata/mybook/images/06-bestpractice/multiline-title.png +0 -0
  71. data/testdata/mybook/images/06-bestpractice/preface_numbered.png +0 -0
  72. data/testdata/mybook/images/06-bestpractice/program_border.png +0 -0
  73. data/testdata/mybook/images/06-bestpractice/sechead_design_4.png +0 -0
  74. data/testdata/mybook/images/06-bestpractice/section_title_wlines.png +0 -0
  75. data/testdata/mybook/images/06-bestpractice/titlepage-samples.png +0 -0
  76. data/testdata/mybook/images/93-background/bug913.png +0 -0
  77. data/testdata/mybook/images/93-background/slide2.png +0 -0
  78. data/testdata/mybook/images/avatar-b.png +0 -0
  79. data/testdata/mybook/images/avatar-g.png +0 -0
  80. data/testdata/mybook/images/avatar-r.png +0 -0
  81. data/testdata/mybook/images/caution-icon.png +0 -0
  82. data/testdata/mybook/images/cover_a5.pdf +0 -0
  83. data/testdata/mybook/images/cover_b5.pdf +0 -0
  84. data/testdata/mybook/images/info-icon.png +0 -0
  85. data/testdata/mybook/images/tw-icon.jpg +0 -0
  86. data/testdata/mybook/images/warning-icon.png +0 -0
  87. data/testdata/mybook/layouts/layout.epub.erb +0 -29
  88. data/testdata/mybook/layouts/layout.html5.erb +0 -108
  89. data/testdata/mybook/layouts/layout.tex.erb +0 -432
  90. data/testdata/mybook/layouts/layout.tex.erb.orig +0 -386
  91. data/testdata/mybook/lib/hooks/beforetexcompile.rb +0 -55
  92. data/testdata/mybook/lib/ruby/review-book.rb +0 -64
  93. data/testdata/mybook/lib/ruby/review-builder.rb +0 -1342
  94. data/testdata/mybook/lib/ruby/review-cli.rb +0 -58
  95. data/testdata/mybook/lib/ruby/review-compiler.rb +0 -1176
  96. data/testdata/mybook/lib/ruby/review-epubbuilder.rb +0 -33
  97. data/testdata/mybook/lib/ruby/review-epubmaker.rb +0 -609
  98. data/testdata/mybook/lib/ruby/review-htmlbuilder.rb +0 -949
  99. data/testdata/mybook/lib/ruby/review-latexbuilder.rb +0 -1065
  100. data/testdata/mybook/lib/ruby/review-maker.rb +0 -346
  101. data/testdata/mybook/lib/ruby/review-markdownbuilder.rb +0 -945
  102. data/testdata/mybook/lib/ruby/review-markdownmaker.rb +0 -91
  103. data/testdata/mybook/lib/ruby/review-monkeypatch.rb +0 -93
  104. data/testdata/mybook/lib/ruby/review-pdfmaker.rb +0 -546
  105. data/testdata/mybook/lib/ruby/review-textbuilder.rb +0 -36
  106. data/testdata/mybook/lib/ruby/review-tocparser.rb +0 -285
  107. data/testdata/mybook/lib/ruby/review-webmaker.rb +0 -448
  108. data/testdata/mybook/lib/tasks/mytasks.rake +0 -31
  109. data/testdata/mybook/lib/tasks/review.rake +0 -156
  110. data/testdata/mybook/lib/tasks/review.rake.orig +0 -72
  111. data/testdata/mybook/lib/tasks/starter.rake +0 -470
  112. data/testdata/mybook/locale.yml +0 -6
  113. data/testdata/mybook/review-ext.rb +0 -206
  114. data/testdata/mybook/sty/indexstyle.ist +0 -25
  115. data/testdata/mybook/sty/jumoline.sty +0 -310
  116. data/testdata/mybook/sty/mycolophon.sty +0 -81
  117. data/testdata/mybook/sty/mystyle.sty +0 -8
  118. data/testdata/mybook/sty/mytextsize.sty +0 -94
  119. data/testdata/mybook/sty/mytitlepage.sty +0 -126
  120. data/testdata/mybook/sty/review-base.sty +0 -276
  121. data/testdata/mybook/sty/reviewmacro.sty +0 -60
  122. data/testdata/mybook/sty/starter-codeblock.sty +0 -463
  123. data/testdata/mybook/sty/starter-color.sty +0 -134
  124. data/testdata/mybook/sty/starter-font.sty +0 -112
  125. data/testdata/mybook/sty/starter-heading.sty +0 -561
  126. data/testdata/mybook/sty/starter-misc.sty +0 -894
  127. data/testdata/mybook/sty/starter-note.sty +0 -180
  128. data/testdata/mybook/sty/starter-section.sty +0 -262
  129. data/testdata/mybook/sty/starter-talklist.sty +0 -105
  130. data/testdata/mybook/sty/starter-toc.sty +0 -72
  131. data/testdata/mybook/sty/starter-util.sty +0 -39
  132. data/testdata/mybook/sty/starter.sty +0 -36
  133. data/testdata/mybook/sty/ut.sty +0 -26
  134. data/testdata/mybook/style.css +0 -597
  135. data/testdata/mybook/ut-catalog.yml +0 -61
  136. data/testdata/mybook/ut-config-starter.yml +0 -3
  137. data/testdata/mybook/ut-config.yml +0 -404
@@ -1,1342 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- ###
4
- ### ReVIEW::Builderクラスを拡張する
5
- ###
6
-
7
- require 'csv'
8
-
9
- require 'review/builder'
10
-
11
-
12
- module ReVIEW
13
-
14
- defined?(Builder) or raise "internal error: Builder not found."
15
-
16
-
17
- class Builder
18
-
19
- def target_name
20
- c = self.class.to_s.gsub('ReVIEW::', '').gsub('Builder', '').downcase
21
- return c
22
- end
23
-
24
- def config_starter
25
- return @book.config['starter']
26
- end
27
-
28
- def _current_target?(name_str)
29
- s = name_str.to_s.strip
30
- return true if s.empty? || s == '*'
31
- t = target_name()
32
- return s.split(',').map(&:strip).include?(t)
33
- end
34
-
35
- ## Re:VIEW3で追加されたもの
36
- def on_inline_balloon(arg)
37
- return "← #{yield}"
38
- end
39
-
40
- ## ul_item_begin() だけあって ol_item_begin() がないのはどうかと思う。
41
- ## ol の入れ子がないからといって、こういう非対称な設計はやめてほしい。
42
- def ol_item_begin(lines, _num)
43
- ol_item(lines, _num)
44
- end
45
- def ol_item_end()
46
- end
47
-
48
- def dl_dd_begin()
49
- end
50
-
51
- def dl_dd_end()
52
- end
53
-
54
- protected
55
-
56
- def truncate_if_endwith?(str)
57
- sio = @output # StringIO object
58
- if sio.string.end_with?(str)
59
- pos = sio.pos - str.length
60
- sio.seek(pos)
61
- sio.truncate(pos)
62
- true
63
- else
64
- false
65
- end
66
- end
67
-
68
- def truncate_blank()
69
- sio = @output # StringIO object
70
- if sio.string.end_with?("\n\n")
71
- pos = sio.pos - 1
72
- sio.seek(pos)
73
- sio.truncate(pos)
74
- true
75
- else
76
- false
77
- end
78
- end
79
-
80
- def enter_context(key)
81
- @doc_status[key] = true
82
- end
83
-
84
- def exit_context(key)
85
- @doc_status[key] = nil
86
- end
87
-
88
- def with_context(key)
89
- enter_context(key)
90
- return yield
91
- ensure
92
- exit_context(key)
93
- end
94
-
95
- def within_context?(key)
96
- return @doc_status[key]
97
- end
98
-
99
- def within_codeblock?
100
- d = @doc_status
101
- d[:program] || d[:terminal] || d[:output]\
102
- || d[:list] || d[:emlist] || d[:listnum] || d[:emlistnum] \
103
- || d[:cmd] || d[:source]
104
- end
105
-
106
- ## 入れ子可能なブロック命令
107
-
108
- public
109
-
110
- def on_note_block caption=nil, &b; on_minicolumn :note , caption, &b; end
111
- def on_memo_block caption=nil, &b; on_minicolumn :memo , caption, &b; end
112
- def on_tip_block caption=nil, &b; on_minicolumn :tip , caption, &b; end
113
- def on_info_block caption=nil, &b; on_minicolumn :info , caption, &b; end
114
- def on_warning_block caption=nil, &b; on_minicolumn :warning , caption, &b; end
115
- def on_important_block caption=nil, &b; on_minicolumn :important, caption, &b; end
116
- def on_caution_block caption=nil, &b; on_minicolumn :caution , caption, &b; end
117
- def on_notice_block caption=nil, &b; on_minicolumn :notice , caption, &b; end
118
-
119
- def on_minicolumn(type, caption=nil, &b)
120
- raise NotImplementedError.new("#{self.class.name}#on_minicolumn(): not implemented yet.")
121
- end
122
- protected :on_minicolumn
123
-
124
- ## 画像横に文章
125
-
126
- def on_sideimage_block(imagefile, imagewidth, option_str=nil, &b)
127
- opts = _sideimage_parse_options(option_str) { "//sideimage[#{imagefile}][#{imagewidth}]" }
128
- _sideimage_validate_args(imagefile, imagewidth)
129
- filepath = find_image_filepath(imagefile)
130
- _render_sideimage(filepath, imagewidth, opts, &b)
131
- end
132
-
133
- def _render_sideimage(filepath, imagewidth, opts, &b)
134
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
135
- end
136
-
137
- def _sideimage_parse_options(option_str, &b)
138
- opts = {}
139
- _each_block_option(option_str) do |k, v|
140
- case k
141
- when 'side'
142
- v == 'L' || v == 'R' or
143
- error "#{yield}[#{k}=#{v}]: 'side=' should be 'L' or 'R'."
144
- when 'boxwidth'
145
- v =~ /\A\d+(\.\d+)?(%|mm|cm|zw)\z/ or
146
- error "#{yield}[#{k}=#{v}]: 'boxwidth=' invalid (expected such as 10%, 30mm, 3.0cm, or 5zw)"
147
- when 'sep'
148
- v =~ /\A\d+(\.\d+)?(%|mm|cm|zw)\z/ or
149
- error "#{yield}[#{k}=#{v}]: 'sep=' invalid (expected such as 2%, 5mm, 0.5cm, or 1zw)"
150
- when 'border'
151
- v.nil? || v =~ /\A(on|off)\z/ or
152
- error "#{yield}[#{k}=#{v}]: 'border=' should be 'on' or 'off'"
153
- v = v.nil? ? true : v == 'on'
154
- else
155
- error "#{yield}[#{k}=#{v}]: unknown option '#{k}=#{v}'."
156
- end
157
- opts[k] = v
158
- end
159
- return opts
160
- end
161
-
162
- def _sideimage_validate_args(imagefile, imagewidth)
163
- imagefile.present? or
164
- error "//sideimage: 1st option (image file) required."
165
- imagewidth.present? or
166
- error "//sideimage: 2nd option (image width) required."
167
- imagewidth =~ /\A\d+(\.\d+)?(%|mm|cm|zw|pt)\z/ or
168
- error "//sideimage: [#{imagewidth}]: invalid image width (expected such as: 30mm, 3.0cm, 5zw, or 100pt)"
169
- end
170
-
171
- ## コードブロック(//program, //terminal, //output)
172
-
173
- ## プログラム用ブロック命令
174
- def program(lines, id=nil, caption=nil, optionstr=nil)
175
- _codeblock('program', lines, id, caption, optionstr)
176
- end
177
-
178
- ## ターミナル用ブロック命令
179
- def terminal(lines, id=nil, caption=nil, optionstr=nil)
180
- _codeblock('terminal', lines, id, caption, optionstr)
181
- end
182
-
183
- ## 出力結果用ブロック命令
184
- def output(lines, id=nil, caption=nil, optionstr=nil)
185
- _codeblock('output', lines, id, caption, optionstr)
186
- end
187
-
188
- protected
189
-
190
- def _codeblock(blockname, lines, id, caption, optionstr)
191
- opts = _codeblock_parse_options(optionstr) {
192
- "//#{blockname}[#{id}][#{caption}]"
193
- }
194
- default_opts = _codeblock_default_options(blockname)
195
- default_opts.each {|k, v| opts[k] = v unless opts.key?(k) }
196
- #
197
- if opts['file']
198
- lines = _codeblock_read_file(opts['file'], opts['encoding'])
199
- end
200
- #
201
- lines = lines.map {|line| detab(line) }
202
- #
203
- if id.present? || caption.present?
204
- caption_str = _build_caption_str(id, caption)
205
- else
206
- caption_str = nil
207
- end
208
- #
209
- _render_codeblock(blockname, lines, id, caption_str, opts)
210
- end
211
-
212
- def _codeblock_default_options(blockname)
213
- terminal_p = (blockname == 'terminal' || blockname == 'cmd')
214
- dict = self.config_starter()
215
- return dict['terminal_default_options'] if terminal_p
216
- return dict['program_default_options']
217
- end
218
-
219
- def _each_block_option(option_str)
220
- return if option_str.nil?
221
- option_str = option_str.strip()
222
- return if option_str.empty?
223
- b = nil
224
- option_str.split(',').each do |kvs|
225
- k, v = kvs.strip.split('=', 2)
226
- if k =~ /::?/
227
- t = $`; k = $'
228
- b ||= target_name()
229
- next unless t == b
230
- end
231
- yield k, v
232
- end
233
- end
234
-
235
- def _codeblock_parse_options(option_str, &b) # parse 'fold={on|off},...'
236
- opts = {}
237
- return opts if option_str.nil? || option_str.empty?
238
- vals = {nil=>true, 'on'=>true, 'off'=>false}
239
- i = -1
240
- _each_block_option(option_str) do |k, v|
241
- i += 1
242
- ## //list[][][1] は //list[][][lineno=1] とみなす
243
- if v.nil? && k =~ /\A[0-9]+\z/
244
- opts['lineno'] = k.to_i
245
- next
246
- end
247
- #
248
- x = v ? "#{k}=#{v}" : k
249
- case k
250
- when 'fold', 'eolmark', 'foldmark', 'widecharfit'
251
- if vals.key?(v)
252
- opts[k] = vals[v]
253
- else
254
- error "#{yield}[#{x}]: expected 'on' or 'off'."
255
- end
256
- when 'lineno'
257
- if vals.key?(v)
258
- opts[k] = vals[v]
259
- elsif v =~ /\A\d+\z/
260
- opts[k] = v.to_i
261
- elsif v =~ /\A\d+-?\d*(?:\&+\d+-?\d*)*\z/
262
- opts[k] = v
263
- else
264
- error "#{yield}[#{x}]: expected line number pattern."
265
- end
266
- when 'linenowidth'
267
- if v =~ /\A-?\d+\z/
268
- opts[k] = v.to_i
269
- else
270
- error "#{yield}[#{x}]: expected integer value."
271
- end
272
- when 'fontsize'
273
- if v =~ /\A((x-|xx-)?small|(x-|xx-)?large)\z/
274
- opts[k] = v
275
- else
276
- error "#{yield}[#{x}]: expected small/x-small/xx-small."
277
- end
278
- when 'indent', 'indentwidth'
279
- if v =~ /\A\d+\z/
280
- k = 'indent'
281
- opts[k] = v.to_i
282
- elsif vals.key?(v)
283
- opts[k] = vals[v]
284
- else
285
- error "#{yield}[#{x}]: expected integer (>=0)."
286
- end
287
- when 'charspace'
288
- if v =~ /\A-?\d+(\.\d*)?\z/
289
- error "#{yield}[#{x}]: unit of measure required, such as '#{v}em'."
290
- elsif v =~ /\A-?\d+(\.\d*)?([a-zA-Z]+)?$/
291
- opts[k] = v
292
- else
293
- error "#{yield}[#{x}]: length expected (such as '0.04em')"
294
- end
295
- when 'lang'
296
- if v
297
- opts[k] = v
298
- else
299
- error "#{yield}[#{x}]: requires option value."
300
- end
301
- when 'file', 'encoding'
302
- v.present? or
303
- error "#{yield}[#{x}]: requires #{k} name."
304
- errmsg = __send__("_check_#{k}_option", v)
305
- error "#{yield}[#{x}]: #{errmsg}" if errmsg
306
- opts[k] = v
307
- when 'classname'
308
- v.present? or
309
- error "#{yield}[#{x}]: class name should not be empty."
310
- v =~ /\A[-\w]+\z/ or
311
- error "#{yield}[#{x}]: invalid class name."
312
- opts[k] = v
313
- else
314
- if i == 0 && v.nil?
315
- opts['lang'] = k # for compatibility with Re:VIEW
316
- else
317
- error "#{yield}[#{x}]: unknown option."
318
- end
319
- end
320
- end
321
- return opts
322
- end
323
-
324
- alias _parse_codeblock_optionstr _codeblock_parse_options
325
-
326
- def _check_file_option(file)
327
- File.exist?(file) or return "file not exist."
328
- File.file?(file) or return "not a file."
329
- File.readable?(file) or return "cannot read that file (maybe permission denied)."
330
- nil
331
- end
332
-
333
- def _check_encoding_option(encoding)
334
- Encoding.find(encoding) or return "unknown encoding."
335
- nil
336
- end
337
-
338
- def _codeblock_read_file(filename, enc)
339
- encoding = enc ? "#{enc}:utf-8" : "utf-8"
340
- lines = File.open(filename, encoding: encoding) {|f| f.to_a }
341
- return lines
342
- end
343
-
344
- def _codeblock_eolmark()
345
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
346
- end
347
-
348
- def _codeblock_indentmark()
349
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
350
- end
351
-
352
- def _render_codeblock(blockname, lines, id, caption_str, opts)
353
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
354
- end
355
-
356
- def _parse_inline(str, &b)
357
- en = enum_for(:_scan_inline_commands, str)
358
- buf = []
359
- _parse_inline_commands(en, buf, nil, &b)
360
- return buf.join()
361
- end
362
-
363
- def _scan_inline_commands(str)
364
- pos = 0
365
- str.scan(/(\@<\w+>[{|$])|(\\[}\\])|([}|$])/) do
366
- s1 = $1; s2 = $2; s3 = $3
367
- m = Regexp.last_match
368
- text = str[pos...m.begin(0)]
369
- pos = m.end(0)
370
- yield text, s1, s2, s3
371
- end
372
- remained = pos == 0 ? str : str[pos..-1]
373
- yield remained, nil, nil, nil
374
- end
375
-
376
- def _parse_inline_commands(en, buf, echar, &b)
377
- while true
378
- text, s1, s2, s3 = en.next()
379
- buf << (yield text) unless text.empty?
380
- if s1
381
- command = s1[2..-3]
382
- schar_ = s1[-1]
383
- echar_ = schar_ == '{' ? '}' : schar_
384
- if respond_to?("on_inline_#{command}")
385
- x = __send__("on_inline_#{command}") { "\0" }
386
- elsif respond_to?("inline_#{command}")
387
- x = __send__("inline_#{command}", "\0")
388
- else
389
- error "#{s1}#{echar_}: inline command not found."
390
- end
391
- stag, etag = x.split("\0", 2)
392
- buf << stag
393
- if _raw_inline_command?(command)
394
- errmsg = _parse_inline_commands(en, buf, echar_) {|text| text }
395
- else
396
- errmsg = _parse_inline_commands(en, buf, echar_, &b)
397
- end
398
- if errmsg
399
- error "#{s1}#{echar_}: #{errmsg}"
400
- end
401
- buf << etag
402
- elsif s2
403
- if echar == '}'
404
- buf << (yield s2[1..-1])
405
- else
406
- buf << (yield s2)
407
- end
408
- elsif s3
409
- if echar && echar == s3
410
- return
411
- else
412
- buf << (yield s3)
413
- end
414
- else
415
- echar.nil? or
416
- return "inline command not closed."
417
- return
418
- end
419
- end
420
- end
421
-
422
- def _raw_inline_command?(cmd)
423
- return cmd == 'm'
424
- end
425
-
426
- def _build_caption_str(id, caption)
427
- str = ""
428
- with_context(:caption) do
429
- str = compile_inline(caption) if caption.present?
430
- end
431
- if id.present?
432
- number = _build_caption_number(id)
433
- prefix = "#{I18n.t('list')}#{number}#{I18n.t('caption_prefix')}"
434
- str = "#{prefix}#{str}"
435
- end
436
- return str
437
- end
438
-
439
- def _build_caption_number(id)
440
- chapter = get_chap()
441
- number = @chapter.list(id).number
442
- return chapter.nil? \
443
- ? I18n.t('format_number_header_without_chapter', [number]) \
444
- : I18n.t('format_number_header', [chapter, number])
445
- rescue KeyError
446
- error "no such list: #{id}"
447
- end
448
-
449
- public
450
-
451
- ## テーブル
452
- def table(lines, id=nil, caption=nil, option=nil)
453
- opts = _table_options(option) { "//table[#{id}][#{caption}]" }
454
- rows = _table_parse(lines, opts) { "//table[#{id}][#{caption}]" }
455
- return if rows.empty?
456
- #
457
- opts[:pos] = 'H' if ! opts[:pos] && ! _positioning_allowed_here?()
458
- #
459
- header_rows, has_header = _table_split_header(rows, opts)
460
- rows = adjust_n_cols(rows)
461
- #
462
- if opts[:hline] != nil ; hline_default = opts[:hline]
463
- #elsif opts[:csv] ; hline_default = false
464
- else ; hline_default = true
465
- end
466
- header_ncols = opts[:headercols] || (has_header ? 0 : 1)
467
- #
468
- _table_before(id, caption, opts)
469
- table_header(id, caption, opts)
470
- table_begin(rows.first.length, fontsize: opts[:fontsize])
471
- _render_table_rows(rows, header_rows,
472
- header_ncols: header_ncols,
473
- hline_default: hline_default)
474
- _table_bottom(hline: !hline_default)
475
- table_end()
476
- _table_after(id, caption, opts)
477
- end
478
-
479
- def _table_parse(lines, opts, &b)
480
- content = nil
481
- if opts[:file]
482
- content = _table_readfile(opts[:file], encoding: opts[:encoding], &b)
483
- opts[:csv] ||= (opts[:file] =~ /\.csv\z/i)
484
- lines = content.each_line unless opts[:csv]
485
- else
486
- content = lines.join() if opts[:csv]
487
- end
488
- rows = opts[:csv] ? _table_parse_csv(content) \
489
- : _table_parse_txt(lines)
490
- return rows
491
- end
492
-
493
- def _table_parse_txt(lines)
494
- rows = []
495
- sepidx = nil
496
- rows = lines.map {|line|
497
- line.strip.split(/\t+/).map {|s| s.sub(/\A\./, '') }
498
- }
499
- return rows
500
- end
501
-
502
- def _table_parse_csv(csvstr)
503
- rows = CSV.parse(csvstr)
504
- return rows
505
- end
506
-
507
- def _table_readfile(filename, encoding: 'utf-8')
508
- _table_checkfile(filename)
509
- encoding ||= 'utf-8'
510
- Encoding.find(encoding) or
511
- error(yield + "[encoding=#{encoding}]: unknown encoding.")
512
- #
513
- return File.read(filename, encoding: "#{encoding}:UTF-8")
514
- end
515
-
516
- def _table_checkfile(filename)
517
- File.exist?(filename) or
518
- error(yield + "[file=#{filename}]: file not found.")
519
- File.file?(filename) or
520
- error(yield + "[file=#{filename}]: not a file.")
521
- File.readable?(filename) or
522
- error(yield + "[file=#{filename}]: file not readable (permission denied).")
523
- end
524
-
525
- def _table_split_header(rows, opts)
526
- has_header = true
527
- top_nlines = 3 # 最初の3行以内にヘッダ区切りがあるか調べる
528
- if opts[:headerrows]
529
- header_rows = rows.shift(opts[:headerrows])
530
- elsif (sepidx = _table_headerline_index(rows, top_nlines))
531
- header_rows = rows.shift(sepidx)
532
- rows.shift # ignore separator
533
- else
534
- header_rows = nil
535
- has_header = false
536
- end
537
- return header_rows, has_header
538
- end
539
-
540
- def _table_headerline_index(rows, top_nlines=3)
541
- top_nlines.times do |i|
542
- return i if rows[i] && _table_headerlinerow?(rows[i])
543
- end
544
- return nil
545
- end
546
-
547
- def _table_headerlinerow?(row)
548
- return false if row.empty?
549
- #return true if row.all? {|s| !s.present? }
550
- return true if _table_headerline?(row[0]) && \
551
- row[1..-1].all? {|s| !s.present? }
552
- return false
553
- end
554
-
555
- def _table_headerline?(line)
556
- return line =~ /\A={12,}\z/ || line =~ /\A-{12,}\z/
557
- end
558
-
559
- def _table_hline?(row)
560
- return true if row.empty?
561
- return true if row.all? {|s| !s.present? }
562
- #return true if row[0] =~ /\A-----+\z/ && \
563
- # row[1..-1].all? {|s| !s.present? }
564
- return false
565
- end
566
-
567
- def _render_table_rows(rows, header_rows=nil, header_ncols: 0, hline_default: true)
568
- if header_rows
569
- n = header_rows.length - 1
570
- header_rows.each_with_index do |row, i|
571
- cells = row.map {|s| th(compile_inline(s)) }
572
- puts _table_tr(cells, hline: n == i)
573
- end
574
- end
575
- flags = rows.map {|row| _table_hline?(row) || nil }
576
- rows.each_with_index do |row, i|
577
- next if flags[i] # skip '-----'
578
- cells = row.map {|s| compile_inline(s) }
579
- if header_ncols == 0
580
- cells = cells.map {|s| td(s) }
581
- elsif header_ncols == 1
582
- cells = [th(cells.shift)] + cells.map {|s| td(s) }
583
- else
584
- n = header_ncols
585
- cells = cells.shift(n).map {|s| th(s) } + cells.map {|s| td(s) }
586
- end
587
- hline = hline_default ? true : flags[i+1]
588
- puts _table_tr(cells, hline: hline)
589
- end
590
- end
591
-
592
- def _table_options(option_str, &b)
593
- opts = {}
594
- return opts unless option_str.present?
595
- _each_block_option(option_str) do |k, v|
596
- case k
597
- when 'file'
598
- v.present? or
599
- error(yield + "[#{k}=]: filename required.")
600
- opts[:file] = v
601
- when 'encoding'
602
- v.present? or
603
- error(yield + "[#{k}=]: encoding name required.")
604
- opts[:encoding] = v
605
- when 'csv', 'hline'
606
- sym = k.intern
607
- if ! v.present? ; opts[sym] = true
608
- elsif v == 'on' ; opts[sym] = true
609
- elsif v == 'off' ; opts[sym] = false
610
- else
611
- error(yield + "[#{k}=#{v}]: 'on' or 'off' expected.")
612
- end
613
- when 'pos'
614
- _validate_positioning_option(k, v, &b)
615
- opts[:pos] = v
616
- when 'fontsize'
617
- case v
618
- when 'small', 'x-small', 'xx-small'
619
- when 'large', 'x-large', 'xx-large'
620
- when 'medium'
621
- else
622
- error(yield + "[#{k}=#{v}]: invalid font size.")
623
- end
624
- opts[:fontsize] = v
625
- when 'headerrows', 'headercols'
626
- v =~ /\A\d+\z/ or
627
- error(yield + "[#{k}=#{v}]: integer expected.")
628
- opts[k.intern] = v.to_i
629
- else
630
- error(yield + "[#{k}=#{v}]: unknown option.")
631
- end
632
- end
633
- return opts
634
- end
635
-
636
- def _table_before(id, caption, opts)
637
- end
638
-
639
- def _table_after(id, caption, opts)
640
- end
641
-
642
- def _table_bottom(hline: false)
643
- nil
644
- end
645
-
646
- def _table_tr(cells, hline: false)
647
- tr(cells)
648
- end
649
-
650
- def _table_th(x)
651
- th(x)
652
- end
653
-
654
- def _table_td(x)
655
- td(x)
656
- end
657
-
658
- public
659
-
660
- ## ブロック命令を上書き
661
-
662
- def tsize(target, str=nil)
663
- ## //tsize[|latex||lcr|] ← Re:VIEW original
664
- ## //tsize[latex][|lcr|] ← Starter extended
665
- if str.nil?
666
- if target =~ /\A\|([^\|]*)\|(.*)/
667
- target, str = $1, $2
668
- else
669
- target, str = nil, target
670
- end
671
- end
672
- if target.nil? || target.empty? || target == '*'
673
- @tsize = str
674
- else
675
- builders = target.split(',').map {|x| x.strip }
676
- c = self.class.to_s.gsub('ReVIEW::', '').gsub('Builder', '').downcase
677
- @tsize = str if builders.include?(c)
678
- end
679
- end
680
-
681
- ## //imgtable
682
- def imgtable(lines, id, caption=nil, option=nil)
683
- @chapter.image(id).bound? or
684
- error "//imgtable[#{id}]: image not found."
685
- opts, metric = _imgtable_parse_options(option) { "//imgtable[#{id}][#{caption}]" }
686
- opts['pos'] = 'H' if ! opts['pos'] && ! _positioning_allowed_here?()
687
- _render_imgtable(id, caption, opts) do
688
- with_context(:caption) do
689
- _render_imgtable_caption(caption)
690
- end
691
- _render_imgtable_label(id)
692
- imgtable_image(id, caption, metric)
693
- end
694
- end
695
-
696
- def _imgtable_parse_options(option_str, &b)
697
- opts = {}; metric = []
698
- _each_block_option(option_str) do |k, v|
699
- case k
700
- when 'pos'
701
- _validate_positioning_option(k, v, &b)
702
- opts[k] = v
703
- else
704
- metric << (v ? "#{k}=#{v}" : k)
705
- end
706
- end
707
- return opts, metric.join()
708
- end
709
-
710
- def _render_imgtable(id, caption, opts)
711
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented.")
712
- end
713
-
714
- def _render_imgtable_caption(caption)
715
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented.")
716
- end
717
-
718
- def _render_imgtable_label(id)
719
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented.")
720
- end
721
-
722
- ## 章 (Chapter) の概要
723
- def on_abstract_block()
724
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented.")
725
- end
726
-
727
- ## 章 (Chapter) の著者
728
- def on_chapterauthor_block(name)
729
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented.")
730
- end
731
-
732
- ## 会話リスト
733
- def on_talklist_block(option=nil, &b)
734
- opts = _talklist_parse_options(option) { "//talklist" }
735
- _render_talklist(opts) do
736
- yield
737
- end
738
- end
739
-
740
- def _talklist_parse_options(option_str, &b)
741
- opts = {}
742
- _each_block_option(option_str) do |k, v|
743
- case k
744
- when 'indent'
745
- when 'imagewidth'
746
- when 'imageheight'
747
- when 'needvspace'
748
- when 'itemmargin'
749
- when 'listmargin'
750
- #
751
- when 'imageborder'
752
- case v
753
- when nil ; v = true
754
- when 'on' ; v = true
755
- when 'off' ; v = false
756
- else
757
- raise "#{yield}[#{k}=#{v}]: unexpected value; 'on' or 'off' expected."
758
- end
759
- #
760
- when 'separator', 'itemstart', 'itemend'
761
- case v
762
- when /\A"(.*)"\z/ ; v = $1
763
- when /\A'(.*)'\z/ ; v = $1
764
- end
765
- #
766
- when 'classname'
767
- else
768
- error "#{yield}[#{k}=#{v}]: unknown talklist option."
769
- end
770
- opts[k.intern] = v
771
- end
772
- return opts
773
- end
774
-
775
- ## 会話項目
776
- def on_talk_block(imagefile, name=nil, text=nil, &b)
777
- if imagefile.present?
778
- imgpath = find_image_filepath(imagefile) or
779
- error "//talk[#{imagefile}]: image file not found."
780
- else
781
- imgpath = nil
782
- end
783
- _render_talk(imgpath, name) do
784
- if text.nil?
785
- yield
786
- else
787
- puts ""
788
- puts compile_inline(text)
789
- end
790
- end
791
- end
792
-
793
- ## 会話項目(ショートカット用)
794
- def on_t_block(key, text=nil, &b)
795
- dict = self.config_starter['talk_shortcuts'] or
796
- error "//t[#{key}]: missing 'talk_shortcuts:' setting in 'config-starter.yml'."
797
- item = dict[key] or
798
- error "//t[#{key}]: key not registered in 'config-starter.yml'."
799
- on_talk_block(item['image'], item['name'], text, &b)
800
- end
801
-
802
- ## キーと説明文のリスト
803
- def on_desclist_block(option=nil, &b)
804
- opts = _desclist_parse_option(option) { "//desclist" }
805
- _render_desclist(opts) do
806
- yield
807
- end
808
- end
809
-
810
- def _desclist_parse_option(option_str)
811
- opts = {}
812
- return opts unless option_str.present?
813
- _each_block_option(option_str) do |k, v|
814
- case k
815
- when 'indent', 'space', 'listmargin', 'itemmargin'
816
- v.present? or
817
- error "[#{k}]: value required."
818
- when 'bold', 'compact'
819
- case v
820
- when nil, '' ; v = true
821
- when 'on' ; v = true
822
- when 'off' ; v = false
823
- else
824
- error yield + "[#{k}=#{v}]: expected 'on' or 'off'."
825
- end
826
- when 'classname'
827
- else
828
- error yield + "[#{k}]: unknown option."
829
- end
830
- opts[k.intern] = v
831
- end
832
- return opts
833
- end
834
-
835
- ## キーと値
836
- def on_desc_block(key, text=nil, &b)
837
- _render_desc(key) do
838
- if text.nil?
839
- yield
840
- else
841
- #puts ""
842
- puts compile_inline(text)
843
- end
844
- end
845
- end
846
-
847
- ## 縦方向の空きを入れる
848
- def vspace(target, size)
849
- if _current_builder?(target)
850
- _render_vspace(size)
851
- end
852
- end
853
-
854
- def addvspace(target, size)
855
- if _current_builder?(target)
856
- _render_addvspace(size)
857
- end
858
- end
859
-
860
- def _current_builder?(target)
861
- return true if target.nil? || target.empty?
862
- return true if target == '*'
863
- c = self.class.to_s.gsub('ReVIEW::', '').gsub('Builder', '').downcase
864
- return target.split(',').any? {|x| x.strip == c }
865
- end
866
-
867
- ## 縦方向のスペースがなければ改ページ
868
- def needvspace(target_name, height)
869
- if _current_target?(target_name)
870
- _render_needvspace(height)
871
- end
872
- end
873
-
874
- ## 生コード埋め込み
875
- def embed(lines, target=nil)
876
- yes = _current_target?(target)
877
- puts lines.join() if yes
878
- end
879
-
880
- ## インライン命令のバグ修正
881
-
882
- def inline_raw(str)
883
- name = target_name()
884
- if str =~ /\A\|(.*?)\|/
885
- str = $'
886
- return "" unless $1.split(',').any? {|s| s.strip == name }
887
- end
888
- return str.gsub('\\n', "\n")
889
- end
890
-
891
- def inline_embed(str)
892
- name = target_name()
893
- if str =~ /\A\|(.*?)\|/
894
- str = $'
895
- return "" unless $1.split(',').any? {|s| s.strip == name }
896
- end
897
- return str
898
- end
899
-
900
- ## インライン命令を入れ子に対応させる
901
-
902
- def on_inline_href
903
- escaped_str, items = yield true
904
- url = label = nil
905
- separator1 = /, /
906
- separator2 = /(?<=[^\\}]),/ # 「\\,」はセパレータと見なさない
907
- [separator1, separator2].each do |sep|
908
- pair = items[0].split(sep, 2)
909
- if pair.length == 2
910
- url = pair[0]
911
- label = escaped_str.split(sep, 2)[-1] # 「,」がエスケープされない前提
912
- break
913
- end
914
- end
915
- url ||= items[0]
916
- url = url.gsub(/\\,/, ',') # 「\\,」を「,」に置換
917
- return build_inline_href(url, label)
918
- end
919
-
920
- def on_inline_ruby
921
- escaped_str = yield
922
- arr = escaped_str.split(', ')
923
- if arr.length > 1 # ex: @<ruby>{小鳥遊, たかなし}
924
- yomi = arr.pop().strip()
925
- word = arr.join(', ')
926
- elsif escaped_str =~ /,([^,]*)\z/ # ex: @<ruby>{小鳥遊,たかなし}
927
- word, yomi = $`, $1.strip()
928
- else
929
- error "@<ruby>: missing yomi, should be '@<ruby>{word, yomi}' style."
930
- end
931
- return build_inline_ruby(word, yomi)
932
- end
933
-
934
- ## 節 (Section) や項 (Subsection) を参照する。
935
- ## 引数 id が節や項のラベルでないなら、エラー。
936
- ## 使い方: @<secref>{label}
937
- def inline_secref(id) # 参考:ReVIEW::Builder#inline_hd(id)
938
- ## 本来、こういった処理はParserで行うべきであり、Builderで行うのはおかしい。
939
- ## しかしRe:VIEWのアーキテクチャがよくないせいで、こうせざるを得ない。無念。
940
- sec_id = id
941
- chapter = nil
942
- if id =~ /\A([^|]+)\|(.+)/
943
- chap_id = $1; sec_id = $2
944
- chapter = @book.contents.detect {|chap| chap.id == chap_id } or
945
- error "@<secref>{#{id}}: chapter '#{chap_id}' not found."
946
- end
947
- begin
948
- _inline_secref(chapter || @chapter, sec_id)
949
- rescue KeyError
950
- error "@<secref>{#{id}}: section (or subsection) not found."
951
- end
952
- end
953
-
954
- private
955
-
956
- def _inline_secref(chap, id)
957
- sec_id = chap.headline(id).id
958
- num, title = _get_secinfo(chap, sec_id)
959
- level = num.split('.').length
960
- #
961
- secnolevel = @book.config['secnolevel']
962
- if secnolevel + 1 < level
963
- error "'secnolevel: #{secnolevel}' should be >= #{level-1} in config.yml"
964
- ## config.ymlの「secnolevel:」が3以上なら、項 (Subsection) にも
965
- ## 番号がつく。なので、節 (Section) のタイトルは必要ない。
966
- elsif secnolevel + 1 > level
967
- parent_title = nil
968
- ## そうではない場合は、節 (Section) と項 (Subsection) とを組み合わせる。
969
- ## たとえば、"「1.1 イントロダクション」内の「はじめに」" のように。
970
- elsif secnolevel + 1 == level
971
- parent_id = sec_id.sub(/\|[^|]+\z/, '')
972
- _, parent_title = _get_secinfo(chap, parent_id)
973
- else
974
- raise "not reachable"
975
- end
976
- #
977
- return _build_secref(chap, num, title, parent_title)
978
- end
979
-
980
- def _get_secinfo(chap, id) # 参考:ReVIEW::LATEXBuilder#inline_hd_chap()
981
- num = chap.headline_index.number(id)
982
- caption = compile_inline(chap.headline(id).caption)
983
- if chap.number && @book.config['secnolevel'] >= num.split('.').size
984
- caption = "#{chap.headline_index.number(id)} #{caption}"
985
- end
986
- #title = I18n.t('chapter_quote', caption)
987
- title = caption
988
- return num, title
989
- end
990
-
991
- def _build_secref(chap, num, title, parent_title)
992
- raise NotImplementedError.new("#{self.class.name}#_build_secref(): not implemented yet.")
993
- end
994
-
995
- ##
996
-
997
- public
998
-
999
- ## ノートを参照する
1000
- def inline_noteref(label)
1001
- begin
1002
- chapter, label = parse_reflabel(label)
1003
- rescue KeyError => ex
1004
- error "@<noteref>{#{label}}: #{ex.message}"
1005
- end
1006
- begin
1007
- item = (chapter || @chapter).note(label)
1008
- rescue KeyError => ex
1009
- error "@<noteref>{#{label}}: note not found."
1010
- end
1011
- build_noteref(chapter, label, item.caption)
1012
- end
1013
-
1014
- ## 数式を参照する
1015
- def inline_eq(label)
1016
- begin
1017
- chapter, label = parse_reflabel(label)
1018
- rescue KeyError => ex
1019
- error "@<eq>{#{label}}: #{ex.message}"
1020
- end
1021
- begin
1022
- item = (chapter || @chapter).equation(label)
1023
- rescue KeyError => ex
1024
- error "@<eq>{#{label}}: equation not found."
1025
- end
1026
- build_eq(chapter || @chapter, label, item.number)
1027
- end
1028
-
1029
- protected
1030
-
1031
- def build_noteref(chapter, label, caption)
1032
- raise NotImplementedError.new("#{self.class.name}#build_noteref(): not implemented yet.")
1033
- end
1034
-
1035
- def build_eq(chapter, label, caption)
1036
- raise NotImplementedError.new("#{self.class.name}#build_noteref(): not implemented yet.")
1037
- end
1038
-
1039
- def parse_reflabel(label)
1040
- ## 本来ならParserで行うべきだけど仕方ない。
1041
- chapter = nil
1042
- if label =~ /\A([^|]+)\|(.+)/
1043
- chap_id = $1; label = $2
1044
- chapter = @book.contents.detect {|chap| chap.id == chap_id } or
1045
- raise KeyError.new("chapter '#{chap_id}' not found.")
1046
- return chapter, label
1047
- end
1048
- return chapter, label
1049
- end
1050
-
1051
- ##
1052
-
1053
- protected
1054
-
1055
- def find_image_filepath(image_id)
1056
- finder = get_image_finder()
1057
- filepath = finder.find_path(image_id)
1058
- return filepath
1059
- end
1060
-
1061
- def get_image_finder()
1062
- imagedir = "#{@book.basedir}/#{@book.config['imagedir']}"
1063
- types = @book.image_types
1064
- builder = @book.config['builder']
1065
- chap_id = @chapter.id
1066
- return ReVIEW::Book::ImageFinder.new(imagedir, chap_id, builder, types)
1067
- end
1068
-
1069
- public
1070
-
1071
- ## 画像ファイルが見つからなければエラーとするよう変更
1072
- def image(lines, id, caption, option=nil)
1073
- image = @chapter.image(id)
1074
- image.bound? or
1075
- error "//image[#{id}]: image not found."
1076
- image_filepath = image.path
1077
- opts = _image_parse_options(option) { "//image[#{id}][#{caption}]" }
1078
- opts[:pos] = 'H' if ! opts[:pos] && ! _positioning_allowed_here?()
1079
- _render_image(id, image.path, caption, opts)
1080
- end
1081
-
1082
- def _render_image(id, image_filepath, caption, opts)
1083
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
1084
- end
1085
-
1086
- def _image_parse_options(option_str, &b)
1087
- opts = {}
1088
- _each_block_option(option_str) do |k, v|
1089
- case k
1090
- when 'pos'
1091
- _validate_positioning_option(k, v, &b)
1092
- when 'border', 'draft'
1093
- case v
1094
- when nil ; v = true
1095
- when 'on' ; v = true
1096
- when 'off'; v = false
1097
- else
1098
- error "#{yield}[#{k}=#{v}]: expected '#{k}=on' or '#{k}=off'"
1099
- end
1100
- when 'scale'
1101
- case v
1102
- when /\A\d+\.\d*\z/, /\A\.\d+\z/
1103
- when /\A\d+(\.\d+)?%\z/
1104
- else
1105
- error "#{yield}[#{k}=#{v}]: invalid scale value."
1106
- end
1107
- when 'width'
1108
- case v
1109
- when /\A[\d.]+\z/
1110
- error "#{yield}[#{k}=#{v}]: unit of measure required (for example: 80%, 200px, 50mm)."
1111
- end
1112
- when 'style'
1113
- if v =~ /\A"(.*?)"\z/ || v =~ /\A'(.*?)'\z/
1114
- v = $1
1115
- end
1116
- when 'class'
1117
- else
1118
- error "#{yield}[#{k}=#{v}]: unknown image option."
1119
- end
1120
- opts[k.intern] = v
1121
- end
1122
- return opts
1123
- end
1124
-
1125
- def _validate_positioning_option(k, v, &b)
1126
- v.present? or
1127
- error "#{yield}[#{k}=]: position char required."
1128
- _positioning_allowed_here?() || v == 'H' or
1129
- error "#{yield}[#{k}=#{v}]: positioning not allowed here; please remove `pos=#{v}' option, or use `pos=H' instead."
1130
- v =~ /\A[hHtbp]+\z/ or # H: Here, h: here, t: top, b: bottom, p: page
1131
- error "#{yield}[#{k}=#{v}]: contains invalid char (availables: 'h', 'H', 't', 'b', 'p', or combination of them)."
1132
- end
1133
-
1134
- def _positioning_allowed_here?
1135
- return true if within_context?(:note) # //note の中はOK
1136
- return false if within_context?(:minicolumn) # //info や //caution の中はダメ
1137
- return false if within_context?(:column) # ==[column] の中はダメ
1138
- return true # それ以外はOK
1139
- end
1140
-
1141
- ## LaTeXでは「``」と「''」で囲み、HTMLでは「“」と「”」で囲む
1142
- def on_inline_qq()
1143
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented.")
1144
- end
1145
-
1146
- ## 索引に載せる用語(@<term>{}, @<idx>{}, @<hidx>{})
1147
- def inline_idx(str)
1148
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
1149
- end
1150
- def inline_hidx(str)
1151
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
1152
- end
1153
- def inline_term(str)
1154
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
1155
- end
1156
- def inline_termnoidx(str)
1157
- raise NotImplementedError.new("#{self.class.name}##{__method__}(): not implemented yet.")
1158
- end
1159
- def parse_term(str, placeholder_str)
1160
- @terms_dict ||= {} # key: term, value: yomigana
1161
- #
1162
- see = nil
1163
- if str =~ %r`==>>(.*?)\z`
1164
- str = $`
1165
- see = $1
1166
- end
1167
- #
1168
- display_str = ""
1169
- tmpchar = "\x08"
1170
- placeholder_rexp = TERM_PLACEHOLDER_REXP # /---/
1171
- str.split('<<>>').each do |item|
1172
- if item =~ /\(\((.*?)\)\)\z/
1173
- term = $`
1174
- yomi = $1
1175
- @terms_dict[term] = yomi # may override existing key
1176
- elsif @terms_dict[item]
1177
- term = item
1178
- yomi = @terms_dict[item]
1179
- else
1180
- term = item
1181
- yomi = _find_yomi(term)
1182
- @terms_dict[term] = yomi
1183
- end
1184
- if ! display_str.empty? && term =~ placeholder_rexp
1185
- display_str = compile_inline(term.gsub(placeholder_rexp, tmpchar)).gsub(tmpchar, display_str)
1186
- else
1187
- display_str += compile_inline(term)
1188
- end
1189
- term_e = compile_inline(term.gsub(placeholder_rexp, tmpchar))
1190
- term_e = escape_index(term_e).gsub(tmpchar, placeholder_str)
1191
- yield term, term_e, yomi
1192
- end
1193
- return display_str, see
1194
- end
1195
- protected :parse_term
1196
-
1197
- TERM_PLACEHOLDER_REXP = /---/
1198
-
1199
- def _find_yomi(term)
1200
- if @index_db
1201
- yomi = @index_db[term]
1202
- return yomi if yomi
1203
- end
1204
- return nil if term =~ /\A[[:ascii:]]+\Z/ || @index_mecab.nil?
1205
- return _to_yomigana(term)
1206
- end
1207
- private :_find_yomi
1208
-
1209
- def _to_yomigana(term)
1210
- return NKF.nkf('-w --hiragana', @index_mecab.parse(term).force_encoding('UTF-8').chomp)
1211
- end
1212
- private :_to_yomigana
1213
-
1214
- def escape_index(str)
1215
- str
1216
- end
1217
-
1218
- ## キーを単語へ展開する(@<w>{}, @<wb>{}, @<W>{})
1219
- def inline_w(str)
1220
- key = str
1221
- @words_dict ||= _load_words_file(@book.config['words_file'], key)
1222
- unless @words_dict.key?(key)
1223
- filenames = [@book.config['words_file']].flatten
1224
- error "@<w>{#{key}}: key '#{key}' not found in words file (#{filenames.join(', ')})."
1225
- end
1226
- return escape(@words_dict[key])
1227
- end
1228
-
1229
- def inline_wb(str)
1230
- inline_b(inline_w(str))
1231
- end
1232
-
1233
- def inline_W(str)
1234
- inline_strong(inline_w(str))
1235
- end
1236
-
1237
- private
1238
-
1239
- def _load_words_file(words_files, key)
1240
- words_files.present? or
1241
- error "`@<w>{#{key}}`: `words_file:` not configured in config.yml."
1242
- filepaths = [words_files].flatten.compact
1243
- filepaths.each do |filepath|
1244
- _validate_words_file(filepath, key)
1245
- end
1246
- #
1247
- dict = {}
1248
- filepaths.each do |filepath|
1249
- case filepath
1250
- when /\.csv/i
1251
- _load_words_file_csv(filepath, dict)
1252
- when /\.tsv/i, /\.txt/i
1253
- _load_words_file_txt(filepath, dict)
1254
- else
1255
- raise "uneachable: filepath=#{filepath.inspect}"
1256
- end
1257
- end
1258
- return dict
1259
- end
1260
-
1261
- def _validate_words_file(filepath, key)
1262
- File.exist?(filepath) or
1263
- error "`@<w>{#{key}}`: words file `#{filepath}` not found."
1264
- File.file?(filepath) or
1265
- error "`@<w>{#{key}}`: words file `#{filepath}` should be a file, but not."
1266
- File.readable?(filepath) or
1267
- error "`@<w>{#{key}}`: cannot read words file `#{filepath}`."
1268
- filepath =~ /\.(csv|tsv|txt)/i or
1269
- error "`@<w>{#{key}}`: words file `#{filepath}` should be *.csv, *.tsv, or *.txt."
1270
- end
1271
-
1272
- def _load_words_file_csv(filepath, dict)
1273
- require 'csv'
1274
- CSV.read(filepath, :encoding=>'utf-8').each do |row|
1275
- next if row.length < 2
1276
- key, val, = row
1277
- dict[key] = val if key.present? && val.present?
1278
- end
1279
- return dict
1280
- end
1281
-
1282
- def _load_words_file_txt(filepath, dict)
1283
- File.open(filepath, :encoding=>'utf-8') do |f|
1284
- f.each do |line|
1285
- next if line =~ /\A\#/
1286
- key, val, = line.chomp.split(/\t+/)
1287
- dict[key] = val if key.present? && val.present?
1288
- end
1289
- end
1290
- return dict
1291
- end
1292
-
1293
- end
1294
-
1295
-
1296
- ##
1297
- ## 行番号を生成するクラス。
1298
- ##
1299
- ## gen = LineNumberGenerator.new("1-3&8-10&15-")
1300
- ## p gen.each.take(15).to_a
1301
- ## #=> [1, 2, 3, nil, 8, 9, 10, nil, 15, 16, 17, 18, 19, 20, 21]
1302
- ##
1303
- class LineNumberGenerator
1304
-
1305
- def initialize(arg)
1306
- @ranges = []
1307
- inf = Float::INFINITY
1308
- case arg
1309
- when true ; @ranges << (1 .. inf)
1310
- when Integer ; @ranges << (arg .. inf)
1311
- when /\A(\d+)\z/ ; @ranges << (arg.to_i .. inf)
1312
- else
1313
- arg.split('&', -1).each do |str|
1314
- case str
1315
- when /\A\z/
1316
- @ranges << nil
1317
- when /\A(\d+)\z/
1318
- @ranges << ($1.to_i .. $1.to_i)
1319
- when /\A(\d+)\-(\d+)?\z/
1320
- start = $1.to_i
1321
- end_ = $2 ? $2.to_i : inf
1322
- @ranges << (start..end_)
1323
- else
1324
- raise ArgumentError.new("'#{arg}': invalid lineno format")
1325
- end
1326
- end
1327
- end
1328
- end
1329
-
1330
- def each(&block)
1331
- return enum_for(:each) unless block_given?
1332
- for range in @ranges
1333
- range.each(&block) if range
1334
- yield nil
1335
- end
1336
- nil
1337
- end
1338
-
1339
- end
1340
-
1341
-
1342
- end