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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +7 -0
- data/.github/workflows/shared-testsuite.yml +24 -0
- data/.gitmodules +3 -0
- data/.rspec-opal +3 -0
- data/Gemfile +18 -0
- data/README.adoc +297 -3
- data/docs/catalog.adoc +73 -0
- data/docs/uncertainty-asciimath-tree.md +92 -0
- data/docs/uncertainty.adoc +522 -0
- data/lib/plurimath/asciimath/parse.rb +43 -226
- data/lib/plurimath/asciimath/parser.rb +58 -1
- data/lib/plurimath/asciimath/slice_adapter.rb +138 -0
- data/lib/plurimath/asciimath/transform.rb +218 -0
- data/lib/plurimath/asciimath.rb +1 -0
- data/lib/plurimath/catalog/entry.rb +109 -0
- data/lib/plurimath/catalog/validation.rb +109 -0
- data/lib/plurimath/catalog.rb +96 -0
- data/lib/plurimath/cli/convert.rb +40 -0
- data/lib/plurimath/cli/helpers.rb +49 -0
- data/lib/plurimath/cli/render.rb +64 -0
- data/lib/plurimath/cli.rb +55 -57
- data/lib/plurimath/documentation.rb +99 -0
- data/lib/plurimath/errors/ambiguous_decimal_error.rb +19 -0
- data/lib/plurimath/errors/invalid_uncertainty.rb +18 -0
- data/lib/plurimath/errors/missing_dependency_error.rb +23 -0
- data/lib/plurimath/errors/rendering_failed.rb +17 -0
- data/lib/plurimath/errors/rendering_unavailable.rb +25 -0
- data/lib/plurimath/errors/unsupported_render_format.rb +18 -0
- data/lib/plurimath/errors/unsupported_semantic_tree_error.rb +12 -0
- data/lib/plurimath/errors.rb +5 -0
- data/lib/plurimath/math/evaluation/evaluator.rb +48 -0
- data/lib/plurimath/math/evaluation/expression_parser.rb +29 -20
- data/lib/plurimath/math/formula/class_tree.rb +92 -0
- data/lib/plurimath/math/formula.rb +43 -2
- data/lib/plurimath/math/function/abs.rb +6 -0
- data/lib/plurimath/math/function/arccos.rb +6 -0
- data/lib/plurimath/math/function/arcsin.rb +6 -0
- data/lib/plurimath/math/function/arctan.rb +6 -0
- data/lib/plurimath/math/function/arg.rb +20 -0
- data/lib/plurimath/math/function/bar.rb +6 -0
- data/lib/plurimath/math/function/base.rb +6 -0
- data/lib/plurimath/math/function/binary_function.rb +4 -0
- data/lib/plurimath/math/function/cancel.rb +6 -0
- data/lib/plurimath/math/function/ceil.rb +6 -0
- data/lib/plurimath/math/function/color.rb +12 -0
- data/lib/plurimath/math/function/cos.rb +6 -0
- data/lib/plurimath/math/function/cosh.rb +6 -0
- data/lib/plurimath/math/function/cot.rb +6 -0
- data/lib/plurimath/math/function/coth.rb +6 -0
- data/lib/plurimath/math/function/csc.rb +6 -0
- data/lib/plurimath/math/function/csch.rb +6 -0
- data/lib/plurimath/math/function/ddot.rb +6 -0
- data/lib/plurimath/math/function/deg.rb +6 -0
- data/lib/plurimath/math/function/det.rb +6 -0
- data/lib/plurimath/math/function/dim.rb +6 -0
- data/lib/plurimath/math/function/dot.rb +6 -0
- data/lib/plurimath/math/function/exp.rb +6 -0
- data/lib/plurimath/math/function/fenced.rb +6 -0
- data/lib/plurimath/math/function/floor.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold-fraktur.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold-italic.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold-sans-serif.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold-script.rb +6 -0
- data/lib/plurimath/math/function/font_style/bold.rb +6 -0
- data/lib/plurimath/math/function/font_style/double_struck.rb +6 -0
- data/lib/plurimath/math/function/font_style/fraktur.rb +6 -0
- data/lib/plurimath/math/function/font_style/italic.rb +6 -0
- data/lib/plurimath/math/function/font_style/monospace.rb +6 -0
- data/lib/plurimath/math/function/font_style/normal.rb +6 -0
- data/lib/plurimath/math/function/font_style/sans-serif-bold-italic.rb +6 -0
- data/lib/plurimath/math/function/font_style/sans-serif-italic.rb +6 -0
- data/lib/plurimath/math/function/font_style/sans-serif.rb +6 -0
- data/lib/plurimath/math/function/font_style/script.rb +6 -0
- data/lib/plurimath/math/function/font_style.rb +4 -0
- data/lib/plurimath/math/function/frac.rb +6 -0
- data/lib/plurimath/math/function/gcd.rb +6 -0
- data/lib/plurimath/math/function/glb.rb +12 -0
- data/lib/plurimath/math/function/hat.rb +6 -0
- data/lib/plurimath/math/function/hom.rb +6 -0
- data/lib/plurimath/math/function/inf.rb +6 -0
- data/lib/plurimath/math/function/int.rb +7 -1
- data/lib/plurimath/math/function/intent.rb +35 -0
- data/lib/plurimath/math/function/ker.rb +6 -0
- data/lib/plurimath/math/function/lcm.rb +12 -0
- data/lib/plurimath/math/function/lg.rb +6 -0
- data/lib/plurimath/math/function/lim.rb +6 -0
- data/lib/plurimath/math/function/liminf.rb +6 -0
- data/lib/plurimath/math/function/limits.rb +6 -0
- data/lib/plurimath/math/function/limsup.rb +6 -0
- data/lib/plurimath/math/function/ln.rb +6 -0
- data/lib/plurimath/math/function/log.rb +6 -0
- data/lib/plurimath/math/function/longdiv.rb +6 -0
- data/lib/plurimath/math/function/lub.rb +6 -0
- data/lib/plurimath/math/function/max.rb +6 -0
- data/lib/plurimath/math/function/mbox.rb +6 -0
- data/lib/plurimath/math/function/menclose.rb +19 -0
- data/lib/plurimath/math/function/merror.rb +22 -0
- data/lib/plurimath/math/function/min.rb +6 -0
- data/lib/plurimath/math/function/mlabeledtr.rb +23 -0
- data/lib/plurimath/math/function/mod.rb +6 -0
- data/lib/plurimath/math/function/msgroup.rb +20 -0
- data/lib/plurimath/math/function/multiscript.rb +6 -0
- data/lib/plurimath/math/function/nary.rb +11 -0
- data/lib/plurimath/math/function/norm.rb +6 -0
- data/lib/plurimath/math/function/obrace.rb +6 -0
- data/lib/plurimath/math/function/oint.rb +7 -1
- data/lib/plurimath/math/function/over.rb +6 -0
- data/lib/plurimath/math/function/overleftrightarrow.rb +6 -0
- data/lib/plurimath/math/function/overset.rb +6 -0
- data/lib/plurimath/math/function/phantom.rb +6 -0
- data/lib/plurimath/math/function/power.rb +6 -0
- data/lib/plurimath/math/function/power_base.rb +6 -0
- data/lib/plurimath/math/function/prod.rb +7 -1
- data/lib/plurimath/math/function/right.rb +1 -1
- data/lib/plurimath/math/function/root.rb +6 -0
- data/lib/plurimath/math/function/rule.rb +20 -0
- data/lib/plurimath/math/function/scarries.rb +20 -0
- data/lib/plurimath/math/function/sec.rb +6 -0
- data/lib/plurimath/math/function/sech.rb +6 -0
- data/lib/plurimath/math/function/semantics.rb +20 -0
- data/lib/plurimath/math/function/sin.rb +6 -0
- data/lib/plurimath/math/function/sinh.rb +6 -0
- data/lib/plurimath/math/function/sqrt.rb +6 -0
- data/lib/plurimath/math/function/stackrel.rb +6 -0
- data/lib/plurimath/math/function/substack.rb +6 -0
- data/lib/plurimath/math/function/sum.rb +7 -1
- data/lib/plurimath/math/function/sup.rb +6 -0
- data/lib/plurimath/math/function/table/align.rb +6 -0
- data/lib/plurimath/math/function/table/array.rb +6 -0
- data/lib/plurimath/math/function/table/bmatrix.rb +6 -0
- data/lib/plurimath/math/function/table/cases.rb +6 -0
- data/lib/plurimath/math/function/table/eqarray.rb +6 -0
- data/lib/plurimath/math/function/table/matrix.rb +6 -0
- data/lib/plurimath/math/function/table/multline.rb +6 -0
- data/lib/plurimath/math/function/table/pmatrix.rb +6 -0
- data/lib/plurimath/math/function/table/split.rb +6 -0
- data/lib/plurimath/math/function/table/vmatrix.rb +6 -0
- data/lib/plurimath/math/function/table.rb +10 -0
- data/lib/plurimath/math/function/tan.rb +6 -0
- data/lib/plurimath/math/function/tanh.rb +6 -0
- data/lib/plurimath/math/function/ternary_function.rb +4 -0
- data/lib/plurimath/math/function/text.rb +6 -0
- data/lib/plurimath/math/function/tilde.rb +6 -0
- data/lib/plurimath/math/function/ubrace.rb +6 -0
- data/lib/plurimath/math/function/ul.rb +6 -0
- data/lib/plurimath/math/function/unary_function.rb +4 -0
- data/lib/plurimath/math/function/uncertainty.rb +655 -0
- data/lib/plurimath/math/function/underover.rb +6 -0
- data/lib/plurimath/math/function/underset.rb +6 -0
- data/lib/plurimath/math/function/vec.rb +6 -0
- data/lib/plurimath/math/function.rb +1 -0
- data/lib/plurimath/math/renderer.rb +95 -0
- data/lib/plurimath/math/semantic_tree.rb +331 -0
- data/lib/plurimath/math/symbols/symbol.rb +2 -0
- data/lib/plurimath/math.rb +26 -0
- data/lib/plurimath/mathml/translator.rb +32 -0
- data/lib/plurimath/mathml/uncertainty_marker.rb +118 -0
- data/lib/plurimath/omml/translator.rb +68 -0
- data/lib/plurimath/omml/utility.rb +9 -1
- data/lib/plurimath/symbol_documentation.rb +57 -0
- data/lib/plurimath/unicode_math/parsing_rules/common_rules.rb +2 -1
- data/lib/plurimath/unicode_math/parsing_rules/masked.rb +3 -0
- data/lib/plurimath/unicode_math/transform.rb +64 -3
- data/lib/plurimath/unicode_math/utility.rb +38 -0
- data/lib/plurimath/version.rb +1 -1
- data/lib/plurimath.rb +3 -0
- data/plurimath.gemspec +3 -1
- data/scripts/replay_asciimath_corpus.rb +611 -0
- 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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
data/lib/plurimath/errors.rb
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
|
46
|
+
result = evaluator.add(result, parse_multiplicative)
|
|
37
47
|
elsif take?(:minus_operator?)
|
|
38
|
-
result
|
|
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
|
|
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
|
|
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
|
|
83
|
+
return evaluator.negated_mod(next_token) if current.is_a?(Function::Mod)
|
|
74
84
|
|
|
75
|
-
|
|
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
|
|
107
|
+
return evaluator.negated_mod(next_token) if current.is_a?(Function::Mod)
|
|
98
108
|
|
|
99
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|