plurimath 0.11.12 → 0.11.13

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +7 -0
  3. data/.github/workflows/shared-testsuite.yml +24 -0
  4. data/.gitmodules +3 -0
  5. data/.rspec-opal +3 -0
  6. data/Gemfile +18 -0
  7. data/README.adoc +297 -3
  8. data/docs/catalog.adoc +73 -0
  9. data/docs/uncertainty-asciimath-tree.md +92 -0
  10. data/docs/uncertainty.adoc +522 -0
  11. data/lib/plurimath/asciimath/parse.rb +43 -226
  12. data/lib/plurimath/asciimath/parser.rb +58 -1
  13. data/lib/plurimath/asciimath/slice_adapter.rb +138 -0
  14. data/lib/plurimath/asciimath/transform.rb +218 -0
  15. data/lib/plurimath/asciimath.rb +1 -0
  16. data/lib/plurimath/catalog/entry.rb +109 -0
  17. data/lib/plurimath/catalog/validation.rb +109 -0
  18. data/lib/plurimath/catalog.rb +96 -0
  19. data/lib/plurimath/cli/convert.rb +40 -0
  20. data/lib/plurimath/cli/helpers.rb +49 -0
  21. data/lib/plurimath/cli/render.rb +64 -0
  22. data/lib/plurimath/cli.rb +55 -57
  23. data/lib/plurimath/documentation.rb +99 -0
  24. data/lib/plurimath/errors/ambiguous_decimal_error.rb +19 -0
  25. data/lib/plurimath/errors/invalid_uncertainty.rb +18 -0
  26. data/lib/plurimath/errors/missing_dependency_error.rb +23 -0
  27. data/lib/plurimath/errors/rendering_failed.rb +17 -0
  28. data/lib/plurimath/errors/rendering_unavailable.rb +25 -0
  29. data/lib/plurimath/errors/unsupported_render_format.rb +18 -0
  30. data/lib/plurimath/errors/unsupported_semantic_tree_error.rb +12 -0
  31. data/lib/plurimath/errors.rb +5 -0
  32. data/lib/plurimath/math/evaluation/evaluator.rb +48 -0
  33. data/lib/plurimath/math/evaluation/expression_parser.rb +29 -20
  34. data/lib/plurimath/math/formula/class_tree.rb +92 -0
  35. data/lib/plurimath/math/formula.rb +43 -2
  36. data/lib/plurimath/math/function/abs.rb +6 -0
  37. data/lib/plurimath/math/function/arccos.rb +6 -0
  38. data/lib/plurimath/math/function/arcsin.rb +6 -0
  39. data/lib/plurimath/math/function/arctan.rb +6 -0
  40. data/lib/plurimath/math/function/arg.rb +20 -0
  41. data/lib/plurimath/math/function/bar.rb +6 -0
  42. data/lib/plurimath/math/function/base.rb +6 -0
  43. data/lib/plurimath/math/function/binary_function.rb +4 -0
  44. data/lib/plurimath/math/function/cancel.rb +6 -0
  45. data/lib/plurimath/math/function/ceil.rb +6 -0
  46. data/lib/plurimath/math/function/color.rb +12 -0
  47. data/lib/plurimath/math/function/cos.rb +6 -0
  48. data/lib/plurimath/math/function/cosh.rb +6 -0
  49. data/lib/plurimath/math/function/cot.rb +6 -0
  50. data/lib/plurimath/math/function/coth.rb +6 -0
  51. data/lib/plurimath/math/function/csc.rb +6 -0
  52. data/lib/plurimath/math/function/csch.rb +6 -0
  53. data/lib/plurimath/math/function/ddot.rb +6 -0
  54. data/lib/plurimath/math/function/deg.rb +6 -0
  55. data/lib/plurimath/math/function/det.rb +6 -0
  56. data/lib/plurimath/math/function/dim.rb +6 -0
  57. data/lib/plurimath/math/function/dot.rb +6 -0
  58. data/lib/plurimath/math/function/exp.rb +6 -0
  59. data/lib/plurimath/math/function/fenced.rb +6 -0
  60. data/lib/plurimath/math/function/floor.rb +6 -0
  61. data/lib/plurimath/math/function/font_style/bold-fraktur.rb +6 -0
  62. data/lib/plurimath/math/function/font_style/bold-italic.rb +6 -0
  63. data/lib/plurimath/math/function/font_style/bold-sans-serif.rb +6 -0
  64. data/lib/plurimath/math/function/font_style/bold-script.rb +6 -0
  65. data/lib/plurimath/math/function/font_style/bold.rb +6 -0
  66. data/lib/plurimath/math/function/font_style/double_struck.rb +6 -0
  67. data/lib/plurimath/math/function/font_style/fraktur.rb +6 -0
  68. data/lib/plurimath/math/function/font_style/italic.rb +6 -0
  69. data/lib/plurimath/math/function/font_style/monospace.rb +6 -0
  70. data/lib/plurimath/math/function/font_style/normal.rb +6 -0
  71. data/lib/plurimath/math/function/font_style/sans-serif-bold-italic.rb +6 -0
  72. data/lib/plurimath/math/function/font_style/sans-serif-italic.rb +6 -0
  73. data/lib/plurimath/math/function/font_style/sans-serif.rb +6 -0
  74. data/lib/plurimath/math/function/font_style/script.rb +6 -0
  75. data/lib/plurimath/math/function/font_style.rb +4 -0
  76. data/lib/plurimath/math/function/frac.rb +6 -0
  77. data/lib/plurimath/math/function/gcd.rb +6 -0
  78. data/lib/plurimath/math/function/glb.rb +12 -0
  79. data/lib/plurimath/math/function/hat.rb +6 -0
  80. data/lib/plurimath/math/function/hom.rb +6 -0
  81. data/lib/plurimath/math/function/inf.rb +6 -0
  82. data/lib/plurimath/math/function/int.rb +7 -1
  83. data/lib/plurimath/math/function/intent.rb +35 -0
  84. data/lib/plurimath/math/function/ker.rb +6 -0
  85. data/lib/plurimath/math/function/lcm.rb +12 -0
  86. data/lib/plurimath/math/function/lg.rb +6 -0
  87. data/lib/plurimath/math/function/lim.rb +6 -0
  88. data/lib/plurimath/math/function/liminf.rb +6 -0
  89. data/lib/plurimath/math/function/limits.rb +6 -0
  90. data/lib/plurimath/math/function/limsup.rb +6 -0
  91. data/lib/plurimath/math/function/ln.rb +6 -0
  92. data/lib/plurimath/math/function/log.rb +6 -0
  93. data/lib/plurimath/math/function/longdiv.rb +6 -0
  94. data/lib/plurimath/math/function/lub.rb +6 -0
  95. data/lib/plurimath/math/function/max.rb +6 -0
  96. data/lib/plurimath/math/function/mbox.rb +6 -0
  97. data/lib/plurimath/math/function/menclose.rb +19 -0
  98. data/lib/plurimath/math/function/merror.rb +22 -0
  99. data/lib/plurimath/math/function/min.rb +6 -0
  100. data/lib/plurimath/math/function/mlabeledtr.rb +23 -0
  101. data/lib/plurimath/math/function/mod.rb +6 -0
  102. data/lib/plurimath/math/function/msgroup.rb +20 -0
  103. data/lib/plurimath/math/function/multiscript.rb +6 -0
  104. data/lib/plurimath/math/function/nary.rb +11 -0
  105. data/lib/plurimath/math/function/norm.rb +6 -0
  106. data/lib/plurimath/math/function/obrace.rb +6 -0
  107. data/lib/plurimath/math/function/oint.rb +7 -1
  108. data/lib/plurimath/math/function/over.rb +6 -0
  109. data/lib/plurimath/math/function/overleftrightarrow.rb +6 -0
  110. data/lib/plurimath/math/function/overset.rb +6 -0
  111. data/lib/plurimath/math/function/phantom.rb +6 -0
  112. data/lib/plurimath/math/function/power.rb +6 -0
  113. data/lib/plurimath/math/function/power_base.rb +6 -0
  114. data/lib/plurimath/math/function/prod.rb +7 -1
  115. data/lib/plurimath/math/function/right.rb +1 -1
  116. data/lib/plurimath/math/function/root.rb +6 -0
  117. data/lib/plurimath/math/function/rule.rb +20 -0
  118. data/lib/plurimath/math/function/scarries.rb +20 -0
  119. data/lib/plurimath/math/function/sec.rb +6 -0
  120. data/lib/plurimath/math/function/sech.rb +6 -0
  121. data/lib/plurimath/math/function/semantics.rb +20 -0
  122. data/lib/plurimath/math/function/sin.rb +6 -0
  123. data/lib/plurimath/math/function/sinh.rb +6 -0
  124. data/lib/plurimath/math/function/sqrt.rb +6 -0
  125. data/lib/plurimath/math/function/stackrel.rb +6 -0
  126. data/lib/plurimath/math/function/substack.rb +6 -0
  127. data/lib/plurimath/math/function/sum.rb +7 -1
  128. data/lib/plurimath/math/function/sup.rb +6 -0
  129. data/lib/plurimath/math/function/table/align.rb +6 -0
  130. data/lib/plurimath/math/function/table/array.rb +6 -0
  131. data/lib/plurimath/math/function/table/bmatrix.rb +6 -0
  132. data/lib/plurimath/math/function/table/cases.rb +6 -0
  133. data/lib/plurimath/math/function/table/eqarray.rb +6 -0
  134. data/lib/plurimath/math/function/table/matrix.rb +6 -0
  135. data/lib/plurimath/math/function/table/multline.rb +6 -0
  136. data/lib/plurimath/math/function/table/pmatrix.rb +6 -0
  137. data/lib/plurimath/math/function/table/split.rb +6 -0
  138. data/lib/plurimath/math/function/table/vmatrix.rb +6 -0
  139. data/lib/plurimath/math/function/table.rb +10 -0
  140. data/lib/plurimath/math/function/tan.rb +6 -0
  141. data/lib/plurimath/math/function/tanh.rb +6 -0
  142. data/lib/plurimath/math/function/ternary_function.rb +4 -0
  143. data/lib/plurimath/math/function/text.rb +6 -0
  144. data/lib/plurimath/math/function/tilde.rb +6 -0
  145. data/lib/plurimath/math/function/ubrace.rb +6 -0
  146. data/lib/plurimath/math/function/ul.rb +6 -0
  147. data/lib/plurimath/math/function/unary_function.rb +4 -0
  148. data/lib/plurimath/math/function/uncertainty.rb +655 -0
  149. data/lib/plurimath/math/function/underover.rb +6 -0
  150. data/lib/plurimath/math/function/underset.rb +6 -0
  151. data/lib/plurimath/math/function/vec.rb +6 -0
  152. data/lib/plurimath/math/function.rb +1 -0
  153. data/lib/plurimath/math/renderer.rb +95 -0
  154. data/lib/plurimath/math/semantic_tree.rb +331 -0
  155. data/lib/plurimath/math/symbols/symbol.rb +2 -0
  156. data/lib/plurimath/math.rb +26 -0
  157. data/lib/plurimath/mathml/translator.rb +32 -0
  158. data/lib/plurimath/mathml/uncertainty_marker.rb +118 -0
  159. data/lib/plurimath/omml/translator.rb +68 -0
  160. data/lib/plurimath/omml/utility.rb +9 -1
  161. data/lib/plurimath/symbol_documentation.rb +57 -0
  162. data/lib/plurimath/unicode_math/parsing_rules/common_rules.rb +2 -1
  163. data/lib/plurimath/unicode_math/parsing_rules/masked.rb +3 -0
  164. data/lib/plurimath/unicode_math/transform.rb +64 -3
  165. data/lib/plurimath/unicode_math/utility.rb +38 -0
  166. data/lib/plurimath/version.rb +1 -1
  167. data/lib/plurimath.rb +3 -0
  168. data/plurimath.gemspec +3 -1
  169. data/scripts/replay_asciimath_corpus.rb +611 -0
  170. metadata +30 -4
@@ -2,232 +2,49 @@
2
2
 
3
3
  module Plurimath
4
4
  class Asciimath
5
- class Parse < Parslet::Parser
6
- include Plurimath::BaseNumberPrefix::Parser
7
-
8
- rule(:td) { expression.as(:td) }
9
- rule(:base) { str("__|").absent? >> str("_") }
10
- rule(:power) { str("^") }
11
- rule(:space) { match(/\s+/) }
12
- rule(:comma) { str(",") >> space? }
13
- rule(:space?) { space.maybe }
14
- rule(:number) do
15
- hex_number |
16
- binary_number |
17
- octal_number |
18
- decimal_number.as(:number) |
19
- str(".").as(:symbol)
20
- end
21
-
22
- rule(:decimal_number) do
23
- (match("[0-9]").repeat(1) >> decimal_marker >> match("[0-9]").repeat(1)) |
24
- match("[0-9]").repeat(1)
25
- end
26
-
27
- rule(:decimal_operand) do
28
- (hex_number | binary_number | octal_number).absent? >> decimal_number
29
- end
30
-
31
- rule(:signed_number) { (str("-") >> decimal_operand).as(:number) }
32
- rule(:script_operand) { signed_number | sequence }
33
- rule(:mod_operand) do
34
- (str("-") >> decimal_operand.present? >> iteration).as(:negative_mod_operand) |
35
- iteration
36
- end
37
-
38
- rule(:controversial_symbols) { power_base | expression }
39
- rule(:left_right_open_paren) { str("(") | str("[") }
40
- rule(:left_right_close_paren) { str(")") | str("]") }
41
- rule(:color_left_parenthesis) { str("(") | str("[") | str("{") }
42
- rule(:color_right_parenthesis) { str(")") | str("]") | str("}") }
43
-
44
- rule(:ternary_classes_rules) do
45
- (str("left") >> space? >> left_right_open_paren.as(:left) >> space? >> (ternary_classes >> power_base >> space? >> iteration.as(:third_value).maybe).as(:ternary) >> space? >> str("right") >> space? >> left_right_close_paren.as(:right)) |
46
- (ternary_classes >> power_base >> space? >> iteration.as(:third_value).maybe).as(:ternary) |
47
- ternary_classes
48
- end
49
-
50
- rule(:ternary_classes) do
51
- arr_to_expression(Constants::TERNARY_CLASSES, :ternary_class)
52
- end
53
-
54
- rule(:binary_classes) do
55
- arr_to_expression(Constants::BINARY_CLASSES, :binary_class)
56
- end
57
-
58
- rule(:sub_sup_classes) do
59
- arr_to_expression(Constants::SUB_SUP_CLASSES, :binary_class)
60
- end
61
-
62
- rule(:open_table) do
63
- arr_to_expression(Constants::TABLE_PARENTHESIS.keys, :table_left)
64
- end
65
-
66
- rule(:close_table) do
67
- arr_to_expression(Constants::TABLE_PARENTHESIS.values, :table_right)
68
- end
69
-
70
- rule(:lparen) do
71
- Constants::PARENTHESIS.keys.reduce do |expression, parenthesis|
72
- expression = str(expression) if expression.is_a?(Symbol)
73
- expression | str(parenthesis)
74
- end
75
- end
76
-
77
- rule(:rparen) do
78
- Constants::PARENTHESIS.values.reduce do |expression, parenthesis|
79
- expression = str(expression) if expression.is_a?(String)
80
- expression | str(parenthesis)
81
- end
82
- end
83
-
84
- rule(:left_right) do
85
- (str("left") >> space? >> left_right_open_paren.as(:left) >> space? >> (iteration.maybe >> sequence.maybe).as(:left_right_value) >> space? >> str("right") >> space? >> left_right_close_paren.as(:right)) |
86
- ((table.as(:numerator) >> space? >> match(/(?<!\/)\/(?!\/)/) >> space? >> iteration.as(:denominator)).as(:frac) >> expression) |
87
- (table.as(:table) >> power_base.maybe >> expression.maybe)
88
- end
89
-
90
- rule(:quoted_text) do
91
- (str('"') >> str("unitsml(") >> (str(')"').absent? >> any).repeat.as(:unitsml) >> str(')"')) |
92
- (str('"') >> match("[^\"]").repeat.as(:text) >> str('"')) |
93
- (str('"') >> str("").as(:text))
94
- end
95
-
96
- rule(:symbol_text_or_integer) do
97
- sub_sup_classes |
98
- binary_classes |
99
- ternary_classes |
100
- hash_to_expression(Constants.precompile_constants) |
101
- (match(/[0-9]/).as(:number) >> (decimal_marker >> match("[0-9]")).absent? >> str(",").as(:comma)).repeat(1).as(:comma_separated) |
102
- quoted_text |
103
- (str("d").as(:d) >> str("x").as(:x)).as(:intermediate_exp) |
104
- ((str("left").absent? >> str("right").absent?) >> match["a-zA-Z"].as(:symbol)) |
105
- match(/[^\[{(\\\/@;:.,'"|\]})0-9a-zA-Z\-><$%^&*_=+!`~\s?ℒℛᑕᑐ]/).as(:symbol) |
106
- number
107
- end
108
-
109
- rule(:power_base) do
110
- (base >> space? >> script_operand.as(:base_value) >> power >> (power >> power.maybe).absent? >> space? >> script_operand.as(:power_value)) |
111
- (space? >> base >> space? >> script_operand.as(:base_value)).as(:base) |
112
- (space? >> power >> (power >> power.maybe).absent? >> space? >> script_operand.as(:power_value)).as(:power) |
113
- (space? >> base >> space? >> power.as(:symbol).as(:base_value)).as(:base) |
114
- (space? >> power >> (power >> power.maybe).absent? >> space? >> base.as(:symbol).as(:power_value)).as(:power)
115
- end
116
-
117
- rule(:power_base_rules) do
118
- (sub_sup_classes >> power_base).as(:power_base) |
119
- (binary_classes >> space? >> sequence.as(:base_value).maybe >> space? >> sequence.as(:power_value).maybe).as(:power_base) |
120
- (sequence.as(:power_base) >> power_base).as(:power_base)
121
- end
122
-
123
- rule(:table) do
124
- (str("{").as(:table_left) >> space? >> tr >> space? >> close_table.as(:table_right)) |
125
- (open_table.as(:table_left) >> space? >> tr >> space? >> close_table.as(:table_right)) |
126
- (str("norm").as(:norm) >> open_table.as(:table_left) >> space? >> tr >> space? >> close_table.as(:table_right)) |
127
- (str("|").as(:table_left) >> space? >> tr >> space? >> str("|").as(:table_right)) |
128
- (str("left") >> space? >> left_right_open_paren.as(:left) >> space? >> tr >> space? >> str("right") >> space? >> left_right_close_paren.as(:right))
129
- end
130
-
131
- rule(:tr) do
132
- ((left_right_open_paren.as(:open_tr) >> td.as(:tds_list) >> left_right_close_paren).as(:table_row) >> space? >> comma >> space? >> tr.as(:expr)) |
133
- (left_right_open_paren.as(:open_tr) >> td.as(:tds_list) >> left_right_close_paren).as(:table_row)
134
- end
135
-
136
- rule(:color_value) do
137
- (color_left_parenthesis.capture(:paren).as(:lparen) >> expression.as(:rgb_color) >> color_right_parenthesis.maybe.as(:rparen)).as(:intermediate_exp) |
138
- iteration
139
- end
140
-
141
- rule(:sequence) do
142
- (lparen.as(:lparen) >> space? >> expression.maybe.as(:expr) >> space? >> rparen.maybe.as(:rparen)).as(:intermediate_exp) |
143
- (str("text") >> lparen.capture(:paren) >> read_text.as(:text) >> rparen.maybe).as(:intermediate_exp) |
144
- symbol_text_or_integer
145
- end
146
-
147
- rule(:frac) do
148
- (sequence.as(:numerator) >> space? >> match(/(?<!\/)\/(?!\/)/) >> space? >> iteration.as(:denominator)).as(:frac) |
149
- ((power_base_rules | power_base).as(:numerator) >> space? >> match(/(?<!\/)\/(?!\/)/) >> space? >> iteration.as(:denominator)).as(:frac)
150
- end
151
-
152
- rule(:mod) do
153
- (sequence.as(:dividend) >> space? >> str("mod").as(:mod) >> space? >> mod_operand.as(:divisor)).as(:mod) |
154
- ((power_base_rules >> power_base).as(:dividend) >> space? >> str("mod").as(:mod) >> space? >> mod_operand.as(:divisor)).as(:mod) |
155
- (power_base_rules.as(:dividend) >> space? >> str("mod").as(:mod) >> space? >> mod_operand.as(:divisor)).as(:mod)
156
- end
157
-
158
- rule(:iteration) do
159
- ternary_classes_rules |
160
- (table.as(:table) >> power_base.maybe) |
161
- comma.as(:comma) |
162
- mod |
163
- (sequence.as(:sequence) >> space? >> str("//").as(:symbol)) |
164
- (str("color") >> color_value.as(:color) >> sequence.as(:color_value)) |
165
- frac |
166
- (power_base_rules >> power_base) |
167
- power_base_rules |
168
- sequence.as(:sequence) |
169
- space
170
- end
171
-
172
- rule(:expression) do
173
- left_right.as(:left_right) |
174
- (iteration >> space? >> expression).as(:expr) |
175
- (base.as(:symbol) >> expression.maybe).as(:expr) |
176
- (power.as(:symbol) >> expression.maybe).as(:expr) |
177
- str("") |
178
- (rparen.as(:rparen) >> space? >> controversial_symbols >> comma.as(:comma).maybe >> expression).repeat(1).as(:expr) |
179
- (power.as(:symbol) >> space? >> expression).as(:expr) |
180
- (table.as(:table) >> space? >> rparen.as(:rparen) >> space? >> expression.as(:expr).maybe) |
181
- comma.as(:comma).maybe
182
- end
183
-
184
- root :expression
185
-
186
- def arr_to_expression(arr, name = nil)
187
- type = arr.first.class
188
- arr.reduce do |expression, expr_string|
189
- expression = str(expression).as(name) if expression.is_a?(type)
190
- expression | str(expr_string).as(name)
191
- end
192
- end
193
-
194
- def read_text
195
- dynamic do |_sour, context|
196
- rparen = Constants::PARENTHESIS[context.captures[:paren].to_sym]
197
- match("[^#{rparen}]").repeat
198
- end
199
- end
200
-
201
- def hash_to_expression(arr)
202
- type = arr.first.class
203
- @@expression ||= arr.reduce do |expression, expr_string|
204
- expression = dynamic_parser_rules(expression) if expression.is_a?(type)
205
- expression | dynamic_parser_rules(expr_string)
206
- end
207
- end
208
-
209
- def dynamic_parser_rules(expr)
210
- first_value = str(expr.first.to_s)
211
- case expr.last
212
- when :symbol then (str("\\").as(:slash) >> match("\s").repeat >> str("\n")) | first_value.as(:symbol)
213
- when :unary_class then unary_functions(first_value)
214
- when :fonts then first_value.as(:fonts_class) >> space? >> sequence.as(:fonts_value)
215
- when :special_fonts then first_value.as(:bold_fonts)
216
- end
217
- end
218
-
219
- def decimal_marker
220
- str(Plurimath.configuration.decimal)
221
- end
222
-
223
- def unary_functions(first_value)
224
- if ["'underbrace'", "'ubrace'"].include?(first_value.to_s)
225
- (first_value.as(:unary_class) >> space? >> str("_").as(:symbol)).as(:unary) |
226
- (first_value.as(:unary_class) >> space? >> sequence.maybe).as(:unary)
227
- else
228
- (first_value.as(:unary_class).as(:power_base) >> space? >> power_base).as(:unary) |
229
- (first_value.as(:unary_class) >> space? >> sequence.maybe).as(:unary)
230
- end
5
+ # The AsciiMath grammar lives in the amath gem. This class keeps the
6
+ # tree-level entry point for callers that parse without transforming
7
+ # (plurimath-testsuite's corpus generator calls Parse.new.parse): it
8
+ # returns the Parslet-shaped tree Asciimath::Transform expects, and
9
+ # raises Parslet::ParseFailed with a cause at amath's rejection position.
10
+ class Parse
11
+ # amath is not released yet, so the Gemfile line points at GitHub.
12
+ AMATH_GEMFILE_LINE = 'gem "amath", github: "plurimath/amath"'
13
+
14
+ def parse(text)
15
+ load_amath
16
+ parse_with_amath(text)
17
+ end
18
+
19
+ private
20
+
21
+ # Loaded here, outside parse_with_amath, so its `rescue ::Amath::...`
22
+ # clause is never evaluated while Amath is undefined.
23
+ def load_amath
24
+ require "amath"
25
+ rescue LoadError => e
26
+ raise unless e.path == "amath" || e.message.end_with?("-- amath")
27
+
28
+ raise Math::MissingDependencyError.new(
29
+ :asciimath,
30
+ format_name: "AsciiMath",
31
+ gem_name: "amath",
32
+ gemfile_line: AMATH_GEMFILE_LINE,
33
+ ), cause: nil
34
+ end
35
+
36
+ def parse_with_amath(text)
37
+ tree = ::Amath.parse(text, decimal: Plurimath.configuration.decimal)
38
+ SliceAdapter.convert(tree, text)
39
+ rescue ::Amath::ParseError => e
40
+ raise parse_failed(e, text)
41
+ end
42
+
43
+ def parse_failed(error, text)
44
+ position = Parslet::Position.new(text, error.byte_offset)
45
+ cause = Parslet::Cause.new(error.message, Parslet::Source.new(text),
46
+ position, [])
47
+ Parslet::ParseFailed.new(error.message, cause)
231
48
  end
232
49
  end
233
50
  end
@@ -3,6 +3,10 @@
3
3
  module Plurimath
4
4
  class Asciimath
5
5
  class Parser
6
+ # An uncertainty call. amath splits letters, so xustd(1,5) is x
7
+ # followed by ustd(1,5): a preceding letter does not exempt it.
8
+ UNCERTAINTY_CALL = /u(?:std|exp|cov)\(/
9
+
6
10
  attr_accessor :text
7
11
 
8
12
  def initialize(text)
@@ -15,12 +19,65 @@ module Plurimath
15
19
  end
16
20
 
17
21
  def parse
18
- nodes = Parse.new.parse(text)
22
+ ambiguous_uncertainty_comma!
23
+ nodes = parse_tree
19
24
  transformed_tree = Transform.new.apply(nodes)
20
25
  return transformed_tree if transformed_tree.is_a?(Math::Formula)
21
26
 
22
27
  Math::Formula.new(transformed_tree)
23
28
  end
29
+
30
+ private
31
+
32
+ def parse_tree
33
+ Parse.new.parse(text)
34
+ end
35
+
36
+ # Uncertainty arguments use "." as the decimal mark and "," between
37
+ # arguments. Under a locale whose decimal mark is not ".", a separator
38
+ # comma directly followed by a digit (ustd(1,5), uexp(10,0, 0,3, 2))
39
+ # most likely is a decimal comma: amath would read it as a separator,
40
+ # or give up on the call and read letters (ustd(1,230, 0,004) under fr).
41
+ # Raise instead of guessing; other input is not checked.
42
+ def ambiguous_uncertainty_comma!
43
+ decimal = Plurimath.configuration.decimal
44
+ return if decimal == "." || text.nil?
45
+
46
+ text.to_enum(:scan, UNCERTAINTY_CALL).each do
47
+ start = Regexp.last_match.begin(0)
48
+ next if text[0...start].count('"').odd?
49
+
50
+ call = uncertainty_call(start, Regexp.last_match.end(0))
51
+ next unless call
52
+
53
+ raise Math::AmbiguousDecimalError.new(
54
+ "uncertainty forms use '.' as the decimal mark; the comma " \
55
+ "before a digit in #{call} is ambiguous with decimal " \
56
+ "'#{decimal}' (locale #{Plurimath.configuration.locale}), " \
57
+ "write the value with '.' and separate arguments with ', '",
58
+ )
59
+ end
60
+ end
61
+
62
+ # The call text from start to its closing parenthesis if a top-level
63
+ # argument separator in it is directly followed by a digit, else nil.
64
+ def uncertainty_call(start, index)
65
+ depth = 1
66
+ quoted = false
67
+ ambiguous = false
68
+ while index < text.length && depth.positive?
69
+ char = text[index]
70
+ quoted = !quoted if char == '"'
71
+ unless quoted
72
+ depth += 1 if char == "("
73
+ depth -= 1 if char == ")"
74
+ ambiguous ||= depth == 1 && char == "," &&
75
+ text[index + 1].to_s.match?(/\A[0-9]\z/)
76
+ end
77
+ index += 1
78
+ end
79
+ text[start...index] if ambiguous
80
+ end
24
81
  end
25
82
  end
26
83
  end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ class Asciimath
5
+ # Turns a parse tree produced by amath (Parsanol) into a Parslet-shaped
6
+ # tree (Symbol keys, Parslet::Slice leaves), so Asciimath::Transform and
7
+ # the other Parslet::Slice consumers keep working unchanged.
8
+ #
9
+ # - Every leaf becomes a real Parslet::Slice. Parsanol slices carry a byte
10
+ # offset into the parsed string; Parslet::Position also stores a byte
11
+ # offset and derives the character offset (Slice#offset) from it, so the
12
+ # byte offset is kept and #offset answers in characters, as Parslet does.
13
+ # - Plain String leaves (Parsanol returns a bare "" for an empty match)
14
+ # become Parslet::Slice at the end of the last leaf seen so far, in tree
15
+ # order; for empty input that is ""@0, as Parslet gives.
16
+ # - Hashes keep their key order (String keys become Symbols, as Parslet
17
+ # always uses Symbols), arrays keep their order, [] stays [], nil stays
18
+ # nil.
19
+ module SliceAdapter
20
+ class OffsetError < StandardError; end
21
+
22
+ module_function
23
+
24
+ def convert(tree, input)
25
+ input = input.to_s
26
+ line_cache = Parslet::Source::LineCache.new
27
+ line_cache.scan_for_line_endings(0, input)
28
+ Walker.new(input, line_cache).walk(tree)
29
+ end
30
+
31
+ class Walker
32
+ def initialize(input, line_cache)
33
+ @input = input
34
+ @line_cache = line_cache
35
+ @cursor = 0
36
+ end
37
+
38
+ # Depth-first with an explicit stack instead of recursion: a long
39
+ # AsciiMath sum is a tree about two levels deep per term, which would
40
+ # overflow a small (1 MiB, as on Windows) stack. Children are visited
41
+ # in the same order as a recursive walk, so the cursor that places
42
+ # empty-string leaves sees the leaves in the same order.
43
+ def walk(root)
44
+ return leaf(root) unless container?(root)
45
+
46
+ result = empty_copy(root)
47
+ stack = [[entries(root), result, 0]]
48
+ until stack.empty?
49
+ frame = stack.last
50
+ items, out, index = frame
51
+ next stack.pop if index == items.size
52
+
53
+ frame[2] = index + 1
54
+ key, value = items[index]
55
+ if container?(value)
56
+ child = empty_copy(value)
57
+ store(out, key, child)
58
+ stack.push([entries(value), child, 0])
59
+ else
60
+ store(out, key, leaf(value))
61
+ end
62
+ end
63
+ result
64
+ end
65
+
66
+ private
67
+
68
+ def container?(node)
69
+ node.is_a?(Hash) || node.is_a?(Array)
70
+ end
71
+
72
+ def empty_copy(node)
73
+ node.is_a?(Hash) ? {} : []
74
+ end
75
+
76
+ def entries(node)
77
+ node.is_a?(Hash) ? node.to_a : node.each_with_index.map { |v, i| [i, v] }
78
+ end
79
+
80
+ def store(out, key, value)
81
+ if out.is_a?(Hash)
82
+ out[symbol_key(key)] = value
83
+ else
84
+ out << value
85
+ end
86
+ end
87
+
88
+ def leaf(node)
89
+ case node
90
+ when ::Parslet::Slice then keep(node)
91
+ when String then slice(@cursor, node)
92
+ when nil then nil
93
+ else
94
+ return slice_from(node) if foreign_slice?(node)
95
+
96
+ node
97
+ end
98
+ end
99
+
100
+ def keep(node)
101
+ advance(node.position.bytepos, node.to_s)
102
+ node
103
+ end
104
+
105
+ def symbol_key(key)
106
+ key.is_a?(String) ? key.to_sym : key
107
+ end
108
+
109
+ def foreign_slice?(node)
110
+ node.respond_to?(:offset) && node.respond_to?(:to_str)
111
+ end
112
+
113
+ def slice_from(node)
114
+ content = node.to_str
115
+ bytepos = node.offset
116
+ unless @input.byteslice(bytepos, content.bytesize) == content
117
+ raise OffsetError,
118
+ "slice #{content.inspect} does not match the input at " \
119
+ "byte #{bytepos}"
120
+ end
121
+
122
+ slice(bytepos, content)
123
+ end
124
+
125
+ def slice(bytepos, content)
126
+ advance(bytepos, content)
127
+ position = ::Parslet::Position.new(@input, bytepos)
128
+ ::Parslet::Slice.new(position, content.to_s.dup, @line_cache)
129
+ end
130
+
131
+ def advance(bytepos, content)
132
+ finish = bytepos + content.to_s.bytesize
133
+ @cursor = finish if finish > @cursor
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end