plurimath 0.11.11 → 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 +12 -5
  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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bdd1dc4754cef478b4fbc48be08d8b4355d3c1e7efba1f4d30060e022daa04e
4
- data.tar.gz: 8c1a40edf9ae2853b16f28b64d3c61510486b4aad50f8d7367f21053df5be775
3
+ metadata.gz: e4112028ba3d71553b4695b371e34b8ba481739085e6d330fcba281970482f35
4
+ data.tar.gz: 1bf3a115ef3dc443d1c02d7e15d288f336177830dbcccbc00b68309c8cf05352
5
5
  SHA512:
6
- metadata.gz: 92cf5add4b7acff47121e6a1557fc602f64ef286e57b0611397fe843f5e203722bc65b3eaa489e269aff63a8fb03cfc9a7595f9f9ba899c8cfd9eafb6093d408
7
- data.tar.gz: e69b68ba523da69b9cac43912d52665f0609de0906928bdd59f5a9711971747ef1083a22f84ccf6e269428dd8b65e2d470c3d2cb51f49ba0c69256164f610e04
6
+ metadata.gz: 28c135b5a874a229183a29adb31d1ef34ad2b700927800c6e557e83a2fa681afb959e3c95f7eb696c0fdaa7f5a2daeb7d7784e11f5db6f1f75e26a3de8ea1e11
7
+ data.tar.gz: 04fb461b9990cc43cb3ef39b5948eb76248e175d3d5bb8bf34a5d83958ed4fe29b843d06aa5c02c3f85684dce26b93c88ad16f8ba6ddcbb7168642c59fb1875c
@@ -14,4 +14,11 @@ jobs:
14
14
  uses: metanorma/ci/.github/workflows/dependent-rake.yml@main
15
15
  with:
16
16
  command: bundle exec rspec
17
+ # Remove the source bootstrap once the amath runtime dependency in #512 is available.
18
+ after-setup-ruby: |
19
+ set -euo pipefail
20
+ git init ../amath
21
+ git -C ../amath fetch --depth 1 https://github.com/plurimath/amath.git b67f924477ce129e85926eda5973d771c007dfec
22
+ git -C ../amath checkout --detach FETCH_HEAD
23
+ printf '\ngem "amath", path: "../amath"\ngem "parsanol", "~> 1.3", ">= 1.3.112"\n' >> Gemfile
17
24
  repos-json-file: .github/workflows/dependent-repos-ruby.json
@@ -0,0 +1,24 @@
1
+ name: Shared conformance suite
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ conformance:
13
+ runs-on: ubuntu-22.04
14
+ timeout-minutes: 45
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ with:
18
+ submodules: recursive
19
+ - uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: "4.0.1"
22
+ bundler-cache: true
23
+ - name: Run shared YAML conformance cases
24
+ run: bundle exec rspec spec/conformance --format progress
data/.gitmodules CHANGED
@@ -1,3 +1,6 @@
1
1
  [submodule "submodules/unicodemath-tests"]
2
2
  path = submodules/unicodemath-tests
3
3
  url = git@github.com:plurimath/unicodemath-tests.git
4
+ [submodule "submodules/plurimath-testsuite"]
5
+ path = submodules/plurimath-testsuite
6
+ url = https://github.com/plurimath/plurimath-testsuite.git
data/.rspec-opal CHANGED
@@ -9,3 +9,6 @@
9
9
  --format documentation
10
10
  --color
11
11
  --require spec_helper
12
+
13
+ # The shared corpus adapter uses native filesystem/YAML validation.
14
+ --exclude-pattern=spec/conformance/**/*_spec.rb
data/Gemfile CHANGED
@@ -4,11 +4,29 @@ source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  gem "canon"
7
+ # Optional native image-rendering backend for `Formula#render` / `plurimath
8
+ # render`. Git-pinned until `lasem` ships to RubyGems; intentionally NOT a
9
+ # gemspec runtime dependency so Opal/plurimath-js and lasem-less installs keep
10
+ # working. For local dev against an in-progress checkout:
11
+ # bundle config set --local local.lasem /path/to/lasem-ruby
12
+ gem "lasem", github: "plurimath/lasem-ruby", branch: "main", submodules: true
7
13
  # gem "lutaml-model", github: "lutaml/lutaml-model", branch: "main"
8
14
  gem "oga"
15
+ # spec_helper selects the Nokogiri adapter directly.
16
+ gem "nokogiri"
17
+ # AsciiMath parser; tracks amath main until a release is published.
18
+ # AMATH_PATH points at a local amath checkout instead.
19
+ if ENV["AMATH_PATH"]
20
+ gem "amath", path: ENV["AMATH_PATH"]
21
+ else
22
+ gem "amath", github: "plurimath/amath", ref: "b67f924477ce129e85926eda5973d771c007dfec"
23
+ end
9
24
  # gem "omml", github: "plurimath/omml", branch: "main"
10
25
  gem "opal-rspec", "~> 1.1.0a"
11
26
  gem "ox"
27
+ # Parsanol::Slice for the amath SliceAdapter specs.
28
+ # 1.3.112 restores the v1 edition default (1.3.110 and 1.3.111 are yanked).
29
+ gem "parsanol", "~> 1.3", ">= 1.3.112"
12
30
  gem "rake", "~> 12.0"
13
31
  gem "rspec", "~> 3.0"
14
32
  gem "rubocop-performance"
data/README.adoc CHANGED
@@ -13,7 +13,8 @@ remains consistent and accurate regardless of the format it is presented in.
13
13
  Supported math representational languages:
14
14
 
15
15
  * https://www.w3.org/TR/MathML3/[MathML 3] (and https://www.w3.org/TR/mathml4/[MathML 4])
16
- * https://www.asciimath.org[AsciiMath]
16
+ * https://www.asciimath.org[AsciiMath] (parsed by the
17
+ https://github.com/plurimath/amath[amath] gem)
17
18
  * http://unicodemath.org[UnicodeMath] (https://www.unicode.org/notes/tn28/UTN28-PlainTextMath-v3.2.pdf[UnicodeMath v3.2])
18
19
  * LaTeX math
19
20
  * Microsoft Office Math Markup Language (OMML) https://devblogs.microsoft.com/math-in-office/officemath/["OfficeMath"]
@@ -38,6 +39,13 @@ Accessibility:: Convert mathematical content into formats that are more
38
39
  accessible for screen readers and other assistive technologies.
39
40
 
40
41
 
42
+ == Reference catalog
43
+
44
+ `Plurimath::Catalog.entries` generates reference data for documented functions,
45
+ including each serializer's output and separate availability notes. See
46
+ link:docs/catalog.adoc[the Ruby catalog contract] for the entry schema and export rules.
47
+
48
+
41
49
  == Installation
42
50
 
43
51
  Add this line to your application's Gemfile:
@@ -142,6 +150,68 @@ plurimath help convert
142
150
  ----
143
151
 
144
152
 
153
+ === Rendering images
154
+
155
+ The `render` command rasterizes or vectorizes a formula to an image
156
+ (`svg`, `png`, `pdf`, or `ps`) using the optional
157
+ https://github.com/plurimath/lasem-ruby[`lasem`] gem (a native extension
158
+ wrapping the Lasem C library). Image rendering is OPTIONAL: Plurimath works
159
+ fully without it, and it is unavailable under Opal/plurimath-js. See
160
+ <<image-rendering,Rendering to images>> for installing the backend.
161
+
162
+ [source,bash]
163
+ ----
164
+ plurimath render [options]
165
+ ----
166
+
167
+ Options mirror `convert` for the input side (`-i`, `-f`, `-p`, `-e`), plus:
168
+
169
+ `-o`, `--output <FILE>`::
170
+ Output file path. The image format is taken from its extension
171
+ (`.svg`/`.png`/`.pdf`/`.ps`); an extension outside that set is an error. When
172
+ omitted, the rendered bytes are written to standard output.
173
+
174
+ `-r`, `--format <FORMAT>`::
175
+ Image format (`svg`, `png`, `pdf`, `ps`). Used when `--output` has no usable
176
+ extension or when writing to standard output (a file's extension takes
177
+ precedence). Defaults to `svg`.
178
+
179
+ `-d`, `--display-style`::
180
+ DisplayStyle, boolean. Defaults to the parsed formula's display style.
181
+
182
+ `-s`, `--split-on-linebreak`::
183
+ Boolean. Note that lasem renders a single MathML document, so this mainly
184
+ affects the serialized MathML rather than producing multiple images.
185
+
186
+ `-e`, `--xml-engine <ENGINE>`::
187
+ XML engine used to parse the input (`ox` or `oga`). Defaults to `ox`.
188
+
189
+ `--ppi`, `--zoom`, `--width`, `--height`, `--offset-x`, `--offset-y`::
190
+ Numeric geometry options passed through to lasem; omit them to use lasem's
191
+ defaults.
192
+
193
+ [example]
194
+ .Render an AsciiMath equation to a PNG file
195
+ ====
196
+ [source,bash]
197
+ ----
198
+ plurimath render -i "sqrt(x^2 + y^2)" -f asciimath -o formula.png --ppi 144
199
+ ----
200
+ ====
201
+
202
+ [example]
203
+ .Render a LaTeX file to SVG on standard output
204
+ ====
205
+ [source,bash]
206
+ ----
207
+ plurimath render -p formula.tex -f latex -r svg > formula.svg
208
+ ----
209
+ ====
210
+
211
+ If the `lasem` backend is unavailable, the format is unsupported, or rendering
212
+ fails, the command prints a message and exits with a non-zero status.
213
+
214
+
145
215
  == Ruby API
146
216
 
147
217
 
@@ -457,6 +527,69 @@ omml = formula.to_omml
457
527
  ----
458
528
 
459
529
 
530
+ [#image-rendering]
531
+ === Rendering to images
532
+
533
+ `Formula#render` produces a binary image (`:svg`, `:png`, `:pdf`, or `:ps`) by
534
+ serializing the formula to MathML and handing it to the optional
535
+ https://github.com/plurimath/lasem-ruby[`lasem`] gem, a native extension
536
+ wrapping the Lasem C library. `lasem` is an OPTIONAL, soft dependency: it is not
537
+ a runtime dependency of Plurimath, is unavailable under Opal/plurimath-js, and
538
+ must be added explicitly.
539
+
540
+ Until `lasem` is published to RubyGems, pin it via Git in your `Gemfile`:
541
+
542
+ [source,ruby]
543
+ ----
544
+ gem "lasem", github: "plurimath/lasem-ruby", branch: "main", submodules: true
545
+ ----
546
+
547
+ Then verify the native toolchain (cairo, pango, and the Lasem C library):
548
+
549
+ [source,sh]
550
+ ----
551
+ bundle exec lasem-doctor
552
+ ----
553
+
554
+ Render with `Formula#render`:
555
+
556
+ [source,ruby]
557
+ ----
558
+ formula = Plurimath::Math.parse("sum_(i=1)^n i", "asciimath")
559
+
560
+ svg = formula.render(format: :svg)
561
+ png = formula.render(format: :png, ppi: 144, zoom: 2)
562
+ File.binwrite("sum.png", png)
563
+ ----
564
+
565
+ Or render directly from source in one step with `Plurimath::Math.render`, which
566
+ parses and then renders (parse options such as `:locale` are routed to parsing):
567
+
568
+ [source,ruby]
569
+ ----
570
+ png = Plurimath::Math.render("sum_(i=1)^n i", "asciimath", format: :png, ppi: 144)
571
+ File.binwrite("sum.png", png)
572
+
573
+ # Check availability before rendering to avoid a raise:
574
+ Plurimath::Math.render_available? # => true / false (always false under Opal)
575
+ ----
576
+
577
+ Supported `format:` values are `:svg`, `:png`, `:pdf`, `:ps`. Remaining keyword
578
+ options are split between the MathML serialization (`:display_style`,
579
+ `:intent`, `:formatter`, `:unitsml`, `:unary_function_spacing`,
580
+ `:split_on_linebreak`) and the lasem renderer (`:ppi`, `:zoom`, `:width`,
581
+ `:height`, `:offset_x`, `:offset_y`); omit the geometry options to use lasem's
582
+ defaults. When `:display_style` is omitted the formula's own display style is
583
+ used.
584
+
585
+ If `lasem` or its native extension is unavailable, the format is unsupported, or
586
+ rendering fails, a `Plurimath::Errors` rendering error is raised
587
+ (`RenderingUnavailable`, `UnsupportedRenderFormat`, or `RenderingFailed` — all
588
+ subclasses of `Plurimath::Error`); the unavailable message points you to
589
+ `lasem-doctor`. A malformed formula surfaces as `Plurimath::Math::ParseError`
590
+ from the MathML step, not as a rendering error.
591
+
592
+
460
593
  == Number formatting
461
594
 
462
595
  === Introduction
@@ -2099,8 +2232,29 @@ of the parsed mathematical expression.
2099
2232
  ----
2100
2233
  formula = Plurimath::Math.parse("sin(1)", :asciimath)
2101
2234
  formula.to_display(:formula)
2102
- # ...
2103
- ----
2235
+ # Plurimath::Math::Formula
2236
+ # value[0]: Plurimath::Math::Function::Sin
2237
+ # parameter_one: Plurimath::Math::Function::Fenced
2238
+ # parameter_one: Plurimath::Math::Symbols::Paren::Lround
2239
+ # value: nil
2240
+ # parameter_two[0]: Plurimath::Math::Number
2241
+ # value: "1"
2242
+ # parameter_three: Plurimath::Math::Symbols::Paren::Rround
2243
+ # value: nil
2244
+ ----
2245
+
2246
+ This tree shows the stored Ruby objects, with their full class names and ordered
2247
+ content fields. Array entries have indexes; empty arrays are shown as `[]` and
2248
+ absent values as `nil`. Some symbol classes, such as `Lround`, represent their
2249
+ symbol through the class itself and store no literal value. String literals are
2250
+ escaped, so quotes, backslashes and newlines stay on one tree line.
2251
+
2252
+ The tree preserves `Formula` and `Fenced` wrappers and does not group operators:
2253
+ the stored siblings in `a+b` remain siblings. It includes `value`, function
2254
+ parameters, table delimiters, UnitsML text and the declared uncertainty fields.
2255
+ Formatting flags and options, original input text, XML internals and rendering
2256
+ caches are omitted. Repeated references appear at each position; a reference to
2257
+ an ancestor is marked `<cycle: ClassName>`.
2104
2258
 
2105
2259
  === Displaying in the math language of expression
2106
2260
 
@@ -2131,6 +2285,107 @@ formula.to_display(:mathml)
2131
2285
  # |_ "<mtext>1</mtext>" text
2132
2286
  ----
2133
2287
 
2288
+ === Displaying a semantic operator tree
2289
+
2290
+ Use `tree: :semantic` to see which operands each operator owns. The default
2291
+ `tree: :display` keeps the display tree shown above. Both views support
2292
+ `:asciimath`, `:latex`, `:mathml`, `:omml`, and `:unicodemath`; semantic expression,
2293
+ operand-role and leaf labels use the existing writer for the selected language.
2294
+
2295
+ [source,ruby]
2296
+ ----
2297
+ formula = Plurimath::Math.parse("a+b*c", :asciimath)
2298
+ puts formula.to_display(:unicodemath, tree: :semantic)
2299
+ # |_ Semantic tree
2300
+ # |_ expression "a + b × c"
2301
+ # |_ add "+"
2302
+ # |_ symbol "a"
2303
+ # |_ multiply
2304
+ # |_ symbol "b"
2305
+ # |_ symbol "c"
2306
+ ----
2307
+
2308
+ The semantic view shares numeric evaluation's precedence parser, but does not
2309
+ evaluate numbers or require variable bindings. It distinguishes explicit and
2310
+ implicit multiplication, retains explicit round groups, and preserves the
2311
+ parser's left nesting for chained subtraction, powers, and relations. Fractions,
2312
+ radicals, scripts, function arguments, and integral limits remain attached to
2313
+ their owning nodes. Fractions expose numerator and denominator roles; functions
2314
+ expose their name and argument; powers expose base and script. A square root
2315
+ exposes the implicit-degree placeholder `⬚` and its radicand. Unbounded and
2316
+ partly bounded integrals, sums and products also retain their body, and a powered
2317
+ function such as `sin^2 x` binds `x` as the function argument inside the power.
2318
+
2319
+ For example, the original integral expression produces this UnicodeMath view:
2320
+
2321
+ [source,ruby]
2322
+ ----
2323
+ formula = Plurimath::Math.parse(
2324
+ '1/(2pi) int_0^(2pi) (bbb"d" theta) / (a + b sin⁡ theta) = 1/sqrt(a^2−b^2)',
2325
+ :asciimath,
2326
+ )
2327
+ puts formula.to_display(:unicodemath, tree: :semantic)
2328
+ # |_ Semantic tree
2329
+ # |_ expression "(1)/(2 π) ∫_(0)^(2 π)▒〖(\\Bbb\"d\" θ)/(a + b sin⁡θ)〗 = (1)/(√(a^(2) − b^(2)))"
2330
+ # |_ relation "="
2331
+ # |_ implicit multiply
2332
+ # |_ fraction "(1)/(2 π)"
2333
+ # |_ numerator "1"
2334
+ # |_ number "1"
2335
+ # |_ denominator "2 π"
2336
+ # |_ implicit multiply
2337
+ # |_ number "2"
2338
+ # |_ symbol "π"
2339
+ # |_ integral "∫_(0)^(2 π)▒〖(\\Bbb\"d\" θ)/(a + b sin⁡θ)〗"
2340
+ # |_ lower limit "0"
2341
+ # |_ number "0"
2342
+ # |_ upper limit "2 π"
2343
+ # |_ implicit multiply
2344
+ # |_ number "2"
2345
+ # |_ symbol "π"
2346
+ # |_ integrand "(\\Bbb\"d\" θ)/(a + b sin⁡θ)"
2347
+ # |_ fraction "(\\Bbb\"d\" θ)/(a + b sin⁡θ)"
2348
+ # |_ numerator "\\Bbb\"d\" θ"
2349
+ # |_ implicit multiply
2350
+ # |_ symbol "\\Bbb\"d\""
2351
+ # |_ symbol "θ"
2352
+ # |_ denominator "a + b sin⁡θ"
2353
+ # |_ add "+"
2354
+ # |_ symbol "a"
2355
+ # |_ implicit multiply
2356
+ # |_ symbol "b"
2357
+ # |_ apply "sin⁡θ"
2358
+ # |_ function name "sin"
2359
+ # |_ argument "θ"
2360
+ # |_ symbol "θ"
2361
+ # |_ fraction "(1)/(√(a^(2) − b^(2)))"
2362
+ # |_ numerator "1"
2363
+ # |_ number "1"
2364
+ # |_ denominator "√(a^(2) − b^(2))"
2365
+ # |_ square root "√(a^(2) − b^(2))"
2366
+ # |_ degree "⬚"
2367
+ # |_ radicand "a^(2) − b^(2)"
2368
+ # |_ subtract "−"
2369
+ # |_ power "a^(2)"
2370
+ # |_ base "a"
2371
+ # |_ symbol "a"
2372
+ # |_ script "2"
2373
+ # |_ number "2"
2374
+ # |_ power "b^(2)"
2375
+ # |_ base "b"
2376
+ # |_ symbol "b"
2377
+ # |_ script "2"
2378
+ # |_ number "2"
2379
+ ----
2380
+
2381
+ The double-struck `d` remains a styled symbol multiplied by `theta`; the view
2382
+ does not infer a differential. It builds a temporary rendering view and leaves
2383
+ the stored Formula model unchanged. This is not a general symbolic algebra
2384
+ engine: unsupported or ambiguous constructs, including tables, text runs,
2385
+ non-round fences, missing operands, and adjacent number literals, raise
2386
+ `Plurimath::Errors::UnsupportedSemanticTreeError` naming the construct. Unknown
2387
+ `tree:` values raise `ArgumentError`.
2388
+
2134
2389
  // == Integration
2135
2390
 
2136
2391
  // Integrate Plurimath into your project by requiring the gem and using its
@@ -2375,6 +2630,45 @@ NOTE: To regenerate these files, delete them and run:
2375
2630
  * link:OMML-Supported-Data.adoc[OMML Supported Data]
2376
2631
  * link:UnitsML-Supported-Data.adoc[UnitsML Supported Data]
2377
2632
 
2633
+ == Shared conformance fixtures
2634
+
2635
+ The language-independent YAML expectations used by the TypeScript port are
2636
+ pinned at `submodules/plurimath-testsuite`. Initialize the test data before
2637
+ running specs:
2638
+
2639
+ [source,sh]
2640
+ ----
2641
+ git submodule update --init --recursive
2642
+ bundle exec rspec spec/conformance
2643
+ ----
2644
+
2645
+ The conformance specs validate the dataset and compare preprocessing, parser
2646
+ trees, formula models, rendering outcomes, rejection positions and recorded
2647
+ formatter/UnitsML calls. Cases are read directly from the pinned repository;
2648
+ the test run never regenerates expectations. Missing, empty or unsupported
2649
+ corpora fail the run. The golden bytes were recorded with Ox, so these specs
2650
+ run once with Ox. Existing native specs still run with Ox and Oga. The native
2651
+ filesystem/YAML adapter is excluded from Opal test discovery.
2652
+
2653
+ Portable parser and renderer expectations also live in the pinned repository's
2654
+ `fixtures/` directory. Native specs select shared case IDs instead of embedding
2655
+ expected trees, constructor graphs or output strings. The authored datasets
2656
+ retain each assertion's source revision and location. They are extracted from
2657
+ the original expectations, independently of production parser or renderer output.
2658
+
2659
+ The adapters preserve the original entry points, argument types, configuration,
2660
+ operation order and comparisons. Typed constructor recipes distinguish symbols,
2661
+ string keys, positional arguments and keyword arguments. Memo references preserve
2662
+ sharing within one example, while each example receives fresh mutable values.
2663
+ Raw grammar fixtures preserve their preprocessing and tree projection steps.
2664
+ External UnicodeMath fixtures retain the original inputs, expected raw trees,
2665
+ models and refusal patterns.
2666
+
2667
+ These examples continue to run through the native Ox/Oga hooks. Node/Opal uses
2668
+ the same YAML files from the checkout. Ruby API, configuration, identity and
2669
+ extension-hook tests remain native. Missing cases, unsupported schemas and
2670
+ unknown recipe operations fail rather than silently skipping coverage.
2671
+
2378
2672
  == Copyright and license
2379
2673
 
2380
2674
  Copyright Ribose. BSD 2-clause license.
data/docs/catalog.adoc ADDED
@@ -0,0 +1,73 @@
1
+ = Ruby reference catalog
2
+
3
+ `Plurimath::Catalog.entries` returns plain Ruby hashes suitable for YAML or JSON
4
+ export. The catalog describes the Ruby implementation. TypeScript conformance
5
+ is separate work and does not gate the Ruby catalog.
6
+
7
+ == Entry identity and compatibility
8
+
9
+ Each entry has a safe lowercase `name` and a `kind`: `function` or `symbol`.
10
+ Use the pair `[kind, name]` as its identity. A function and symbol may share a
11
+ name, such as `bar`; write them to separate directories.
12
+ The catalog rejects duplicate identities instead of allowing exports to overwrite
13
+ one another.
14
+
15
+ `arity` is the reference-page category (`unary`, `binary`, or `ternary`) for a
16
+ function, and `nil` for a symbol. It describes the reference classification,
17
+ not the number of arguments accepted by a Ruby constructor.
18
+
19
+ The legacy `type` field remains unchanged: it holds the function's reference
20
+ category, or `symbol`. Function entries retain `description` and `reference`;
21
+ symbol entries omit them. Existing `asciimath`, `latexmath`, `mathml`, and `omml`
22
+ fields retain the serializers' exact output, including empty strings and
23
+ placeholders. The additional metadata does not repair or rewrite serializer output.
24
+
25
+ == Format availability
26
+
27
+ `formats` contains an entry for each of the four serialization formats:
28
+
29
+ [source,ruby]
30
+ ----
31
+ entry = Plurimath::Math::Function::Sum.catalog_entry
32
+ entry["kind"] # => "function"
33
+ entry["arity"] # => "ternary"
34
+ entry["formats"]["mathml"]["status"] # => "available"
35
+ entry["formats"]["mathml"]["notes"] # => []
36
+ ----
37
+
38
+ * `available`: the serializer returns nonempty output and no limitation is
39
+ declared. This is not proof of semantic parity or round-trip correctness.
40
+ * `limited`: a known loss or limitation is described in `notes`.
41
+ * `unavailable`: output is empty or the declaration identifies an unsupported
42
+ representation, including nonempty placeholders. `notes` explains why.
43
+
44
+ Consumers should read this metadata before presenting an example as supported.
45
+ Unexpected serializer exceptions still propagate; the catalog does not convert
46
+ programming failures into a successful availability record.
47
+
48
+ A class can declare its own limitations without affecting sibling classes:
49
+
50
+ [source,ruby]
51
+ ----
52
+ CATALOG_FORMAT_LIMITATIONS = {
53
+ mathml: { status: :limited, notes: ["The current serializer omits a modifier."] },
54
+ }.freeze
55
+ ----
56
+
57
+ Declarations use canonical keys `asciimath`, `latexmath`, `mathml`, and `omml`.
58
+ Keys and statuses accept strings or symbols. A declaration must use `limited`
59
+ or `unavailable` and include a nonempty array of explanatory strings.
60
+ Empty serializer output always becomes `unavailable`, with a reason.
61
+
62
+ == Validation and extension classes
63
+
64
+ The shared entry builder validates required fields, safe slugs, type/kind/arity
65
+ agreement, format records, and limitation declarations. Invalid data raises
66
+ `Plurimath::Catalog::InvalidEntryError`, a subclass of `ArgumentError`.
67
+ `Plurimath::Catalog::Validation.validate_all!(entries)` also checks identity
68
+ uniqueness when validating an exported array.
69
+
70
+ Function classes need their own description, reference, and deferred example
71
+ to be documentable. Anonymous classes are excluded because they have no stable
72
+ Ruby name. Examples and format notes are built afresh; modifying returned notes
73
+ does not modify the class's declarations.
@@ -0,0 +1,92 @@
1
+ # AsciiMath uncertainty: parse tree contract (plurimath#493, B4)
2
+
3
+ The amath grammar turns `ustd(...)`, `uexp(...)` and `ucov(...)` into the tree
4
+ below. `Plurimath::Asciimath::Transform` (rule `uncertainty:`) turns it into
5
+ `Math::Function::Uncertainty`. Design: `docs/uncertainty.adoc`; cases:
6
+ `spec/plurimath/fixtures/uncertainty/contract.yml`; spec that checks this
7
+ tree against amath, the transform and `Math.parse`, and round-trips every case
8
+ through `to_asciimath`: `spec/plurimath/asciimath/uncertainty_spec.rb`.
9
+
10
+ ## Shape
11
+
12
+ The node sits where any other operand sits, for example as the whole formula:
13
+
14
+ ```ruby
15
+ { expr: { sequence: { uncertainty: { uncertainty_class: "uexp", value: "10.0",
16
+ magnitude: "0.3", coverage_factor: "2" } } } }
17
+ ```
18
+
19
+ Every leaf under `uncertainty:` is a `Parslet::Slice` holding the text exactly
20
+ as written (no `{number: ...}` wrapper, no sign or whitespace). Keys absent from
21
+ the input are left out, never given as nil.
22
+
23
+ | Input | Keys under `uncertainty:` |
24
+ |---|---|
25
+ | `ustd(y, u)` | `uncertainty_class: "ustd"`, `value`, `magnitude` |
26
+ | `ustd(y(d))` | `uncertainty_class: "ustd"`, `value`, `concise_digits` |
27
+ | `uexp(y, U, k[, p])` | `uncertainty_class: "uexp"`, `value`, `magnitude`, `coverage_factor`[, `coverage_probability`] |
28
+ | `ucov(y, lo, hi, p[, kind])` | `uncertainty_class: "ucov"`, `value`, `lower`, `upper`, `coverage_probability`[, `interval_kind`] |
29
+ | `..., "unitsml(g)")` (last argument, any form) | adds `unit: "g"` (the text inside `unitsml(...)`) |
30
+
31
+ - `value` may carry a leading `-`; every other number is unsigned. The decimal
32
+ marker is always a full stop, whatever the locale.
33
+ - `concise_digits` is the digit run inside the inner brackets: `ustd(1.230(4))`
34
+ gives `value: "1.230"`, `concise_digits: "4"`. The transform turns it into
35
+ `magnitude: 0.004`, `notation: :concise`.
36
+ - `unit` must not use the existing `{unitsml: ...}` key: that rule turns the unit
37
+ into a rendered `Formula`, but the node keeps a `Math::Function::Unitsml`.
38
+
39
+ ## Shared decimal exponents
40
+
41
+ The following call forms add an optional `shared_exponent` slice containing
42
+ signed exponent digits without `e`. No space is allowed before `e`:
43
+
44
+ ```asciimath
45
+ ustd((3.85, 0.23)e4)
46
+ ustd(680(41)e-3)
47
+ uexp((3.85, 0.23)e+4, 2, 0.95)
48
+ ucov((1.024, 0.983, 1.088)e-3, 0.95, shortest)
49
+ ```
50
+
51
+ The transform shifts the decimal point using strings, never floating-point
52
+ arithmetic. It scales value and magnitude, or value and both interval
53
+ endpoints; coverage factors and probabilities remain unchanged. Concise
54
+ magnitude is derived before applying the scale. The first two examples yield
55
+ `38500` with magnitude `2300`, and `0.680` with magnitude `0.041`.
56
+ Trailing decimal zeros remain significant stored text. Writers emit the
57
+ normalized existing uncertainty form, which can be read back into the same
58
+ model.
59
+
60
+ Existing calls omit this key and retain their behavior. Bare inline uncertainty
61
+ is part of the separate expression-language work in #513. To bound allocation,
62
+ the reader rejects a shared exponent that would expand a number beyond
63
+ 1,000,000 decimal characters with `Errors::InvalidUncertainty`; no partial
64
+ number or rounded approximation is returned.
65
+
66
+ The shipped amath corpus `corpora/uncertainty-shared-exponent.yml` contains the
67
+ input trees and exact normalized model expectations checked by the reader specs.
68
+
69
+ ## Interval kind spelling
70
+
71
+ `interval_kind` is a bare word: `shortest` or `symmetric`.
72
+ `probabilistically-symmetric` cannot be used, because AsciiMath reads the `-` as
73
+ minus. `symmetric` means the probabilistically symmetric coverage interval of
74
+ JCGM 101 §5.3.4 and maps to `:probabilistically_symmetric`. The output side
75
+ (`Uncertainty#to_asciimath`, which today writes `probabilistically-symmetric`)
76
+ must write `symmetric` so that output reads back.
77
+
78
+ ## Errors
79
+
80
+ - Wrong argument count (for example `uexp(10.0, 0.3)`, no `k`): the call is
81
+ not an uncertainty, and amath falls back to the letter reading it had before
82
+ the uncertainty words (plurimath/amath#9). `uexp(10.0, 0.3)` reads as the
83
+ symbol `u` followed by `exp(10.0, 0.3)`, as the contract cell
84
+ `invalid.asciimath_missing_k` records; no error is raised. Should a tree with
85
+ too few fields reach the transform anyway, the node raises
86
+ `Errors::InvalidUncertainty`, which `Math.parse` reports as
87
+ `Math::ParseError`.
88
+ - An unknown `uncertainty_class`, an unexpected key, a non-text leaf, both
89
+ `magnitude` and `concise_digits`, non-digit `concise_digits` or an unknown
90
+ `interval_kind` raise `Errors::InvalidUncertainty`.
91
+ - `+-`, `pm`, `±` and `1.230(4)` outside these three words never produce this
92
+ tree.