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