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.
- checksums.yaml +4 -4
- data/.dockerignore +9 -0
- data/.editorconfig +3 -0
- data/.github/workflows/retrovert.yml +20 -5
- data/.gitignore +1 -0
- data/.vscode/launch.json +48 -0
- data/Gemfile.lock +13 -4
- data/Makefile +24 -0
- data/docker/review.Dockerfile +10 -0
- data/lib/review/retrovert/converter.rb +374 -111
- data/lib/review/retrovert/reviewcompat.rb +57 -0
- data/lib/review/retrovert/reviewdef.rb +70 -0
- data/lib/review/retrovert/sty/{ird.sty → ird.sty.erb} +9 -1
- data/lib/review/retrovert/sty/review-retrovert-custom.sty.erb +6 -0
- data/lib/review/retrovert/utils.rb +35 -0
- data/lib/review/retrovert/version.rb +1 -1
- data/lib/review/retrovert/yamlconfig.rb +0 -1
- data/package-lock.json +6 -0
- data/package.json +1 -0
- data/review-retrovert.gemspec +5 -1
- data/testdata/mybook/.gitignore +3 -1
- data/testdata/mybook/.textlintrc +11 -0
- data/testdata/mybook/Rakefile +8 -0
- data/testdata/mybook/catalog.yml +36 -10
- data/testdata/mybook/config-starter.yml +134 -9
- data/testdata/mybook/config.yml +44 -15
- data/testdata/mybook/contents/00-preface.re +48 -2
- data/testdata/mybook/contents/01-install.re +38 -5
- data/testdata/mybook/contents/02-tutorial.re +333 -113
- data/testdata/mybook/contents/03-syntax.re +2370 -373
- data/testdata/mybook/contents/04-customize.re +424 -88
- data/testdata/mybook/contents/05-faq.re +288 -10
- data/testdata/mybook/contents/06-bestpractice.re +431 -59
- data/testdata/mybook/contents/91-compare.re +402 -2
- data/testdata/mybook/contents/92-filelist.re +14 -8
- data/testdata/mybook/contents/93-background.re +10 -10
- data/testdata/mybook/contents/99-postface.re +2 -1
- data/testdata/mybook/contents/r0-root.re +1 -1
- data/testdata/mybook/css/webstyle.css +180 -2
- data/testdata/mybook/data/terms.txt +3 -0
- data/testdata/mybook/data/words.txt +15 -0
- data/testdata/mybook/images/03-syntax/index-page.png +0 -0
- data/testdata/mybook/images/03-syntax/order-detail.png +0 -0
- data/testdata/mybook/images/04-customize/section_decoration_samples.png +0 -0
- data/testdata/mybook/images/05-faq/dummy-image.png +0 -0
- data/testdata/mybook/images/06-bestpractice/section_title_wlines.png +0 -0
- data/testdata/mybook/images/avatar-b.png +0 -0
- data/testdata/mybook/images/avatar-g.png +0 -0
- data/testdata/mybook/images/avatar-r.png +0 -0
- data/testdata/mybook/images/caution-icon.png +0 -0
- data/testdata/mybook/images/info-icon.png +0 -0
- data/testdata/mybook/images/warning-icon.png +0 -0
- data/testdata/mybook/layouts/layout.html5.erb +3 -1
- data/testdata/mybook/layouts/layout.tex.erb +265 -379
- data/testdata/mybook/layouts/layout.tex.erb.orig +386 -0
- data/testdata/mybook/lib/ruby/review-book.rb +64 -0
- data/testdata/mybook/lib/ruby/review-builder.rb +902 -63
- data/testdata/mybook/lib/ruby/review-compiler.rb +675 -22
- data/testdata/mybook/lib/ruby/review-epubbuilder.rb +33 -0
- data/testdata/mybook/lib/ruby/review-epubmaker.rb +10 -7
- data/testdata/mybook/lib/ruby/review-htmlbuilder.rb +354 -66
- data/testdata/mybook/lib/ruby/review-latexbuilder.rb +429 -146
- data/testdata/mybook/lib/ruby/review-maker.rb +117 -6
- data/testdata/mybook/lib/ruby/review-markdownbuilder.rb +945 -0
- data/testdata/mybook/lib/ruby/review-markdownmaker.rb +91 -0
- data/testdata/mybook/lib/ruby/review-monkeypatch.rb +2 -0
- data/testdata/mybook/lib/ruby/review-pdfmaker.rb +160 -82
- data/testdata/mybook/lib/ruby/review-webmaker.rb +20 -5
- data/testdata/mybook/lib/tasks/review.rake +14 -0
- data/testdata/mybook/lib/tasks/starter.rake +148 -4
- data/testdata/mybook/sty/indexstyle.ist +25 -0
- data/testdata/mybook/sty/mytextsize.sty +29 -0
- data/testdata/mybook/sty/mytitlepage.sty +34 -11
- data/testdata/mybook/sty/review-base.sty +276 -0
- data/testdata/mybook/sty/starter-codeblock.sty +237 -106
- data/testdata/mybook/sty/starter-color.sty +72 -17
- data/testdata/mybook/sty/starter-heading.sty +60 -13
- data/testdata/mybook/sty/starter-misc.sty +894 -0
- data/testdata/mybook/sty/starter-note.sty +67 -14
- data/testdata/mybook/sty/starter-talklist.sty +105 -0
- data/testdata/mybook/sty/starter-util.sty +35 -0
- data/testdata/mybook/sty/starter.sty +8 -526
- 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
|
-
|
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
|
137
|
+
def _sideimage_parse_options(option_str, &b)
|
93
138
|
opts = {}
|
94
|
-
|
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 "
|
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 "
|
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 "
|
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 "
|
118
|
-
|
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 "
|
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
|
-
|
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
|
-
|
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
|
232
|
+
end
|
171
233
|
end
|
172
234
|
|
173
|
-
def
|
235
|
+
def _codeblock_parse_options(option_str, &b) # parse 'fold={on|off},...'
|
174
236
|
opts = {}
|
175
|
-
return opts if
|
237
|
+
return opts if option_str.nil? || option_str.empty?
|
176
238
|
vals = {nil=>true, 'on'=>true, 'off'=>false}
|
177
|
-
|
178
|
-
|
239
|
+
i = -1
|
240
|
+
_each_block_option(option_str) do |k, v|
|
241
|
+
i += 1
|
179
242
|
## //list[][][1] は //list[][][lineno=1] とみなす
|
180
|
-
if
|
181
|
-
opts['lineno'] =
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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'] =
|
314
|
+
if i == 0 && v.nil?
|
315
|
+
opts['lang'] = k # for compatibility with Re:VIEW
|
232
316
|
else
|
233
|
-
|
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("'#{
|
1324
|
+
raise ArgumentError.new("'#{arg}': invalid lineno format")
|
486
1325
|
end
|
487
1326
|
end
|
488
1327
|
end
|