review-retrovert 0.9.7 → 0.9.8

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 (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