latexmath 0.1.0 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +14 -0
- data/.github/workflows/test.yml +80 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +129 -0
- data/Gemfile +6 -3
- data/Gemfile.lock +44 -3
- data/README.adoc +28 -0
- data/Rakefile +93 -4
- data/bin/console +3 -3
- data/exe/latexmath +8 -0
- data/latexmath.gemspec +17 -14
- data/lib/latexmath.rb +76 -10
- data/lib/latexmath/aggregator.rb +351 -0
- data/lib/latexmath/constants/symbols.rb +3936 -0
- data/lib/latexmath/converter.rb +424 -0
- data/lib/latexmath/equation.rb +3 -30
- data/lib/latexmath/ext.rb +9 -0
- data/lib/latexmath/symbol.rb +21 -0
- data/lib/latexmath/tokenizer.rb +82 -0
- data/lib/latexmath/version.rb +1 -1
- data/lib/latexmath/xml/builder.rb +4 -0
- data/lib/latexmath/xml/element.rb +25 -0
- data/lib/unimathsymbols.js.erb +4 -0
- data/lib/unimathsymbols.txt +2864 -0
- data/opal/latexmath-opal.rb +40 -0
- data/opal/ox.rb +64 -0
- data/opal/pseudoenumerator.rb +19 -0
- metadata +74 -17
- data/.travis.yml +0 -6
- data/README.md +0 -40
- data/lib/latexmath/latexml_requirement.rb +0 -84
- data/lib/latexmath/requirement.rb +0 -12
data/bin/console
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'latexmath'
|
5
5
|
|
6
6
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
7
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +10,5 @@ require "latexmath"
|
|
10
10
|
# require "pry"
|
11
11
|
# Pry.start
|
12
12
|
|
13
|
-
require
|
13
|
+
require 'irb'
|
14
14
|
IRB.start(__FILE__)
|
data/exe/latexmath
ADDED
data/latexmath.gemspec
CHANGED
@@ -1,31 +1,34 @@
|
|
1
1
|
require_relative 'lib/latexmath/version'
|
2
2
|
|
3
3
|
Gem::Specification.new do |spec|
|
4
|
-
spec.name =
|
4
|
+
spec.name = 'latexmath'
|
5
5
|
spec.version = Latexmath::VERSION
|
6
6
|
spec.authors = ['Ribose Inc.']
|
7
7
|
spec.email = ['open.source@ribose.com']
|
8
8
|
|
9
|
-
spec.summary =
|
10
|
-
spec.description =
|
11
|
-
spec.homepage =
|
12
|
-
spec.license =
|
9
|
+
spec.summary = 'Converts LaTeX math into MathML.'
|
10
|
+
spec.description = 'Converts LaTeX math into MathML.'
|
11
|
+
spec.homepage = 'https://github.com/plurimath/latexmath'
|
12
|
+
spec.license = 'BSD-2-Clause'
|
13
13
|
|
14
|
-
spec.required_ruby_version = Gem::Requirement.new(
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
|
15
15
|
|
16
|
-
spec.metadata[
|
17
|
-
spec.metadata[
|
18
|
-
spec.metadata[
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
spec.metadata['source_code_uri'] = 'https://github.com/plurimath/latexmath'
|
18
|
+
spec.metadata['changelog_uri'] = 'https://github.com/plurimath/latexmath/blob/master/CHANGELOG.md.'
|
19
19
|
|
20
20
|
# Specify which files should be added to the gem when it is released.
|
21
21
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
-
spec.files
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
23
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
24
|
end
|
25
|
-
spec.bindir =
|
25
|
+
spec.bindir = 'exe'
|
26
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
-
spec.require_paths = [
|
27
|
+
spec.require_paths = ['lib']
|
28
28
|
|
29
|
-
spec.add_dependency
|
30
|
-
spec.add_dependency
|
29
|
+
spec.add_dependency 'htmlentities', '~> 4.3'
|
30
|
+
spec.add_dependency 'ox', '~> 2.13'
|
31
|
+
spec.add_development_dependency 'equivalent-xml'
|
32
|
+
spec.add_development_dependency 'execjs'
|
33
|
+
spec.add_development_dependency 'opal'
|
31
34
|
end
|
data/lib/latexmath.rb
CHANGED
@@ -1,22 +1,88 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
|
1
|
+
#require 'byebug' unless RUBY_ENGINE == 'opal'
|
2
|
+
require 'json'
|
3
|
+
require 'htmlentities'
|
4
|
+
require 'ox'
|
5
|
+
require_relative 'latexmath/ext'
|
6
|
+
require_relative 'latexmath/version'
|
7
|
+
require_relative 'latexmath/constants/symbols'
|
8
|
+
require_relative 'latexmath/aggregator'
|
9
|
+
require_relative 'latexmath/converter'
|
10
|
+
require_relative 'latexmath/symbol'
|
11
|
+
require_relative 'latexmath/tokenizer'
|
12
|
+
require_relative 'latexmath/xml/element'
|
13
|
+
require_relative 'latexmath/equation'
|
6
14
|
|
7
15
|
module Latexmath
|
8
|
-
|
9
|
-
|
16
|
+
MATRICES = [
|
17
|
+
'\\matrix',
|
18
|
+
'\\matrix*',
|
19
|
+
'\\pmatrix',
|
20
|
+
'\\pmatrix*',
|
21
|
+
'\\bmatrix',
|
22
|
+
'\\bmatrix*',
|
23
|
+
'\\Bmatrix',
|
24
|
+
'\\Bmatrix*',
|
25
|
+
'\\vmatrix',
|
26
|
+
'\\vmatrix*',
|
27
|
+
'\\Vmatrix',
|
28
|
+
'\\Vmatrix*',
|
29
|
+
'\\array',
|
30
|
+
'\\split',
|
31
|
+
'\\substack'
|
32
|
+
].freeze
|
33
|
+
|
34
|
+
SPACES = ['\\,', '\\:', '\\;', '\\\\'].freeze
|
35
|
+
STYLES = {
|
36
|
+
'\\bf' => 'mathbf'
|
37
|
+
}.freeze
|
10
38
|
|
11
|
-
|
12
|
-
|
39
|
+
LIMITS = ['\\lim', '\\sup', '\\inf', '\\max', '\\min'].freeze
|
40
|
+
COMMANDS = {
|
41
|
+
# command: [params_count, mathml_equivalent, attributes]
|
42
|
+
'_' => [2, 'msub', {}],
|
43
|
+
'^' => [2, 'msup', {}],
|
44
|
+
'_^' => [3, 'msubsup', {}],
|
45
|
+
'\\frac' => [2, 'mfrac', {}],
|
46
|
+
'\\sqrt' => [1, 'msqrt', {}],
|
47
|
+
'\\root' => [2, 'mroot', {}],
|
48
|
+
'\\binom' => [2, 'mfrac', { "linethickness": '0' }],
|
49
|
+
'\\left' => [
|
50
|
+
1,
|
51
|
+
'mo',
|
52
|
+
[%w[stretchy true], %w[fence true], %w[form prefix]]
|
53
|
+
],
|
54
|
+
'\\right' => [
|
55
|
+
1,
|
56
|
+
'mo',
|
57
|
+
[%w[stretchy true], %w[fence true], %w[form postfix]]
|
58
|
+
],
|
59
|
+
'\\overline' => [1, 'mover', {}],
|
60
|
+
'\\bar' => [1, 'mover', {}],
|
61
|
+
'\\underline' => [1, 'munder', {}],
|
62
|
+
'\\limits' => [3, 'munderover', {}],
|
63
|
+
'\\overrightarrow' => [1, 'mover', {}]
|
13
64
|
}
|
14
65
|
|
66
|
+
COMMANDS['\\quad'] = [0, 'mo', { "mathvariant": 'italic', separator: 'true' }]
|
67
|
+
COMMANDS['\\qquad'] = [0, 'mo', { "mathvariant": 'italic', separator: 'true' }]
|
68
|
+
SPACES.each do |space|
|
69
|
+
COMMANDS[space] = [0, 'mspace', { "width": '0.167em' }]
|
70
|
+
end
|
71
|
+
|
72
|
+
MATRICES.each do |matrix|
|
73
|
+
COMMANDS[matrix] = [1, 'mtable', {}]
|
74
|
+
end
|
75
|
+
|
76
|
+
LIMITS.each do |limit|
|
77
|
+
COMMANDS[limit] = [1, 'munder', {}]
|
78
|
+
end
|
79
|
+
|
15
80
|
def self.parse(string)
|
16
|
-
lxm_input =
|
81
|
+
lxm_input = HTMLEntities.new.decode(string)
|
17
82
|
|
18
83
|
# parse
|
19
84
|
Equation.new(lxm_input)
|
20
85
|
end
|
21
86
|
|
87
|
+
class Error < StandardError; end
|
22
88
|
end
|
@@ -0,0 +1,351 @@
|
|
1
|
+
module Latexmath
|
2
|
+
class Aggregator
|
3
|
+
OPERATORS = '+-*/=[]_^{}()'.freeze
|
4
|
+
|
5
|
+
OPENING_BRACES = '{'.freeze
|
6
|
+
CLOSING_BRACES = '}'.freeze
|
7
|
+
OPENING_BRACKET = '['.freeze
|
8
|
+
CLOSING_BRACKET = ']'.freeze
|
9
|
+
OPENING_PARENTHESIS = '('.freeze
|
10
|
+
CLOSING_PARENTHESIS = ')'.freeze
|
11
|
+
|
12
|
+
BACKSLASH = '\\\\'.freeze
|
13
|
+
AMPERSAND = '&'.freeze
|
14
|
+
DASH = '-'.freeze
|
15
|
+
|
16
|
+
SUB_SUP = '_^'.freeze
|
17
|
+
SUBSCRIPT = '_'.freeze
|
18
|
+
SUPERSCRIPT = '^'.freeze
|
19
|
+
|
20
|
+
# Added prefix LATEX_ to avoid ruby reserved words
|
21
|
+
LATEX_LEFT = '\\left'.freeze
|
22
|
+
LATEX_RIGHT = '\\right'.freeze
|
23
|
+
LATEX_OVER = '\\over'.freeze
|
24
|
+
LATEX_HLINE = '\\hline'.freeze
|
25
|
+
LATEX_BEGIN = '\\begin'.freeze
|
26
|
+
LATEX_FRAC = '\\frac'.freeze
|
27
|
+
LATEX_ROOT = '\\root'.freeze
|
28
|
+
LATEX_SQRT = '\\sqrt'.freeze
|
29
|
+
|
30
|
+
def initialize(tokens)
|
31
|
+
@tokens = tokens
|
32
|
+
end
|
33
|
+
|
34
|
+
def aggregate(tokens = @tokens)
|
35
|
+
aggregated = []
|
36
|
+
|
37
|
+
loop do
|
38
|
+
begin
|
39
|
+
token = next_item_or_group(tokens)
|
40
|
+
raise StopIteration if token.nil?
|
41
|
+
|
42
|
+
if token.is_a?(Array)
|
43
|
+
aggregated << token
|
44
|
+
elsif token == OPENING_BRACKET
|
45
|
+
previous = nil
|
46
|
+
previous = aggregated[-1] if aggregated.any?
|
47
|
+
begin
|
48
|
+
g = group(tokens, opening: OPENING_BRACKET, closing: CLOSING_BRACKET)
|
49
|
+
if previous == LATEX_SQRT
|
50
|
+
root = tokens.shift
|
51
|
+
raise StopIteration if root.nil?
|
52
|
+
|
53
|
+
if root == OPENING_BRACES
|
54
|
+
begin
|
55
|
+
root = group(tokens)
|
56
|
+
rescue EmptyGroupError
|
57
|
+
root = ''
|
58
|
+
end
|
59
|
+
end
|
60
|
+
aggregated[-1] = LATEX_ROOT
|
61
|
+
aggregated << root
|
62
|
+
end
|
63
|
+
aggregated << g
|
64
|
+
rescue EmptyGroupError
|
65
|
+
next if previous == LATEX_SQRT
|
66
|
+
|
67
|
+
aggregated += [OPENING_BRACKET, CLOSING_BRACKET]
|
68
|
+
end
|
69
|
+
elsif LIMITS.include?(token)
|
70
|
+
raise StopIteration if tokens.shift.nil?
|
71
|
+
|
72
|
+
a = next_item_or_group(tokens)
|
73
|
+
aggregated += [token, a]
|
74
|
+
elsif token == '\\limits'
|
75
|
+
previous = aggregated.pop
|
76
|
+
raise StopIteration if tokens.shift.nil?
|
77
|
+
|
78
|
+
a = next_item_or_group(tokens)
|
79
|
+
raise StopIteration if tokens.shift.nil?
|
80
|
+
|
81
|
+
b = next_item_or_group(tokens)
|
82
|
+
aggregated += [token, previous, a, b]
|
83
|
+
elsif token && SUB_SUP.include?(token)
|
84
|
+
aggregated = process_sub_sup(aggregated, token, tokens)
|
85
|
+
elsif token.start_with?(LATEX_BEGIN) || MATRICES.include?(token)
|
86
|
+
aggregated += environment(token, tokens)
|
87
|
+
elsif token == LATEX_OVER
|
88
|
+
numerator = aggregated
|
89
|
+
aggregated = []
|
90
|
+
aggregated << LATEX_FRAC
|
91
|
+
aggregated << numerator
|
92
|
+
aggregated << aggregate(tokens)
|
93
|
+
else
|
94
|
+
aggregated << token
|
95
|
+
end
|
96
|
+
rescue EmptyGroupError
|
97
|
+
aggregated += [OPENING_BRACES, CLOSING_BRACES]
|
98
|
+
next
|
99
|
+
rescue StopIteration
|
100
|
+
aggregated << token unless token.nil?
|
101
|
+
break
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
aggregated
|
106
|
+
end
|
107
|
+
|
108
|
+
def environment(token, tokens)
|
109
|
+
env = if token.start_with?(LATEX_BEGIN)
|
110
|
+
token[7..-2]
|
111
|
+
else
|
112
|
+
token[1..token.size]
|
113
|
+
end
|
114
|
+
|
115
|
+
alignment = nil
|
116
|
+
content = []
|
117
|
+
row = []
|
118
|
+
has_rowline = false
|
119
|
+
|
120
|
+
loop do
|
121
|
+
begin
|
122
|
+
token = next_item_or_group(tokens)
|
123
|
+
raise StopIteration if token.nil?
|
124
|
+
|
125
|
+
if token.is_a? Array
|
126
|
+
begin
|
127
|
+
if env == 'array' && token.all? { |x| 'lcr|'.include?(x) }
|
128
|
+
alignment = token
|
129
|
+
else
|
130
|
+
row << process_row(token)
|
131
|
+
end
|
132
|
+
rescue TypeError
|
133
|
+
row << token
|
134
|
+
end
|
135
|
+
elsif token == "\\end{#{env}}"
|
136
|
+
break
|
137
|
+
elsif token == AMPERSAND
|
138
|
+
row << token
|
139
|
+
elsif token == BACKSLASH
|
140
|
+
row = group_columns(row) if row.include?(AMPERSAND)
|
141
|
+
row.insert(0, LATEX_HLINE) if has_rowline
|
142
|
+
content << row
|
143
|
+
row = []
|
144
|
+
has_rowline = false
|
145
|
+
elsif token == LATEX_HLINE
|
146
|
+
has_rowline = true
|
147
|
+
elsif token == OPENING_BRACKET && content.empty?
|
148
|
+
begin
|
149
|
+
alignment = group(tokens, opening: OPENING_BRACKET, closing: CLOSING_BRACKET)
|
150
|
+
rescue EmptyGroupError
|
151
|
+
next
|
152
|
+
end
|
153
|
+
elsif token == DASH
|
154
|
+
next_token = tokens.shift
|
155
|
+
raise StopIteration if next_token.nil?
|
156
|
+
|
157
|
+
row << if next_token == "\\end{#{env}}"
|
158
|
+
token
|
159
|
+
else
|
160
|
+
[token, next_token]
|
161
|
+
end
|
162
|
+
elsif SUB_SUP.include?(token)
|
163
|
+
row = process_sub_sup(row, token, tokens)
|
164
|
+
elsif token.start_with?(LATEX_BEGIN)
|
165
|
+
row += environment(token, tokens)
|
166
|
+
else
|
167
|
+
row << token
|
168
|
+
end
|
169
|
+
rescue EmptyGroupError
|
170
|
+
row << []
|
171
|
+
next
|
172
|
+
rescue StopIteration
|
173
|
+
break
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
if row.any?
|
178
|
+
row = group_columns(row) if row.include?(AMPERSAND)
|
179
|
+
row.insert(0, LATEX_HLINE) if has_rowline
|
180
|
+
content << row
|
181
|
+
end
|
182
|
+
|
183
|
+
content = content.pop while content.size == 1 && content.first.is_a?(Array)
|
184
|
+
|
185
|
+
return ["\\#{env}", alignment.join, content] if alignment
|
186
|
+
|
187
|
+
["\\#{env}", content]
|
188
|
+
end
|
189
|
+
|
190
|
+
def group(tokens, opening: OPENING_BRACES, closing: CLOSING_BRACES, delimiter: nil)
|
191
|
+
g = []
|
192
|
+
|
193
|
+
if delimiter
|
194
|
+
g << delimiter
|
195
|
+
g << tokens.shift
|
196
|
+
end
|
197
|
+
|
198
|
+
loop do
|
199
|
+
begin
|
200
|
+
token = tokens.shift
|
201
|
+
raise StopIteration if token.nil?
|
202
|
+
|
203
|
+
if token == closing && delimiter.nil?
|
204
|
+
break if g.any?
|
205
|
+
|
206
|
+
raise EmptyGroupError
|
207
|
+
elsif token == opening
|
208
|
+
begin
|
209
|
+
g << group(tokens)
|
210
|
+
rescue EmptyGroupError
|
211
|
+
g += [[]]
|
212
|
+
end
|
213
|
+
elsif token == LATEX_LEFT
|
214
|
+
g << group(tokens, delimiter: token)
|
215
|
+
elsif token == LATEX_RIGHT
|
216
|
+
g << token
|
217
|
+
_token = tokens.shift
|
218
|
+
raise StopIteration if _token.nil?
|
219
|
+
|
220
|
+
g << _token
|
221
|
+
break
|
222
|
+
else
|
223
|
+
g << token
|
224
|
+
end
|
225
|
+
rescue StopIteration
|
226
|
+
break
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
if delimiter
|
231
|
+
right = g.index(LATEX_RIGHT)
|
232
|
+
raise ExtraLeftOrMissingRight if right.nil?
|
233
|
+
|
234
|
+
content = g[2..right - 1]
|
235
|
+
g_ = g
|
236
|
+
g_ = g[0..1] + [aggregate(content)] + g[right..g.size] if content.any?
|
237
|
+
|
238
|
+
return g_
|
239
|
+
end
|
240
|
+
|
241
|
+
aggregate(g)
|
242
|
+
end
|
243
|
+
|
244
|
+
def group_columns(row)
|
245
|
+
grouped = [[]]
|
246
|
+
row.each do |item|
|
247
|
+
if item == AMPERSAND
|
248
|
+
grouped << []
|
249
|
+
else
|
250
|
+
grouped[-1] << item
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
grouped.map { |item| item.size > 1 ? item : item.pop }
|
255
|
+
end
|
256
|
+
|
257
|
+
def next_item_or_group(tokens)
|
258
|
+
token = tokens.shift
|
259
|
+
raise StopIteration if token.nil?
|
260
|
+
|
261
|
+
return group(tokens) if token == OPENING_BRACES
|
262
|
+
|
263
|
+
return group(tokens, delimiter: token) if token == LATEX_LEFT
|
264
|
+
|
265
|
+
token
|
266
|
+
end
|
267
|
+
|
268
|
+
def find_opening_parenthesis(tokens)
|
269
|
+
closing = 0
|
270
|
+
|
271
|
+
tokens.map.with_index { |x, i| [i, x] }.reverse.each do |index, token|
|
272
|
+
if token == CLOSING_PARENTHESIS
|
273
|
+
closing += 1
|
274
|
+
elsif token == OPENING_PARENTHESIS
|
275
|
+
return index if closing == 0
|
276
|
+
|
277
|
+
closing -= 1
|
278
|
+
end
|
279
|
+
end
|
280
|
+
raise ExtraLeftOrMissingRight
|
281
|
+
end
|
282
|
+
|
283
|
+
def process_row(tokens)
|
284
|
+
row = []
|
285
|
+
content = []
|
286
|
+
|
287
|
+
tokens.each do |token|
|
288
|
+
if token == AMPERSAND
|
289
|
+
next
|
290
|
+
elsif token == BACKSLASH
|
291
|
+
content << row if row.any?
|
292
|
+
row = []
|
293
|
+
else
|
294
|
+
row << token
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
content << row if row.any?
|
299
|
+
|
300
|
+
content = content.pop while content.size == 1 && content.first.is_a?(Array)
|
301
|
+
|
302
|
+
content
|
303
|
+
end
|
304
|
+
|
305
|
+
def process_sub_sup(aggregated, token, tokens)
|
306
|
+
begin
|
307
|
+
previous = aggregated.pop
|
308
|
+
raise IndexError if previous.nil?
|
309
|
+
|
310
|
+
if previous.is_a?(String) && OPERATORS.include?(previous)
|
311
|
+
if (previous == CLOSING_PARENTHESIS) && aggregated.include?(OPENING_PARENTHESIS)
|
312
|
+
index = find_opening_parenthesis(aggregated)
|
313
|
+
aggregated = aggregated[0, index] + [token] + [aggregated[index..aggregated.size] + [previous]]
|
314
|
+
else
|
315
|
+
aggregated += [previous, token]
|
316
|
+
end
|
317
|
+
return aggregated
|
318
|
+
end
|
319
|
+
|
320
|
+
begin
|
321
|
+
next_token = next_item_or_group(tokens)
|
322
|
+
if aggregated.size >= 2
|
323
|
+
if aggregated[-2] == SUBSCRIPT && token == SUPERSCRIPT
|
324
|
+
aggregated[-2] = SUB_SUP
|
325
|
+
aggregated += [previous, next_token]
|
326
|
+
elsif (aggregated[-2] == SUPERSCRIPT) && (token == SUBSCRIPT)
|
327
|
+
aggregated[-2] = SUB_SUP
|
328
|
+
aggregated += [next_token, previous]
|
329
|
+
else
|
330
|
+
aggregated += [token, previous, next_token]
|
331
|
+
end
|
332
|
+
else
|
333
|
+
aggregated += [token, previous, next_token]
|
334
|
+
end
|
335
|
+
rescue EmptyGroupError
|
336
|
+
aggregated += [token, previous, []]
|
337
|
+
end
|
338
|
+
rescue IndexError
|
339
|
+
next_token = next_item_or_group(tokens)
|
340
|
+
aggregated += [token, '', next_token]
|
341
|
+
end
|
342
|
+
|
343
|
+
aggregated
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
class EmptyGroupError < StandardError; end
|
348
|
+
class ExtraLeftOrMissingRight < StandardError; end
|
349
|
+
class MissingSuperScriptOrSubscript < StandardError; end
|
350
|
+
class StopIteration < StandardError; end
|
351
|
+
end
|