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