review-retrovert 0.9.7 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +9 -0
  3. data/.editorconfig +3 -0
  4. data/.github/workflows/retrovert.yml +20 -5
  5. data/.gitignore +1 -0
  6. data/.vscode/launch.json +48 -0
  7. data/Gemfile.lock +13 -4
  8. data/Makefile +24 -0
  9. data/docker/review.Dockerfile +10 -0
  10. data/lib/review/retrovert/converter.rb +374 -111
  11. data/lib/review/retrovert/reviewcompat.rb +57 -0
  12. data/lib/review/retrovert/reviewdef.rb +70 -0
  13. data/lib/review/retrovert/sty/{ird.sty → ird.sty.erb} +9 -1
  14. data/lib/review/retrovert/sty/review-retrovert-custom.sty.erb +6 -0
  15. data/lib/review/retrovert/utils.rb +35 -0
  16. data/lib/review/retrovert/version.rb +1 -1
  17. data/lib/review/retrovert/yamlconfig.rb +0 -1
  18. data/package-lock.json +6 -0
  19. data/package.json +1 -0
  20. data/review-retrovert.gemspec +5 -1
  21. data/testdata/mybook/.gitignore +3 -1
  22. data/testdata/mybook/.textlintrc +11 -0
  23. data/testdata/mybook/Rakefile +8 -0
  24. data/testdata/mybook/catalog.yml +36 -10
  25. data/testdata/mybook/config-starter.yml +134 -9
  26. data/testdata/mybook/config.yml +44 -15
  27. data/testdata/mybook/contents/00-preface.re +48 -2
  28. data/testdata/mybook/contents/01-install.re +38 -5
  29. data/testdata/mybook/contents/02-tutorial.re +333 -113
  30. data/testdata/mybook/contents/03-syntax.re +2370 -373
  31. data/testdata/mybook/contents/04-customize.re +424 -88
  32. data/testdata/mybook/contents/05-faq.re +288 -10
  33. data/testdata/mybook/contents/06-bestpractice.re +431 -59
  34. data/testdata/mybook/contents/91-compare.re +402 -2
  35. data/testdata/mybook/contents/92-filelist.re +14 -8
  36. data/testdata/mybook/contents/93-background.re +10 -10
  37. data/testdata/mybook/contents/99-postface.re +2 -1
  38. data/testdata/mybook/contents/r0-root.re +1 -1
  39. data/testdata/mybook/css/webstyle.css +180 -2
  40. data/testdata/mybook/data/terms.txt +3 -0
  41. data/testdata/mybook/data/words.txt +15 -0
  42. data/testdata/mybook/images/03-syntax/index-page.png +0 -0
  43. data/testdata/mybook/images/03-syntax/order-detail.png +0 -0
  44. data/testdata/mybook/images/04-customize/section_decoration_samples.png +0 -0
  45. data/testdata/mybook/images/05-faq/dummy-image.png +0 -0
  46. data/testdata/mybook/images/06-bestpractice/section_title_wlines.png +0 -0
  47. data/testdata/mybook/images/avatar-b.png +0 -0
  48. data/testdata/mybook/images/avatar-g.png +0 -0
  49. data/testdata/mybook/images/avatar-r.png +0 -0
  50. data/testdata/mybook/images/caution-icon.png +0 -0
  51. data/testdata/mybook/images/info-icon.png +0 -0
  52. data/testdata/mybook/images/warning-icon.png +0 -0
  53. data/testdata/mybook/layouts/layout.html5.erb +3 -1
  54. data/testdata/mybook/layouts/layout.tex.erb +265 -379
  55. data/testdata/mybook/layouts/layout.tex.erb.orig +386 -0
  56. data/testdata/mybook/lib/ruby/review-book.rb +64 -0
  57. data/testdata/mybook/lib/ruby/review-builder.rb +902 -63
  58. data/testdata/mybook/lib/ruby/review-compiler.rb +675 -22
  59. data/testdata/mybook/lib/ruby/review-epubbuilder.rb +33 -0
  60. data/testdata/mybook/lib/ruby/review-epubmaker.rb +10 -7
  61. data/testdata/mybook/lib/ruby/review-htmlbuilder.rb +354 -66
  62. data/testdata/mybook/lib/ruby/review-latexbuilder.rb +429 -146
  63. data/testdata/mybook/lib/ruby/review-maker.rb +117 -6
  64. data/testdata/mybook/lib/ruby/review-markdownbuilder.rb +945 -0
  65. data/testdata/mybook/lib/ruby/review-markdownmaker.rb +91 -0
  66. data/testdata/mybook/lib/ruby/review-monkeypatch.rb +2 -0
  67. data/testdata/mybook/lib/ruby/review-pdfmaker.rb +160 -82
  68. data/testdata/mybook/lib/ruby/review-webmaker.rb +20 -5
  69. data/testdata/mybook/lib/tasks/review.rake +14 -0
  70. data/testdata/mybook/lib/tasks/starter.rake +148 -4
  71. data/testdata/mybook/sty/indexstyle.ist +25 -0
  72. data/testdata/mybook/sty/mytextsize.sty +29 -0
  73. data/testdata/mybook/sty/mytitlepage.sty +34 -11
  74. data/testdata/mybook/sty/review-base.sty +276 -0
  75. data/testdata/mybook/sty/starter-codeblock.sty +237 -106
  76. data/testdata/mybook/sty/starter-color.sty +72 -17
  77. data/testdata/mybook/sty/starter-heading.sty +60 -13
  78. data/testdata/mybook/sty/starter-misc.sty +894 -0
  79. data/testdata/mybook/sty/starter-note.sty +67 -14
  80. data/testdata/mybook/sty/starter-talklist.sty +105 -0
  81. data/testdata/mybook/sty/starter-util.sty +35 -0
  82. data/testdata/mybook/sty/starter.sty +8 -526
  83. metadata +80 -4
@@ -4,6 +4,8 @@
4
4
  ### ReVIEW::Builderクラスを拡張する
5
5
  ###
6
6
 
7
+ require 'csv'
8
+
7
9
  require 'review/builder'
8
10
 
9
11
 
@@ -14,6 +16,22 @@ module ReVIEW
14
16
 
15
17
  class Builder
16
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
+
17
35
  ## Re:VIEW3で追加されたもの
18
36
  def on_inline_balloon(arg)
19
37
  return "← #{yield}"
@@ -27,6 +45,12 @@ module ReVIEW
27
45
  def ol_item_end()
28
46
  end
29
47
 
48
+ def dl_dd_begin()
49
+ end
50
+
51
+ def dl_dd_end()
52
+ end
53
+
30
54
  protected
31
55
 
32
56
  def truncate_if_endwith?(str)
@@ -41,6 +65,18 @@ module ReVIEW
41
65
  end
42
66
  end
43
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
+
44
80
  def enter_context(key)
45
81
  @doc_status[key] = true
46
82
  end
@@ -62,7 +98,7 @@ module ReVIEW
62
98
 
63
99
  def within_codeblock?
64
100
  d = @doc_status
65
- d[:program] || d[:terminal] \
101
+ d[:program] || d[:terminal] || d[:output]\
66
102
  || d[:list] || d[:emlist] || d[:listnum] || d[:emlistnum] \
67
103
  || d[:cmd] || d[:source]
68
104
  end
@@ -85,112 +121,137 @@ module ReVIEW
85
121
  end
86
122
  protected :on_minicolumn
87
123
 
124
+ ## 画像横に文章
125
+
88
126
  def on_sideimage_block(imagefile, imagewidth, option_str=nil, &b)
89
- raise NotImplementedError.new("#{self.class.name}#on_sideimage_block(): not implemented yet.")
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.")
90
135
  end
91
136
 
92
- def validate_sideimage_args(imagefile, imagewidth, option_str)
137
+ def _sideimage_parse_options(option_str, &b)
93
138
  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|
139
+ _each_block_option(option_str) do |k, v|
105
140
  case k
106
141
  when 'side'
107
142
  v == 'L' || v == 'R' or
108
- error "//sideimage: [#{option_str}]: 'side=' should be 'L' or 'R'."
143
+ error "#{yield}[#{k}=#{v}]: 'side=' should be 'L' or 'R'."
109
144
  when 'boxwidth'
110
145
  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)"
146
+ error "#{yield}[#{k}=#{v}]: 'boxwidth=' invalid (expected such as 10%, 30mm, 3.0cm, or 5zw)"
112
147
  when 'sep'
113
148
  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)"
149
+ error "#{yield}[#{k}=#{v}]: 'sep=' invalid (expected such as 2%, 5mm, 0.5cm, or 1zw)"
115
150
  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
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'
119
154
  else
120
- error "//sideimage: [#{option_str}]: unknown option '#{k}=#{v}'."
155
+ error "#{yield}[#{k}=#{v}]: unknown option '#{k}=#{v}'."
121
156
  end
157
+ opts[k] = v
122
158
  end
123
- #
159
+ return opts
160
+ end
161
+
162
+ def _sideimage_validate_args(imagefile, imagewidth)
124
163
  imagefile.present? or
125
164
  error "//sideimage: 1st option (image file) required."
126
165
  imagewidth.present? or
127
166
  error "//sideimage: 2nd option (image width) required."
128
167
  imagewidth =~ /\A\d+(\.\d+)?(%|mm|cm|zw|pt)\z/ or
129
168
  error "//sideimage: [#{imagewidth}]: invalid image width (expected such as: 30mm, 3.0cm, 5zw, or 100pt)"
130
- #
131
- return imagefile, imagewidth, opts
132
169
  end
133
- protected :validate_sideimage_args
134
-
135
- ## コードブロック(//program, //terminal)
136
170
 
137
- CODEBLOCK_OPTIONS = {
138
- 'fold' => true,
139
- 'lineno' => false,
140
- 'linenowidth' => -1,
141
- 'eolmark' => false,
142
- 'foldmark' => true,
143
- 'lang' => nil,
144
- }
171
+ ## コードブロック(//program, //terminal, //output)
145
172
 
146
173
  ## プログラム用ブロック命令
147
- ## ・//list と似ているが、長い行を自動的に折り返す
148
- ## ・seqsplit.styの「\seqsplit{...}」コマンドを使っている
149
174
  def program(lines, id=nil, caption=nil, optionstr=nil)
150
175
  _codeblock('program', lines, id, caption, optionstr)
151
176
  end
152
177
 
153
178
  ## ターミナル用ブロック命令
154
- ## ・//cmd と似ているが、長い行を自動的に折り返す
155
- ## ・seqsplit.styの「\seqsplit{...}」コマンドを使っている
156
179
  def terminal(lines, id=nil, caption=nil, optionstr=nil)
157
180
  _codeblock('terminal', lines, id, caption, optionstr)
158
181
  end
159
182
 
183
+ ## 出力結果用ブロック命令
184
+ def output(lines, id=nil, caption=nil, optionstr=nil)
185
+ _codeblock('output', lines, id, caption, optionstr)
186
+ end
187
+
160
188
  protected
161
189
 
162
190
  def _codeblock(blockname, lines, id, caption, optionstr)
163
- raise NotImplementedError.new("#{self.class.name}#_codeblock(): not implemented yet.")
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']
164
217
  end
165
218
 
166
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
167
224
  option_str.split(',').each do |kvs|
168
- k, v = kvs.split('=', 2)
225
+ k, v = kvs.strip.split('=', 2)
226
+ if k =~ /::?/
227
+ t = $`; k = $'
228
+ b ||= target_name()
229
+ next unless t == b
230
+ end
169
231
  yield k, v
170
- end if option_str && !option_str.empty?
232
+ end
171
233
  end
172
234
 
173
- def _parse_codeblock_optionstr(optionstr, blockname) # parse 'fold={on|off},...'
235
+ def _codeblock_parse_options(option_str, &b) # parse 'fold={on|off},...'
174
236
  opts = {}
175
- return opts if optionstr.nil? || optionstr.empty?
237
+ return opts if option_str.nil? || option_str.empty?
176
238
  vals = {nil=>true, 'on'=>true, 'off'=>false}
177
- optionstr.split(',').each_with_index do |x, i|
178
- x = x.strip()
239
+ i = -1
240
+ _each_block_option(option_str) do |k, v|
241
+ i += 1
179
242
  ## //list[][][1] は //list[][][lineno=1] とみなす
180
- if x =~ /\A[0-9]+\z/
181
- opts['lineno'] = x.to_i
243
+ if v.nil? && k =~ /\A[0-9]+\z/
244
+ opts['lineno'] = k.to_i
182
245
  next
183
246
  end
184
247
  #
185
- x =~ /\A([-\w]+)(?:=(.*))?\z/ or
186
- raise "//#{blockname}[][][#{x}]: invalid option format."
187
- k, v = $1, $2
248
+ x = v ? "#{k}=#{v}" : k
188
249
  case k
189
- when 'fold', 'eolmark', 'foldmark'
250
+ when 'fold', 'eolmark', 'foldmark', 'widecharfit'
190
251
  if vals.key?(v)
191
252
  opts[k] = vals[v]
192
253
  else
193
- raise "//#{blockname}[][][#{x}]: expected 'on' or 'off'."
254
+ error "#{yield}[#{x}]: expected 'on' or 'off'."
194
255
  end
195
256
  when 'lineno'
196
257
  if vals.key?(v)
@@ -200,43 +261,168 @@ module ReVIEW
200
261
  elsif v =~ /\A\d+-?\d*(?:\&+\d+-?\d*)*\z/
201
262
  opts[k] = v
202
263
  else
203
- raise "//#{blockname}[][][#{x}]: expected line number pattern."
264
+ error "#{yield}[#{x}]: expected line number pattern."
204
265
  end
205
266
  when 'linenowidth'
206
267
  if v =~ /\A-?\d+\z/
207
268
  opts[k] = v.to_i
208
269
  else
209
- raise "//#{blockname}[][][#{x}]: expected integer value."
270
+ error "#{yield}[#{x}]: expected integer value."
210
271
  end
211
272
  when 'fontsize'
212
273
  if v =~ /\A((x-|xx-)?small|(x-|xx-)?large)\z/
213
274
  opts[k] = v
214
275
  else
215
- raise "//#{blockname}[][][#{x}]: expected small/x-small/xx-small."
276
+ error "#{yield}[#{x}]: expected small/x-small/xx-small."
216
277
  end
217
- when 'indentwidth'
278
+ when 'indent', 'indentwidth'
218
279
  if v =~ /\A\d+\z/
280
+ k = 'indent'
219
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
220
292
  else
221
- raise "//#{blockname}[][][#{x}]: expected integer (>=0)."
293
+ error "#{yield}[#{x}]: length expected (such as '0.04em')"
222
294
  end
223
295
  when 'lang'
224
296
  if v
225
297
  opts[k] = v
226
298
  else
227
- raise "//#{blockname}[][][#{x}]: requires option value."
299
+ error "#{yield}[#{x}]: requires option value."
228
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
229
313
  else
230
- if i == 0
231
- opts['lang'] = v
314
+ if i == 0 && v.nil?
315
+ opts['lang'] = k # for compatibility with Re:VIEW
232
316
  else
233
- raise "//#{blockname}[][][#{x}]: unknown option."
317
+ error "#{yield}[#{x}]: unknown option."
234
318
  end
235
319
  end
236
320
  end
237
321
  return opts
238
322
  end
239
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
+
240
426
  def _build_caption_str(id, caption)
241
427
  str = ""
242
428
  with_context(:caption) do
@@ -262,6 +448,435 @@ module ReVIEW
262
448
 
263
449
  public
264
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
+
265
880
  ## インライン命令のバグ修正
266
881
 
267
882
  def inline_raw(str)
@@ -451,6 +1066,230 @@ module ReVIEW
451
1066
  return ReVIEW::Book::ImageFinder.new(imagedir, chap_id, builder, types)
452
1067
  end
453
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
+
454
1293
  end
455
1294
 
456
1295
 
@@ -482,7 +1321,7 @@ module ReVIEW
482
1321
  end_ = $2 ? $2.to_i : inf
483
1322
  @ranges << (start..end_)
484
1323
  else
485
- raise ArgumentError.new("'#{strpat}': invalid lineno format")
1324
+ raise ArgumentError.new("'#{arg}': invalid lineno format")
486
1325
  end
487
1326
  end
488
1327
  end