latexmath 0.1.4 → 1.0.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/LICENSE.md +33 -0
  4. data/README.adoc +133 -14
  5. data/artifacts/latex.bindings.python.json +1 -0
  6. data/artifacts/latex.bindings.rs.json +1 -0
  7. data/artifacts/latex.bindings.ruby.json +1 -0
  8. data/artifacts/latex.bindings.ts.json +1 -0
  9. data/artifacts/latex.checksums.txt +19 -0
  10. data/artifacts/latex.json +1 -0
  11. data/data/latex-symbols.json +3301 -0
  12. data/docs/model-contract.adoc +125 -0
  13. data/docs/parser-contract.adoc +349 -0
  14. data/docs/testsuite-pin.adoc +76 -0
  15. data/grammars/latex.wrappers.json +17 -0
  16. data/lib/latexmath/model/nodes.rb +918 -0
  17. data/lib/latexmath/model.rb +44 -0
  18. data/lib/latexmath/parser/artifact.rb +80 -0
  19. data/lib/latexmath/parser/configuration.rb +35 -0
  20. data/lib/latexmath/parser/engine.rb +161 -0
  21. data/lib/latexmath/parser/errors.rb +51 -0
  22. data/lib/latexmath/parser/native_recursion_guard.rb +43 -0
  23. data/lib/latexmath/parser/preprocessor.rb +77 -0
  24. data/lib/latexmath/parser/raw_parser.rb +84 -0
  25. data/lib/latexmath/parser/source_map.rb +121 -0
  26. data/lib/latexmath/parser/unwrap.rb +91 -0
  27. data/lib/latexmath/parser.rb +97 -0
  28. data/lib/latexmath/symbols.rb +74 -0
  29. data/lib/latexmath/version.rb +1 -1
  30. data/lib/latexmath.rb +5 -87
  31. data/model/latex.lml.checksums.txt +36 -0
  32. data/model/latex.lml.json +3350 -0
  33. metadata +60 -71
  34. data/.editorconfig +0 -14
  35. data/.github/workflows/test.yml +0 -80
  36. data/.gitignore +0 -22
  37. data/.rspec +0 -3
  38. data/.rubocop.yml +0 -129
  39. data/CODE_OF_CONDUCT.md +0 -74
  40. data/Gemfile +0 -10
  41. data/Gemfile.lock +0 -79
  42. data/Rakefile +0 -95
  43. data/bin/console +0 -14
  44. data/bin/setup +0 -8
  45. data/exe/latexmath +0 -8
  46. data/latexmath.gemspec +0 -34
  47. data/lib/latexmath/aggregator.rb +0 -351
  48. data/lib/latexmath/converter.rb +0 -424
  49. data/lib/latexmath/equation.rb +0 -13
  50. data/lib/latexmath/ext.rb +0 -9
  51. data/lib/latexmath/symbol.rb +0 -21
  52. data/lib/latexmath/tokenizer.rb +0 -82
  53. data/lib/latexmath/xml/builder.rb +0 -4
  54. data/lib/latexmath/xml/element.rb +0 -25
  55. data/lib/unimathsymbols.js.erb +0 -4
  56. data/lib/unimathsymbols.txt +0 -2864
  57. data/opal/latexmath-opal.rb +0 -40
  58. data/opal/ox.rb +0 -64
  59. data/opal/pseudoenumerator.rb +0 -19
data/Rakefile DELETED
@@ -1,95 +0,0 @@
1
- require 'bundler/setup'
2
- require 'bundler/gem_tasks'
3
- require 'rspec/core/rake_task'
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task 'build-opal' do
7
- require 'opal'
8
- require 'erb'
9
- require 'json'
10
-
11
- b = Opal::Builder.new
12
-
13
- b.append_paths __dir__ + '/lib'
14
- b.append_paths __dir__ + '/opal'
15
- b.use_gem 'htmlentities'
16
-
17
- build = b.build('latexmath-opal').to_s
18
- # This is a hack intended for ExecJS to work properly.
19
- build = build.gsub('Object freezing is not supported by Opal', '')
20
-
21
- FileUtils.mkdir_p 'dist'
22
- File.write('dist/latexmath.js', build)
23
- end
24
-
25
- task 'spec-opal' => 'build-opal' do
26
- ENV['TEST_OPAL'] = '1'
27
- Rake::Task['spec'].execute
28
- ENV.delete 'TEST_OPAL'
29
- end
30
-
31
- def read_symbols(input)
32
- @symbols = {}
33
- File.readlines(input).each do |line|
34
- next if line.start_with?('#')
35
-
36
- columns = line.chop.split('^')
37
- _unicode = columns[0]
38
- latex = columns[2]
39
- unicode_math = columns[3]
40
-
41
- @symbols[latex] = _unicode if latex && !@symbols.include?(latex)
42
- @symbols[unicode_math] = _unicode if unicode_math && !@symbols.include?(unicode_math)
43
-
44
- matches = columns[-1].match(/=\s+(\\[^,^ ]+),?/)
45
- if matches
46
- @symbols[matches[1]] = _unicode unless @symbols[matches[1]]
47
- end
48
- end
49
- @symbols
50
- end
51
-
52
- directory 'lib/latexmath/constants'
53
- file 'lib/latexmath/constants/symbols.rb' => [
54
- 'lib/latexmath/constants',
55
- 'lib/unimathsymbols.txt'
56
- ] do |file|
57
- symbols = read_symbols(file.prerequisites.last)
58
-
59
- File.open(file.name, "w+") do |f|
60
- f.puts <<~HERE
61
- module Latexmath
62
- module Constants
63
- SYMBOLS = {
64
- HERE
65
-
66
- @symbols.each_pair do |k,v|
67
- f.puts(" '#{k.to_s}' => '#{v}',")
68
- end
69
-
70
- f.puts <<~HERE
71
- }.freeze
72
- end
73
- end
74
- HERE
75
- end
76
- end
77
-
78
- Rake::Task['build'].enhance ['lib/latexmath/constants/symbols.rb']
79
- Rake::Task[:spec].enhance ['lib/latexmath/constants/symbols.rb']
80
-
81
- desc 'Generate and write MathML fixtures'
82
- task 'create-mathml-fixtures' do
83
- require_relative 'lib/latexmath'
84
- records = Dir.glob('spec/fixtures/*/*.tex')
85
-
86
- records.each do |file|
87
- tex = File.read("./#{file}")
88
- tokens = Latexmath::Tokenizer.new(tex).tokenize
89
- aggr = Latexmath::Aggregator.new(tokens).aggregate
90
-
91
- File.write("./#{file.chomp('.tex')}.html", Latexmath::Converter.new(aggr).convert)
92
- end
93
- end
94
-
95
- task default: [:spec, 'spec-opal']
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'bundler/setup'
4
- require 'latexmath'
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require 'irb'
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
data/exe/latexmath DELETED
@@ -1,8 +0,0 @@
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
data/latexmath.gemspec DELETED
@@ -1,34 +0,0 @@
1
- require_relative 'lib/latexmath/version'
2
-
3
- Gem::Specification.new do |spec|
4
- spec.name = 'latexmath'
5
- spec.version = Latexmath::VERSION
6
- spec.authors = ['Ribose Inc.']
7
- spec.email = ['open.source@ribose.com']
8
-
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
-
14
- spec.required_ruby_version = Gem::Requirement.new('>= 2.3.0')
15
-
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
-
20
- # Specify which files should be added to the gem when it is released.
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(__dir__)) do
23
- `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
- end
25
- spec.bindir = 'exe'
26
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
- spec.require_paths = ['lib']
28
-
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'
34
- end
@@ -1,351 +0,0 @@
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