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
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ class Cli < Thor
5
+ # Standalone command object for `plurimath render`. Usable on its own:
6
+ # Plurimath::Cli::Render.new(options).call
7
+ class Render
8
+ include Helpers
9
+
10
+ def call
11
+ text = input_string
12
+ warn_and_exit("missing generator argument --input or --file-input") unless text
13
+
14
+ output_path = options[:output]
15
+ configure_xml_engine(options[:input_format], "mathml")
16
+ bytes = Plurimath::Math.render(
17
+ text, options[:input_format],
18
+ format: render_format(output_path), **render_options
19
+ )
20
+ write_render_output(bytes, output_path)
21
+ rescue Plurimath::Error => e
22
+ warn_and_exit(e.message)
23
+ end
24
+
25
+ private
26
+
27
+ def write_render_output(bytes, output_path)
28
+ return File.binwrite(output_path, bytes) if output_path
29
+
30
+ $stdout.binmode
31
+ $stdout.write(bytes)
32
+ end
33
+
34
+ # Resolve the output image format. The --output extension is authoritative
35
+ # for a file (so `-o x.png` always writes PNG); --format applies when there
36
+ # is no usable extension (stdout, or an extensionless --output); otherwise
37
+ # svg. Math::Renderer validates the result and raises
38
+ # Errors::UnsupportedRenderFormat on an unsupported value.
39
+ def render_format(output_path)
40
+ extension = File.extname(output_path.to_s).delete_prefix(".")
41
+ return extension.downcase.to_sym unless extension.empty?
42
+
43
+ explicit = options[:format]
44
+ return explicit.to_s.downcase.to_sym if explicit && !explicit.to_s.strip.empty?
45
+
46
+ :svg
47
+ end
48
+
49
+ # Keyword options forwarded to Plurimath::Math.render. --display-style is
50
+ # passed only when given, so the formula's own display style is honored by
51
+ # default; nil-valued numeric options are dropped.
52
+ def render_options
53
+ opts = {}
54
+ opts[:display_style] = options[:display_style] unless options[:display_style].nil?
55
+ opts[:split_on_linebreak] = options[:split_on_linebreak] unless options[:split_on_linebreak].nil?
56
+ Plurimath::Math::Renderer::LASEM_OPTIONS.each do |key|
57
+ value = options[key]
58
+ opts[key] = value unless value.nil?
59
+ end
60
+ opts
61
+ end
62
+ end
63
+ end
64
+ end
data/lib/plurimath/cli.rb CHANGED
@@ -2,8 +2,14 @@
2
2
 
3
3
  require "thor"
4
4
  require_relative "../plurimath"
5
+ require_relative "cli/helpers"
6
+ require_relative "cli/convert"
7
+ require_relative "cli/render"
5
8
 
6
9
  module Plurimath
10
+ # Thor entry point. Declares each command's options and delegates to an
11
+ # independent, standalone command object (Cli::Convert / Cli::Render) that can
12
+ # also be used directly: `Plurimath::Cli::Render.new(options).call`.
7
13
  class Cli < Thor
8
14
  desc "convert", "Convert between math formats"
9
15
 
@@ -48,65 +54,57 @@ module Plurimath
48
54
  desc: "XML engine to use for parsing and rendering (ox or oga)"
49
55
 
50
56
  def convert
51
- input = options[:input]
52
- input_string = options[:file_path] ? File.read(options[:file_path]) : input
53
- warn_and_exit("missing generator argument --input or --file-input") unless input_string
54
-
55
- input_format = options[:input_format]
56
- output_format = options[:output_format]
57
- configure_xml_engine(input_format, output_format)
58
- formula = Plurimath::Math.parse(input_string, input_format)
59
- return puts formula.to_display(output_format.to_sym) if options[:math_rendering].to_s == "true"
60
-
61
- display_style = options[:display_style]
62
- split = options[:split_on_linebreak]
63
- style = display_style.to_s.empty? ? "true" : display_style
64
- output_text = case output_format
65
- when "unicodemath"
66
- formula.to_unicodemath
67
- when "asciimath"
68
- formula.to_asciimath
69
- when "mathml"
70
- formula.to_mathml(display_style: style,
71
- split_on_linebreak: split)
72
- when "latex"
73
- formula.to_latex
74
- when "omml"
75
- formula.to_omml(display_style: style,
76
- split_on_linebreak: split)
77
- else
78
- warn_and_exit("Invalid output format: #{output_format}")
79
- end
80
-
81
- puts output_text
57
+ Convert.new(options).call
82
58
  end
83
59
 
84
- no_commands do
85
- def configure_xml_engine(input_format, output_format)
86
- xml_formats = %w[mathml omml]
87
- return unless xml_formats.include?(input_format) || xml_formats.include?(output_format)
88
-
89
- set_xml_engine(options[:xml_engine])
90
- end
91
-
92
- def set_xml_engine(engine)
93
- engine_class = case engine
94
- when "ox"
95
- require_relative "setup/ox_engine"
96
- Plurimath::XmlEngine::OxEngine
97
- when "oga"
98
- require_relative "setup/oga"
99
- Plurimath::XmlEngine::Oga
100
- else
101
- warn_and_exit("Invalid XML engine: #{engine}. Use 'ox' or 'oga'.")
102
- end
103
- Plurimath.xml_engine = engine_class
104
- end
105
-
106
- def warn_and_exit(message)
107
- warn(message)
108
- abort
109
- end
60
+ desc "render", "Render math to an image (svg/png/pdf/ps) via the optional lasem gem"
61
+
62
+ option :input,
63
+ aliases: "-i",
64
+ desc: "Input value should be in quoted string"
65
+
66
+ option :input_format,
67
+ aliases: "-f",
68
+ default: "asciimath",
69
+ desc: "Input format should be in quoted string => \"asciimath\""
70
+
71
+ option :file_path,
72
+ aliases: "-p",
73
+ desc: "Reads input from a file instead of the command line input. Use this for larger inputs or when input contains special characters.",
74
+ force: :boolean
75
+
76
+ option :output,
77
+ aliases: "-o",
78
+ desc: "Output file path; the image format is taken from its extension (.svg/.png/.pdf/.ps). When omitted, the rendered bytes are written to stdout."
79
+
80
+ option :format,
81
+ aliases: "-r",
82
+ desc: "Image format (svg, png, pdf, ps). Used for stdout or when --output has no usable extension; a file's extension takes precedence. Defaults to svg."
83
+
84
+ option :display_style,
85
+ aliases: "-d",
86
+ desc: "DisplayStyle, Boolean only. Defaults to the parsed formula's display style.",
87
+ force: :boolean
88
+
89
+ option :split_on_linebreak,
90
+ aliases: "-s",
91
+ desc: "Splits into multiple equations. Note: lasem renders a single MathML document.",
92
+ force: :boolean
93
+
94
+ option :ppi, type: :numeric, desc: "Pixels per inch for raster output (lasem)"
95
+ option :zoom, type: :numeric, desc: "Render scale factor (lasem)"
96
+ option :width, type: :numeric, desc: "Explicit output width (lasem)"
97
+ option :height, type: :numeric, desc: "Explicit output height (lasem)"
98
+ option :offset_x, type: :numeric, desc: "Horizontal offset (lasem)"
99
+ option :offset_y, type: :numeric, desc: "Vertical offset (lasem)"
100
+
101
+ option :xml_engine,
102
+ aliases: "-e",
103
+ default: "ox",
104
+ desc: "XML engine to use for parsing and rendering (ox or oga)"
105
+
106
+ def render
107
+ Render.new(options).call
110
108
  end
111
109
  end
112
110
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ # Per-class documentation metadata for the function side of the catalog
5
+ # (see Plurimath::Catalog, which powers the plurimath.org "Functions" and
6
+ # "Symbols" pages). A documentable base class extends this module —
7
+ # TernaryFunction, BinaryFunction, UnaryFunction, Table, and Nary — and every
8
+ # descendant then inherits the shared, derivable machinery (name, type, the
9
+ # four renderings) while each concrete class "explains itself" with three
10
+ # declarations: DESCRIPTION, REFERENCE, and an EXAMPLE lambda. Table and Nary
11
+ # are themselves documented (the /functions/table and /functions/n-ary pages),
12
+ # not just bases. Symbols are documented separately by
13
+ # Plurimath::SymbolDocumentation, which derives everything.
14
+ module Documentation
15
+ # Slug for the /functions/<name>/ page and YAML filename, matching the
16
+ # lowercased, separator-free convention the site already uses:
17
+ # Sum -> "sum", PowerBase -> "powerbase". A class whose page slug can't be
18
+ # derived from its name declares CATALOG_NAME to override it (Nary ->
19
+ # "n-ary").
20
+ def catalog_name
21
+ declared_constant(:CATALOG_NAME) || name.split("::").last.downcase
22
+ end
23
+
24
+ # :unary / :binary / :ternary — declared once per base and inherited by
25
+ # every descendant.
26
+ def catalog_type
27
+ declared_constant(:CATALOG_TYPE, inherit: true)
28
+ end
29
+
30
+ def catalog_kind
31
+ :function
32
+ end
33
+
34
+ # The reference-page grouping, not the Ruby constructor's parameter count.
35
+ def catalog_arity
36
+ catalog_type
37
+ end
38
+
39
+ def description
40
+ declared_constant(:DESCRIPTION)
41
+ end
42
+
43
+ def reference
44
+ declared_constant(:REFERENCE)
45
+ end
46
+
47
+ # Builds the example fresh from the class's EXAMPLE lambda. Because EXAMPLE
48
+ # is deferred, nothing is constructed — and no sibling class referenced —
49
+ # until the catalog is generated. nil when the class declares no example.
50
+ def example_formula
51
+ block = declared_constant(:EXAMPLE)
52
+ return unless block
53
+
54
+ result = instance_exec(&block)
55
+ case result
56
+ when Math::Formula then result # lambda already built one — don't re-wrap
57
+ when Array then expr(*result) # splat a multi-part example
58
+ else expr(result) # wrap a bare node
59
+ end
60
+ end
61
+
62
+ # Catalogued only when a class carries the full three-declaration contract —
63
+ # description, reference and example — so partially-documented classes never
64
+ # leak into the output.
65
+ def documented?
66
+ !name.nil? && !description.nil? && !reference.nil? &&
67
+ !declared_constant(:EXAMPLE).nil?
68
+ end
69
+
70
+ # The YAML-ready entry the docs consume: metadata plus the example rendered
71
+ # to every target format.
72
+ def catalog_entry
73
+ Catalog::Entry.build(
74
+ name: catalog_name, type: catalog_type, kind: catalog_kind,
75
+ arity: catalog_arity, description: description, reference: reference,
76
+ formula: example_formula,
77
+ limitations: declared_constant(:CATALOG_FORMAT_LIMITATIONS) || {}
78
+ )
79
+ end
80
+
81
+ private
82
+
83
+ # Reads an own constant (or an inherited one when inherit: true), else nil.
84
+ def declared_constant(name, inherit: false)
85
+ const_get(name, inherit) if const_defined?(name, inherit)
86
+ end
87
+
88
+ # Constructor helpers: sym() keeps the EXAMPLE lambda bodies readable, and
89
+ # expr() wraps each example result in a Formula (used by example_formula and
90
+ # available to lambdas that build multi-part examples).
91
+ def sym(value)
92
+ Math::Symbols::Symbol.new(value)
93
+ end
94
+
95
+ def expr(*parts)
96
+ Math::Formula.new(parts)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Math
5
+ # An AsciiMath uncertainty call whose comma could be a decimal comma under
6
+ # the active locale. A ParseError, so Plurimath::Math.parse passes it on
7
+ # with its message intact.
8
+ class AmbiguousDecimalError < ParseError
9
+ def initialize(detail)
10
+ @detail = detail
11
+ super(detail, :asciimath)
12
+ end
13
+
14
+ def parsing_message
15
+ "[plurimath] #{@detail}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ # Raised when an uncertainty (Math::Function::Uncertainty) is built from
6
+ # fields that do not form a valid reported result.
7
+ class InvalidUncertainty < Plurimath::Error
8
+ def initialize(detail)
9
+ @detail = detail
10
+ super(message)
11
+ end
12
+
13
+ def message
14
+ "[plurimath] Invalid uncertainty: #{@detail}."
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Math
5
+ # Raised when a format's parser needs a gem that cannot be loaded. It is
6
+ # a ParseError, so Math.parse passes it through with this message instead
7
+ # of the generic one, and callers rescuing ParseError still catch it.
8
+ class MissingDependencyError < ParseError
9
+ def initialize(type, format_name:, gem_name:, gemfile_line:)
10
+ @format_name = format_name
11
+ @gem_name = gem_name
12
+ @gemfile_line = gemfile_line
13
+ super(nil, type)
14
+ end
15
+
16
+ def parsing_message
17
+ "[plurimath] #{@format_name} parsing needs the #{@gem_name} gem, " \
18
+ "which could not be loaded; add #{@gemfile_line} to your Gemfile " \
19
+ "and run `bundle install`."
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ class RenderingFailed < Plurimath::Error
6
+ def initialize(format, cause)
7
+ @format = format
8
+ @failure = cause
9
+ super(message)
10
+ end
11
+
12
+ def message
13
+ "[plurimath] Failed to render MathML to #{@format}: #{@failure.message}."
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ class RenderingUnavailable < Plurimath::Error
6
+ def initialize(load_error = nil)
7
+ @load_error = load_error
8
+ super(message)
9
+ end
10
+
11
+ def message
12
+ text =
13
+ "[plurimath] Image rendering requires the optional `lasem` gem with " \
14
+ "its native extension, which is unavailable here (it is never " \
15
+ "available under Opal/plurimath-js). Add `gem \"lasem\"` to your " \
16
+ "Gemfile, then run `bundle exec lasem-doctor` to diagnose missing " \
17
+ "system libraries (cairo, pango, the Lasem C library) and rebuild " \
18
+ "the extension."
19
+ return text unless @load_error
20
+
21
+ "#{text} Underlying load error: #{@load_error.message}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ class UnsupportedRenderFormat < Plurimath::Error
6
+ def initialize(format, supported)
7
+ @format = format
8
+ @supported = supported.map(&:inspect).join(", ")
9
+ super(message)
10
+ end
11
+
12
+ def message
13
+ "[plurimath] Unsupported render format #{@format.inspect}. " \
14
+ "Supported formats are: #{@supported}."
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plurimath
4
+ module Errors
5
+ # A display-only diagnostic; numeric evaluation has its own error contract.
6
+ class UnsupportedSemanticTreeError < Plurimath::Error
7
+ def initialize(construct)
8
+ super("Unsupported semantic tree construct: #{construct}")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -3,8 +3,13 @@
3
3
  module Plurimath
4
4
  module Errors
5
5
  autoload :Evaluation, "#{__dir__}/errors/evaluation"
6
+ autoload :UnsupportedSemanticTreeError, "#{__dir__}/errors/unsupported_semantic_tree_error"
6
7
  autoload :InvalidNumber, "#{__dir__}/errors/invalid_number"
8
+ autoload :RenderingFailed, "#{__dir__}/errors/rendering_failed"
9
+ autoload :RenderingUnavailable, "#{__dir__}/errors/rendering_unavailable"
10
+ autoload :InvalidUncertainty, "#{__dir__}/errors/invalid_uncertainty"
7
11
  autoload :UnsupportedBase, "#{__dir__}/errors/unsupported_base"
8
12
  autoload :UnsupportedLocale, "#{__dir__}/errors/unsupported_locale"
13
+ autoload :UnsupportedRenderFormat, "#{__dir__}/errors/unsupported_render_format"
9
14
  end
10
15
  end
@@ -54,6 +54,54 @@ module Plurimath
54
54
  bindings[name]
55
55
  end
56
56
 
57
+ # ExpressionParser delegates its folds to a backend. These operations
58
+ # retain the numeric contract; the semantic display uses the same
59
+ # precedence ladder with a backend that only builds a render view.
60
+ def normalize_tokens(tokens)
61
+ tokens
62
+ end
63
+
64
+ def relation_operator?(_node)
65
+ false
66
+ end
67
+
68
+ def add(left, right)
69
+ left + right
70
+ end
71
+
72
+ def subtract(left, right)
73
+ left - right
74
+ end
75
+
76
+ def multiply(left, right, implicit:)
77
+ left * right
78
+ end
79
+
80
+ def positive(value)
81
+ value
82
+ end
83
+
84
+ def negate(value)
85
+ -value
86
+ end
87
+
88
+ def negated_mod(node)
89
+ node.evaluate_negated(self)
90
+ end
91
+
92
+ def group(value)
93
+ value
94
+ end
95
+
96
+ def nary_function?(node)
97
+ (node.is_a?(Function::Sum) || node.is_a?(Function::Prod)) &&
98
+ node.parameter_one && node.parameter_two
99
+ end
100
+
101
+ def log_argument(node, argument)
102
+ real_result(node.evaluate_with_argument(self, argument))
103
+ end
104
+
57
105
  def divide(dividend, divisor)
58
106
  raise Errors::Evaluation::DivisionByZeroError if divisor.zero?
59
107
 
@@ -4,22 +4,23 @@ module Plurimath
4
4
  module Math
5
5
  module Evaluation
6
6
  # Resolves operator precedence over the flat token sequences stored in
7
- # Formula#value, delegating node evaluation back to the evaluator.
7
+ # Formula#value, delegating operands and folds to a backend. The numeric
8
+ # evaluator computes values; the semantic display builds a transient view.
8
9
  #
9
10
  # Standard recursive-descent precedence ladder — each level parses its
10
11
  # operands at the next-tighter level, so the operands of `+`/`-` are
11
12
  # whole multiplicative expressions, and so on down to single operands:
12
- # parse_additive -> parse_multiplicative -> parse_unary
13
+ # parse_relation -> parse_additive -> parse_multiplicative -> parse_unary
13
14
  # -> parse_power -> parse_operand
14
15
  class ExpressionParser
15
16
  def initialize(evaluator, tokens)
16
17
  @evaluator = evaluator
17
- @tokens = tokens
18
+ @tokens = evaluator.normalize_tokens(tokens)
18
19
  @index = 0
19
20
  end
20
21
 
21
22
  def parse
22
- result = parse_additive
23
+ result = parse_relation
23
24
  evaluator.unsupported(current) unless eof?
24
25
 
25
26
  result
@@ -29,13 +30,22 @@ module Plurimath
29
30
 
30
31
  attr_reader :evaluator, :tokens
31
32
 
33
+ def parse_relation
34
+ result = parse_additive
35
+ while evaluator.relation_operator?(current)
36
+ operator = next_token
37
+ result = evaluator.relation(operator, result, parse_additive)
38
+ end
39
+ result
40
+ end
41
+
32
42
  def parse_additive
33
43
  result = parse_multiplicative
34
44
  loop do
35
45
  if take?(:plus_operator?)
36
- result += parse_multiplicative
46
+ result = evaluator.add(result, parse_multiplicative)
37
47
  elsif take?(:minus_operator?)
38
- result -= parse_multiplicative
48
+ result = evaluator.subtract(result, parse_multiplicative)
39
49
  else
40
50
  break result
41
51
  end
@@ -46,11 +56,11 @@ module Plurimath
46
56
  result = parse_unary
47
57
  loop do
48
58
  if take?(:multiply_operator?)
49
- result *= parse_unary
59
+ result = evaluator.multiply(result, parse_unary, implicit: false)
50
60
  elsif take?(:divide_operator?)
51
61
  result = evaluator.divide(result, parse_unary)
52
62
  elsif implicit_multiplication?
53
- result *= parse_power
63
+ result = evaluator.multiply(result, parse_power, implicit: true)
54
64
  else
55
65
  break result
56
66
  end
@@ -59,7 +69,7 @@ module Plurimath
59
69
 
60
70
  def parse_unary
61
71
  if take?(:plus_operator?)
62
- parse_unary
72
+ evaluator.positive(parse_unary)
63
73
  elsif take?(:minus_operator?)
64
74
  negated_unary
65
75
  else
@@ -70,9 +80,9 @@ module Plurimath
70
80
  # Unary minus binds tighter than `mod`, so `-7 mod 3` negates the
71
81
  # dividend, not the result: `(-7) mod 3 == 2`, not `-(7 mod 3)`.
72
82
  def negated_unary
73
- return next_token.evaluate_negated(evaluator) if current.is_a?(Function::Mod)
83
+ return evaluator.negated_mod(next_token) if current.is_a?(Function::Mod)
74
84
 
75
- -parse_unary
85
+ evaluator.negate(parse_unary)
76
86
  end
77
87
 
78
88
  # Chained powers evaluate left-to-right, matching the left-nested
@@ -85,7 +95,7 @@ module Plurimath
85
95
 
86
96
  def parse_exponent
87
97
  if take?(:plus_operator?)
88
- parse_exponent
98
+ evaluator.positive(parse_exponent)
89
99
  elsif take?(:minus_operator?)
90
100
  negated_exponent
91
101
  else
@@ -94,9 +104,9 @@ module Plurimath
94
104
  end
95
105
 
96
106
  def negated_exponent
97
- return next_token.evaluate_negated(evaluator) if current.is_a?(Function::Mod)
107
+ return evaluator.negated_mod(next_token) if current.is_a?(Function::Mod)
98
108
 
99
- -parse_exponent
109
+ evaluator.negate(parse_exponent)
100
110
  end
101
111
 
102
112
  def parse_operand
@@ -115,11 +125,11 @@ module Plurimath
115
125
  # "unmatched parenthesis" is reserved for true end-of-input.
116
126
  def parse_group
117
127
  advance
118
- result = parse_additive
128
+ result = parse_relation
119
129
  evaluator.unsupported(current || "unmatched parenthesis") unless close_paren?(current)
120
130
 
121
131
  advance
122
- result
132
+ evaluator.group(result)
123
133
  end
124
134
 
125
135
  # Unary functions and `log` parse separately from their argument in
@@ -137,9 +147,8 @@ module Plurimath
137
147
  # the following sibling token; AsciiMath binds a single operand as the
138
148
  # body, so we match by adopting the next operand as parameter_three.
139
149
  def nary_body?(node)
140
- (node.is_a?(Function::Sum) || node.is_a?(Function::Prod)) &&
150
+ evaluator.nary_function?(node) &&
141
151
  node.parameter_three.nil? &&
142
- node.parameter_one && node.parameter_two &&
143
152
  !eof? && operand_start?(current)
144
153
  end
145
154
 
@@ -157,7 +166,7 @@ module Plurimath
157
166
  end
158
167
 
159
168
  def bind_log_argument(node)
160
- evaluator.real_result(node.evaluate_with_argument(evaluator, next_token))
169
+ evaluator.log_argument(node, next_token)
161
170
  end
162
171
 
163
172
  # Adjacent operands multiply by juxtaposition (`2a`, `2(a+b)`), except
@@ -169,7 +178,7 @@ module Plurimath
169
178
  end
170
179
 
171
180
  def operand_start?(node)
172
- return false if node.nil? || node.operator?
181
+ return false if node.nil? || node.operator? || evaluator.relation_operator?(node)
173
182
 
174
183
  !close_paren?(node)
175
184
  end