latexmath 0.1.0 → 0.1.5

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "latexmath"
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 "irb"
13
+ require 'irb'
14
14
  IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/latexmath'
3
+
4
+ latex = File.read(ARGV[0])
5
+
6
+ tokens = Latexmath::Tokenizer.new(latex).tokenize
7
+ aggr = Latexmath::Aggregator.new(tokens).aggregate
8
+ puts Latexmath::Converter.new(aggr).convert
@@ -1,31 +1,34 @@
1
1
  require_relative 'lib/latexmath/version'
2
2
 
3
3
  Gem::Specification.new do |spec|
4
- spec.name = "latexmath"
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 = %q{Converts LaTeX math into MathML.}
10
- spec.description = %q{Converts LaTeX math into MathML.}
11
- spec.homepage = "https://github.com/metanorma/latexmath"
12
- spec.license = "BSD-2-Clause"
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(">= 2.3.0")
14
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
15
15
 
16
- spec.metadata["homepage_uri"] = spec.homepage
17
- spec.metadata["source_code_uri"] = "https://github.com/metanorma/latexmath"
18
- spec.metadata["changelog_uri"] = "https://github.com/metanorma/latexmath/blob/master/CHANGELOG.md."
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 = Dir.chdir(File.expand_path('..', __FILE__)) do
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 = "exe"
25
+ spec.bindir = 'exe'
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
- spec.require_paths = ["lib"]
27
+ spec.require_paths = ['lib']
28
28
 
29
- spec.add_dependency "unicode2latex", "~> 0.0.1"
30
- spec.add_dependency "htmlentities", "~> 4.3"
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
@@ -1,22 +1,88 @@
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/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
- 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
33
+
34
+ SPACES = ['\\,', '\\:', '\\;', '\\\\'].freeze
35
+ STYLES = {
36
+ '\\bf' => 'mathbf'
37
+ }.freeze
10
38
 
11
- Requirements = {
12
- latexml: LatexmlRequirement.new
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 = Unicode2LaTeX.unicode2latex(HTMLEntities.new.decode(string))
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