latexmath 0.1.0 → 0.1.1
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 +14 -0
- data/.github/workflows/test.yml +80 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +132 -0
- data/Gemfile +6 -3
- data/Gemfile.lock +44 -3
- data/README.adoc +28 -0
- data/Rakefile +29 -3
- data/bin/console +3 -3
- data/exe/latexmath +8 -0
- data/latexmath.gemspec +17 -14
- data/lib/latexmath.rb +80 -10
- data/lib/latexmath/aggregator.rb +351 -0
- data/lib/latexmath/converter.rb +421 -0
- data/lib/latexmath/equation.rb +3 -30
- data/lib/latexmath/ext.rb +9 -0
- data/lib/latexmath/latexml_requirement.rb +11 -12
- data/lib/latexmath/requirement.rb +2 -3
- data/lib/latexmath/symbol.rb +43 -0
- data/lib/latexmath/tokenizer.rb +81 -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 +73 -15
- data/.travis.yml +0 -6
- data/README.md +0 -40
data/lib/latexmath.rb
CHANGED
@@ -1,22 +1,92 @@
|
|
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/latexml_requirement'
|
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
|
10
33
|
|
11
|
-
|
12
|
-
|
34
|
+
SPACES = ['\\,', '\\:', '\\;', '\\\\'].freeze
|
35
|
+
STYLES = {
|
36
|
+
'\\bf' => 'mathbf'
|
37
|
+
}.freeze
|
38
|
+
|
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
|
+
|
80
|
+
Requirements = {
|
81
|
+
#latexml: LatexmlRequirement.new
|
82
|
+
}.freeze
|
83
|
+
|
15
84
|
def self.parse(string)
|
16
|
-
lxm_input =
|
85
|
+
lxm_input = HTMLEntities.new.decode(string)
|
17
86
|
|
18
87
|
# parse
|
19
88
|
Equation.new(lxm_input)
|
20
89
|
end
|
21
90
|
|
91
|
+
class Error < StandardError; end
|
22
92
|
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
|