latexmath 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,22 +1,92 @@
1
- require "latexmath/version"
2
- require "latexmath/latexml_requirement"
3
- require "latexmath/equation"
4
- require "htmlentities"
5
- require "unicode2latex"
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
- class Error < StandardError; end
9
- # Your code goes here...
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
- Requirements = {
12
- latexml: LatexmlRequirement.new
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 = Unicode2LaTeX.unicode2latex(HTMLEntities.new.decode(string))
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