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
|
@@ -105,6 +105,7 @@ module Plurimath
|
|
|
105
105
|
autoload :Underbrace, "#{__dir__}/function/ubrace"
|
|
106
106
|
autoload :Underline, "#{__dir__}/function/ul"
|
|
107
107
|
autoload :UnaryFunction, "#{__dir__}/function/unary_function"
|
|
108
|
+
autoload :Uncertainty, "#{__dir__}/function/uncertainty"
|
|
108
109
|
autoload :Underover, "#{__dir__}/function/underover"
|
|
109
110
|
autoload :Underset, "#{__dir__}/function/underset"
|
|
110
111
|
autoload :Unitsml, "#{__dir__}/function/unitsml"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Math
|
|
5
|
+
# Internal bridge to the optional `lasem` gem, which renders MathML to
|
|
6
|
+
# SVG/PNG/PDF/PostScript through a native extension.
|
|
7
|
+
#
|
|
8
|
+
# This is NOT part of the public API: reach rendering via Plurimath::Math.render
|
|
9
|
+
# or Plurimath::Math::Formula#render. Renderer is the single place `lasem`
|
|
10
|
+
# (native, Opal-incompatible) is required, so the rest of the library stays
|
|
11
|
+
# decoupled from it. Opal excludes the guarded native require when compiling
|
|
12
|
+
# this autoloaded bridge, and available? is always false under Opal.
|
|
13
|
+
#
|
|
14
|
+
# Not thread-safe: the underlying Lasem native render keeps static parser
|
|
15
|
+
# state and the +@load_error+ diagnostic is shared module state, so callers
|
|
16
|
+
# should not render concurrently.
|
|
17
|
+
module Renderer
|
|
18
|
+
# Output image formats accepted by lasem.
|
|
19
|
+
OUTPUT_FORMATS = %i[svg png pdf ps].freeze
|
|
20
|
+
|
|
21
|
+
# Keyword options forwarded to Formula#to_mathml (the serialization step).
|
|
22
|
+
MATHML_OPTIONS = %i[
|
|
23
|
+
intent formatter unitsml split_on_linebreak display_style
|
|
24
|
+
unary_function_spacing
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
# Geometry options forwarded straight to Lasem.render. The names match
|
|
28
|
+
# lasem's own kwargs; nil values are dropped so lasem applies its defaults.
|
|
29
|
+
LASEM_OPTIONS = %i[ppi zoom width height offset_x offset_y].freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# Render a MathML document to a binary image string. Raises a
|
|
34
|
+
# Plurimath::Errors rendering error (UnsupportedRenderFormat,
|
|
35
|
+
# RenderingUnavailable, or RenderingFailed) on any problem.
|
|
36
|
+
def render(mathml, format:, **opts)
|
|
37
|
+
format = normalize_format(format)
|
|
38
|
+
ensure_available!
|
|
39
|
+
render_with_backend(mathml, format, opts)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Only the backend call is wrapped here, so the UnsupportedRenderFormat /
|
|
43
|
+
# RenderingUnavailable errors raised above are not re-wrapped.
|
|
44
|
+
def render_with_backend(mathml, format, opts)
|
|
45
|
+
Lasem.render(
|
|
46
|
+
mathml,
|
|
47
|
+
input: :mathml,
|
|
48
|
+
output: format,
|
|
49
|
+
**opts.slice(*LASEM_OPTIONS).compact,
|
|
50
|
+
)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
# Catches Lasem::RenderError/DependencyError AND Lasem::OptionError, which
|
|
53
|
+
# is an ArgumentError living OUTSIDE Lasem::Error (lasem-ruby errmodel-1),
|
|
54
|
+
# so we deliberately rescue StandardError rather than Lasem::Error.
|
|
55
|
+
raise Errors::RenderingFailed.new(format, e)
|
|
56
|
+
end
|
|
57
|
+
private_class_method :render_with_backend
|
|
58
|
+
|
|
59
|
+
# True only when this is a non-Opal runtime, the `lasem` gem loads, and its
|
|
60
|
+
# native extension reports itself available. Never raises.
|
|
61
|
+
def available?
|
|
62
|
+
return false if RUBY_ENGINE == "opal"
|
|
63
|
+
|
|
64
|
+
@load_error = nil
|
|
65
|
+
load_lasem && Lasem.native_available?
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
@load_error = e
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def normalize_format(format)
|
|
72
|
+
symbol = format.to_s.downcase.to_sym
|
|
73
|
+
return symbol if OUTPUT_FORMATS.include?(symbol)
|
|
74
|
+
|
|
75
|
+
raise Errors::UnsupportedRenderFormat.new(format, OUTPUT_FORMATS)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ensure_available!
|
|
79
|
+
return if available?
|
|
80
|
+
|
|
81
|
+
raise Errors::RenderingUnavailable.new(@load_error)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Lazily require the optional gem. Only ever reached on the non-Opal path.
|
|
85
|
+
# Returns true on success, false on LoadError (recorded for diagnostics).
|
|
86
|
+
def load_lasem
|
|
87
|
+
require "lasem" unless RUBY_ENGINE == "opal"
|
|
88
|
+
true
|
|
89
|
+
rescue LoadError => e
|
|
90
|
+
@load_error = e
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
module Math
|
|
5
|
+
# Builds a transient display view using the evaluator's precedence parser.
|
|
6
|
+
# It never evaluates operands or replaces the Formula's stored model.
|
|
7
|
+
class SemanticTree
|
|
8
|
+
View = Struct.new(:label, :children, :source)
|
|
9
|
+
private_constant :View
|
|
10
|
+
|
|
11
|
+
LANGUAGES = %i[omml latex mathml asciimath unicodemath].freeze
|
|
12
|
+
private_constant :LANGUAGES
|
|
13
|
+
|
|
14
|
+
RELATIONS = %w[Equal Less Greater Lt Gt Le Ge Leq Geq Ne Neq].freeze
|
|
15
|
+
APPLICATIONS = %w[
|
|
16
|
+
Sin Cos Tan Cot Sec Csc Sinh Cosh Tanh Coth Sech Csch
|
|
17
|
+
Arcsin Arccos Arctan Arccot Arcsec Arccsc Arcsinh Arccosh Arctanh
|
|
18
|
+
Ln Exp Abs Sgn Det Min Max Gcd Lcm
|
|
19
|
+
].freeze
|
|
20
|
+
private_constant :RELATIONS, :APPLICATIONS
|
|
21
|
+
|
|
22
|
+
def initialize(language, options)
|
|
23
|
+
unsupported("label language #{language.inspect}") unless LANGUAGES.include?(language)
|
|
24
|
+
@language = language
|
|
25
|
+
@options = options
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render(formula)
|
|
29
|
+
@label_context = formula.cloned_objects
|
|
30
|
+
root = evaluate_node(formula)
|
|
31
|
+
(["|_ Semantic tree", " |_ expression #{label(root.source)}"] + lines(root, 2)).join("\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Normalize only the symbolic backend's input. Some source parsers store
|
|
35
|
+
# Unicode minus and function application as generic Symbol instances.
|
|
36
|
+
def normalize_tokens(tokens)
|
|
37
|
+
normalized = []
|
|
38
|
+
index = 0
|
|
39
|
+
while index < tokens.length
|
|
40
|
+
node = tokens[index]
|
|
41
|
+
if node.instance_of?(Symbols::Symbol) && ["−", "−"].include?(node.value)
|
|
42
|
+
node = Symbols::Minus.new
|
|
43
|
+
elsif unbound_application?(node)
|
|
44
|
+
offset = application_marker?(tokens[index + 1]) ? 2 : 1
|
|
45
|
+
argument = tokens[index + offset]
|
|
46
|
+
valid_argument!(argument)
|
|
47
|
+
node = bind_application(node, argument)
|
|
48
|
+
index += offset
|
|
49
|
+
end
|
|
50
|
+
normalized << node
|
|
51
|
+
index += 1
|
|
52
|
+
end
|
|
53
|
+
normalized
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def relation_operator?(node)
|
|
57
|
+
return false unless node.is_a?(Symbols::Symbol)
|
|
58
|
+
|
|
59
|
+
RELATIONS.include?(node.class.name.split("::").last) ||
|
|
60
|
+
(node.instance_of?(Symbols::Symbol) && %w[= < > <= >= != ≤ ≥ ≠].include?(node.value))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def relation(operator, left, right)
|
|
64
|
+
view("relation #{label(operator)}", left, right,
|
|
65
|
+
source: sequence(left, operator, right))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def add(left, right)
|
|
69
|
+
view("add #{label(Symbols::Plus.new)}", left, right,
|
|
70
|
+
source: sequence(left, Symbols::Plus.new, right))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def subtract(left, right)
|
|
74
|
+
view("subtract #{label(Symbols::Minus.new)}", left, right,
|
|
75
|
+
source: sequence(left, Symbols::Minus.new, right))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def multiply(left, right, implicit:)
|
|
79
|
+
view(implicit ? "implicit multiply" : "multiply", left, right,
|
|
80
|
+
source: sequence(left, implicit ? nil : Symbols::Times.new, right))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def divide(left, right)
|
|
84
|
+
view("divide", left, right,
|
|
85
|
+
source: sequence(left, Symbols::Symbol.new("/"), right))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def power(left, right)
|
|
89
|
+
composite("power", Function::Power.new(left.source, right.source),
|
|
90
|
+
role("base", left), role("script", right))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def positive(value)
|
|
94
|
+
view("positive", value, source: Formula.new([Symbols::Plus.new, value.source]))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def negate(value)
|
|
98
|
+
view("negative", value, source: Formula.new([Symbols::Minus.new, value.source]))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def negated_mod(node)
|
|
102
|
+
left = negate(evaluate_node(node.parameter_one))
|
|
103
|
+
right = evaluate_node(node.parameter_two)
|
|
104
|
+
composite("mod", Function::Mod.new(left.source, right.source), left, right)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def group(value)
|
|
108
|
+
source = Function::Fenced.new(Symbols::Paren::Lround.new, [value.source], Symbols::Paren::Rround.new)
|
|
109
|
+
composite("group", source, value)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def nary_function?(node)
|
|
113
|
+
node.is_a?(Function::Int) || node.is_a?(Function::Sum) || node.is_a?(Function::Prod)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def log_argument(node, argument)
|
|
117
|
+
argument = evaluate_node(argument)
|
|
118
|
+
children = []
|
|
119
|
+
children << view("function name #{label(Function::Log.new)}")
|
|
120
|
+
children << role("base", evaluate_node(node.parameter_one)) if node.parameter_one
|
|
121
|
+
children << role("argument", argument)
|
|
122
|
+
function = node.dup
|
|
123
|
+
function.parameter_two = nil
|
|
124
|
+
result = composite("apply", Formula.new([function, argument.source]), *children)
|
|
125
|
+
node.parameter_two ? power(result, evaluate_node(node.parameter_two)) : result
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def evaluate_node(node)
|
|
129
|
+
case node
|
|
130
|
+
when Formula
|
|
131
|
+
Evaluation::ExpressionParser.new(self, node.value).parse
|
|
132
|
+
when Array
|
|
133
|
+
Evaluation::ExpressionParser.new(self, node).parse
|
|
134
|
+
when Number
|
|
135
|
+
view("number #{label(node)}", source: node)
|
|
136
|
+
when Symbols::Symbol
|
|
137
|
+
symbol(node)
|
|
138
|
+
when Function::Fenced
|
|
139
|
+
fenced(node)
|
|
140
|
+
when Function::Frac
|
|
141
|
+
numerator = evaluate_node(node.parameter_one)
|
|
142
|
+
denominator = evaluate_node(node.parameter_two)
|
|
143
|
+
composite("fraction", with_parameters(node, numerator, denominator),
|
|
144
|
+
role("numerator", numerator), role("denominator", denominator))
|
|
145
|
+
when Function::Sqrt
|
|
146
|
+
radicand = evaluate_node(node.parameter_one)
|
|
147
|
+
composite("square root", with_parameters(node, radicand),
|
|
148
|
+
view('degree "⬚"'), role("radicand", radicand))
|
|
149
|
+
when Function::Root
|
|
150
|
+
index = evaluate_node(node.parameter_one)
|
|
151
|
+
radicand = evaluate_node(node.parameter_two)
|
|
152
|
+
composite("root", with_parameters(node, index, radicand),
|
|
153
|
+
role("index", index), role("radicand", radicand))
|
|
154
|
+
when Function::Power
|
|
155
|
+
power(evaluate_node(node.parameter_one), evaluate_node(node.parameter_two))
|
|
156
|
+
when Function::Base
|
|
157
|
+
base = evaluate_node(node.parameter_one)
|
|
158
|
+
script = evaluate_node(node.parameter_two)
|
|
159
|
+
composite("subscript", with_parameters(node, base, script),
|
|
160
|
+
role("base", base), role("script", script))
|
|
161
|
+
when Function::PowerBase
|
|
162
|
+
base = evaluate_node(node.parameter_one)
|
|
163
|
+
subscript = evaluate_node(node.parameter_two)
|
|
164
|
+
superscript = evaluate_node(node.parameter_three)
|
|
165
|
+
composite("subsup", with_parameters(node, base, subscript, superscript),
|
|
166
|
+
role("base", base), role("subscript", subscript), role("superscript", superscript))
|
|
167
|
+
when Function::Int, Function::Sum, Function::Prod
|
|
168
|
+
nary(node)
|
|
169
|
+
when Function::Mod
|
|
170
|
+
left = evaluate_node(node.parameter_one)
|
|
171
|
+
right = evaluate_node(node.parameter_two)
|
|
172
|
+
composite("mod", with_parameters(node, left, right), left, right)
|
|
173
|
+
when Function::FontStyle
|
|
174
|
+
styled_symbol(node)
|
|
175
|
+
when Function::UnaryFunction
|
|
176
|
+
unsupported(node) unless application_function?(node)
|
|
177
|
+
argument = evaluate_node(node.parameter_one)
|
|
178
|
+
composite("apply", with_parameters(node, argument),
|
|
179
|
+
view("function name #{label(node.class.new, function_name: true)}"),
|
|
180
|
+
role("argument", argument))
|
|
181
|
+
else
|
|
182
|
+
unsupported(node || "missing operand")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def unsupported(construct)
|
|
187
|
+
name = construct.is_a?(String) ? construct : construct.class.name.sub(/^Plurimath::Math::/, "")
|
|
188
|
+
raise Errors::UnsupportedSemanticTreeError, name
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def view(label, *children, source: nil)
|
|
194
|
+
View.new(label, children, source)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def composite(name, source, *children)
|
|
198
|
+
view("#{name} #{label(source)}", *children, source: source)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def role(name, child)
|
|
202
|
+
composite(name, child.source, child)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def sequence(left, operator, right)
|
|
206
|
+
Formula.new([left.source, operator, right.source].compact)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def with_parameters(node, *children)
|
|
210
|
+
copy = node.dup
|
|
211
|
+
children.each_with_index do |child, index|
|
|
212
|
+
copy.public_send("#{Core::ALL_PARAMETERS.fetch(index)}=", child.source)
|
|
213
|
+
end
|
|
214
|
+
copy
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def lines(node, depth)
|
|
218
|
+
["#{' ' * depth}|_ #{node.label}"] +
|
|
219
|
+
node.children.flat_map { |child| lines(child, depth + 1) }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Use the existing language writers for labels. A copy isolates writer
|
|
223
|
+
# caches from the stored leaf; composite labels are never reparsed.
|
|
224
|
+
def label(node, function_name: false)
|
|
225
|
+
copy = node.cloned_objects
|
|
226
|
+
options = @options.dup
|
|
227
|
+
options[:formula] = @label_context
|
|
228
|
+
text = case @language
|
|
229
|
+
when :mathml
|
|
230
|
+
copy.dump_mathml(copy, false, options: options)
|
|
231
|
+
when :omml
|
|
232
|
+
copy.dump_omml(copy, true, options: options)
|
|
233
|
+
else
|
|
234
|
+
copy.public_send("to_#{@language}", options: options)
|
|
235
|
+
end
|
|
236
|
+
text = text.to_s
|
|
237
|
+
text = text.delete("") if function_name
|
|
238
|
+
text.inspect
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def symbol(node)
|
|
242
|
+
if node.operator? || node.open? || node.close? || relation_operator?(node) || application_marker?(node)
|
|
243
|
+
unsupported(node)
|
|
244
|
+
end
|
|
245
|
+
# Punctuation, separators and unclassified operators cannot safely be
|
|
246
|
+
# treated as operands. Named mathematical letters remain symbol leaves.
|
|
247
|
+
glyph = Utility.html_entity_to_unicode(node.to_unicodemath(options: @options.dup).to_s)
|
|
248
|
+
unsupported(node) unless glyph.match?(/\A[\p{L}\p{N}]+\z/) || %w[∞ ∅].include?(glyph)
|
|
249
|
+
|
|
250
|
+
view("symbol #{label(node)}", source: node)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def fenced(node)
|
|
254
|
+
left = node.parameter_one
|
|
255
|
+
right = node.parameter_three
|
|
256
|
+
unless left.nil? == right.nil? &&
|
|
257
|
+
(left.nil? || left.is_a?(Symbols::Paren::Lround)) &&
|
|
258
|
+
(right.nil? || right.is_a?(Symbols::Paren::Rround))
|
|
259
|
+
unsupported("Function::Fenced (non-round or unmatched delimiters)")
|
|
260
|
+
end
|
|
261
|
+
group(evaluate_node(node.parameter_two))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def styled_symbol(node)
|
|
265
|
+
child = node.parameter_one
|
|
266
|
+
if child.is_a?(Function::Text) && child.parameter_one.to_s.match?(/\A\p{L}\z/)
|
|
267
|
+
copy = node.dup
|
|
268
|
+
copy.parameter_one = child.dup
|
|
269
|
+
view("symbol #{label(copy)}", source: copy)
|
|
270
|
+
elsif child.is_a?(Symbols::Symbol) || child.is_a?(Number)
|
|
271
|
+
symbol(child) if child.is_a?(Symbols::Symbol)
|
|
272
|
+
copy = node.dup
|
|
273
|
+
copy.parameter_one = child.dup
|
|
274
|
+
view("symbol #{label(copy)}", source: copy)
|
|
275
|
+
else
|
|
276
|
+
unsupported("#{node.class.name.sub(/^Plurimath::Math::/, '')} (non-atomic font style)")
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def nary(node)
|
|
281
|
+
children = []
|
|
282
|
+
lower = evaluate_node(node.parameter_one) if node.parameter_one
|
|
283
|
+
upper = evaluate_node(node.parameter_two) if node.parameter_two
|
|
284
|
+
body = evaluate_node(node.parameter_three)
|
|
285
|
+
children << role("lower limit", lower) if lower
|
|
286
|
+
children << role("upper limit", upper) if upper
|
|
287
|
+
children << role(node.is_a?(Function::Int) ? "integrand" : "body", body)
|
|
288
|
+
source = node.dup
|
|
289
|
+
source.parameter_one = lower&.source
|
|
290
|
+
source.parameter_two = upper&.source
|
|
291
|
+
source.parameter_three = body.source
|
|
292
|
+
composite(node.class::FUNCTION.fetch(:name), source, *children)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def application_function?(node)
|
|
296
|
+
node.is_a?(Function::UnaryFunction) && APPLICATIONS.include?(node.class.name.split("::").last)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# A function power such as sin^2 carries the exponent on an unbound
|
|
300
|
+
# function node. Bind its following operand inside the power, so the
|
|
301
|
+
# result is (sin x)^2, while retaining the shared parser's precedence.
|
|
302
|
+
def unbound_application?(node)
|
|
303
|
+
return unbound_application?(node.parameter_one) if node.is_a?(Function::Power)
|
|
304
|
+
|
|
305
|
+
application_function?(node) &&
|
|
306
|
+
(node.parameter_one.nil? || application_marker?(node.parameter_one))
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def bind_application(node, argument)
|
|
310
|
+
copy = node.dup
|
|
311
|
+
copy.parameter_one = if node.is_a?(Function::Power)
|
|
312
|
+
bind_application(node.parameter_one, argument)
|
|
313
|
+
else
|
|
314
|
+
argument
|
|
315
|
+
end
|
|
316
|
+
copy
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def application_marker?(node)
|
|
320
|
+
node.is_a?(Symbols::Funcapply) ||
|
|
321
|
+
(node.instance_of?(Symbols::Symbol) && ["", "⁡"].include?(node.value))
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def valid_argument!(node)
|
|
325
|
+
if !node.is_a?(Core) || node.operator? || node.open? || node.close? || relation_operator?(node)
|
|
326
|
+
unsupported("function application (missing or ambiguous argument)")
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
data/lib/plurimath/math.rb
CHANGED
|
@@ -4,13 +4,18 @@ module Plurimath
|
|
|
4
4
|
module Math
|
|
5
5
|
autoload :Core, "#{__dir__}/math/core"
|
|
6
6
|
autoload :Evaluation, "#{__dir__}/math/evaluation"
|
|
7
|
+
autoload :SemanticTree, "#{__dir__}/math/semantic_tree"
|
|
7
8
|
autoload :Formula, "#{__dir__}/math/formula"
|
|
8
9
|
autoload :ModelHelper, "#{__dir__}/math/model_helper"
|
|
9
10
|
autoload :Function, "#{__dir__}/math/function"
|
|
10
11
|
autoload :InvalidTypeError, "#{__dir__}/errors/invalid_type_error"
|
|
12
|
+
autoload :MissingDependencyError,
|
|
13
|
+
"#{__dir__}/errors/missing_dependency_error"
|
|
11
14
|
autoload :Number, "#{__dir__}/math/number"
|
|
12
15
|
autoload :ParseOptionError, "#{__dir__}/errors/parse_option_error"
|
|
13
16
|
autoload :ParseError, "#{__dir__}/errors/parse_error"
|
|
17
|
+
autoload :Renderer, "#{__dir__}/math/renderer"
|
|
18
|
+
autoload :AmbiguousDecimalError, "#{__dir__}/errors/ambiguous_decimal_error"
|
|
14
19
|
autoload :Symbols, "#{__dir__}/math/symbols"
|
|
15
20
|
|
|
16
21
|
VALID_TYPES = {
|
|
@@ -50,6 +55,27 @@ module Plurimath
|
|
|
50
55
|
end
|
|
51
56
|
end
|
|
52
57
|
|
|
58
|
+
# Parse +text+ of +type+ and render the resulting formula to a binary image
|
|
59
|
+
# (one of :svg, :png, :pdf, :ps) via the optional `lasem` gem. Parse options
|
|
60
|
+
# (e.g. :locale) are routed to .parse; the rest go to Formula#render. Raises a
|
|
61
|
+
# Plurimath::Errors rendering error (RenderingUnavailable, UnsupportedRenderFormat,
|
|
62
|
+
# or RenderingFailed) when lasem/its native extension is unavailable, the format
|
|
63
|
+
# is unsupported, or rendering fails, and Plurimath::Math::ParseError when +text+
|
|
64
|
+
# cannot be parsed (raised during parsing, before rendering).
|
|
65
|
+
def render(text, type, format: :svg, **options)
|
|
66
|
+
parse_options = options.slice(*SUPPORTED_PARSE_OPTIONS)
|
|
67
|
+
render_options = options.except(*SUPPORTED_PARSE_OPTIONS)
|
|
68
|
+
parse(text, type, **parse_options).render(format: format, **render_options)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# True when image rendering is available (the `lasem` gem and its native
|
|
72
|
+
# extension are present; always false under Opal). Never raises.
|
|
73
|
+
def render_available?
|
|
74
|
+
return false if RUBY_ENGINE == "opal"
|
|
75
|
+
|
|
76
|
+
Renderer.available?
|
|
77
|
+
end
|
|
78
|
+
|
|
53
79
|
def parse_with_configuration(text, type, options)
|
|
54
80
|
return parse_formula(text, type) unless options.key?(:locale)
|
|
55
81
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "mml"
|
|
4
|
+
require_relative "uncertainty_marker"
|
|
4
5
|
|
|
5
6
|
module Plurimath
|
|
6
7
|
class Mathml
|
|
@@ -91,6 +92,8 @@ module Plurimath
|
|
|
91
92
|
|
|
92
93
|
# MathML element: <mrow>
|
|
93
94
|
def mrow_to_mrow(mrow)
|
|
95
|
+
return mrow_to_uncertainty(mrow) if uncertainty_mrow?(mrow)
|
|
96
|
+
|
|
94
97
|
children = ordered_children(mrow)
|
|
95
98
|
formula_values = children.filter_map do |child|
|
|
96
99
|
mml_to_plurimath(child)
|
|
@@ -115,6 +118,35 @@ module Plurimath
|
|
|
115
118
|
Plurimath::Math::Formula.new(mrow_obj.value)
|
|
116
119
|
end
|
|
117
120
|
|
|
121
|
+
# An mrow whose intent is an uncertainty marker (plurimath#493). The
|
|
122
|
+
# marker is authoritative; the visible children are regenerated on
|
|
123
|
+
# output, so only the child marked arg="unit" is read.
|
|
124
|
+
def uncertainty_mrow?(mrow)
|
|
125
|
+
mrow.respond_to?(:intent) && UncertaintyMarker.marker?(mrow.intent)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def mrow_to_uncertainty(mrow)
|
|
129
|
+
UncertaintyMarker.build(mrow.intent) { uncertainty_unit(mrow) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def uncertainty_unit(mrow)
|
|
133
|
+
units = ordered_children(mrow).select do |child|
|
|
134
|
+
child.respond_to?(:arg) && child.arg == "unit"
|
|
135
|
+
end
|
|
136
|
+
return if units.empty?
|
|
137
|
+
|
|
138
|
+
if units.length > 1
|
|
139
|
+
raise Plurimath::Errors::InvalidUncertainty.new(
|
|
140
|
+
"intent #{mrow.intent.inspect}: more than one arg=\"unit\" child",
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
unit = mml_to_plurimath(units.first)
|
|
145
|
+
return if unit.nil?
|
|
146
|
+
|
|
147
|
+
unit.is_a?(Plurimath::Math::Formula) ? unit : Plurimath::Math::Formula.new([unit])
|
|
148
|
+
end
|
|
149
|
+
|
|
118
150
|
# MathML element: <mover> base overscript
|
|
119
151
|
# Note: Overset convention is parameter_one=overscript, parameter_two=base
|
|
120
152
|
# (reversed from MathML document order) to match rendering methods
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plurimath
|
|
4
|
+
class Mathml
|
|
5
|
+
# Reads the measurement-uncertainty marker (plurimath#493) shared by the
|
|
6
|
+
# MathML, OMML and UnicodeMath readers: the MathML `intent` of an mrow,
|
|
7
|
+
# the text of the hidden OMML phantom, and the UnicodeMath ⓘ("...")
|
|
8
|
+
# string. See docs/uncertainty.adoc.
|
|
9
|
+
#
|
|
10
|
+
# standard-uncertainty(1.230,0.004)
|
|
11
|
+
# standard-uncertainty:concise(1.230,0.004)
|
|
12
|
+
# expanded-uncertainty(10.0,0.3,2[,0.95][,$unit])
|
|
13
|
+
# coverage-interval[:shortest|:probabilistically-symmetric](y,lo,hi,p[,$unit])
|
|
14
|
+
#
|
|
15
|
+
# A string that starts with one of the three concept names is a marker.
|
|
16
|
+
# A marker that does not have exactly this shape raises
|
|
17
|
+
# Errors::InvalidUncertainty; it is never read as ordinary content.
|
|
18
|
+
module UncertaintyMarker
|
|
19
|
+
UNIT_REFERENCE = "$unit"
|
|
20
|
+
|
|
21
|
+
ARGUMENT_FIELDS = {
|
|
22
|
+
standard: [%i[magnitude]],
|
|
23
|
+
expanded: [%i[magnitude coverage_factor],
|
|
24
|
+
%i[magnitude coverage_factor coverage_probability]],
|
|
25
|
+
coverage_interval: [%i[lower upper coverage_probability]],
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def names
|
|
31
|
+
Math::Function::Uncertainty::MARKER_NAMES
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def name_pattern
|
|
35
|
+
@name_pattern ||= begin
|
|
36
|
+
alternatives = names.values.map { |name| Regexp.escape(name) }
|
|
37
|
+
/\A\s*(#{alternatives.join('|')})(?![\w-])/
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# True when the text uses one of the three concept names, whether or
|
|
42
|
+
# not the rest of the marker is well formed.
|
|
43
|
+
def marker?(text)
|
|
44
|
+
text.is_a?(String) && name_pattern.match?(text)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Builds the Uncertainty node described by the marker. `unit` is
|
|
48
|
+
# called only when the marker refers to `$unit`, and must return the
|
|
49
|
+
# unit content read from the markup.
|
|
50
|
+
def build(text, &unit)
|
|
51
|
+
fields = parse(text)
|
|
52
|
+
options = fields[:options]
|
|
53
|
+
if fields[:unit]
|
|
54
|
+
unit_value = unit&.call
|
|
55
|
+
invalid!(text, "it refers to $unit but no unit content was found") unless unit_value
|
|
56
|
+
|
|
57
|
+
options[:unit] = unit_value
|
|
58
|
+
end
|
|
59
|
+
Math::Function::Uncertainty.new(fields[:value], **options)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse(text)
|
|
63
|
+
match = marker_match(text)
|
|
64
|
+
category = names.key(match[:name])
|
|
65
|
+
arguments = match[:arguments].split(",", -1).map(&:strip)
|
|
66
|
+
unit = arguments.last == UNIT_REFERENCE
|
|
67
|
+
arguments.pop if unit
|
|
68
|
+
if arguments.any? { |argument| argument.empty? || argument.start_with?("$") }
|
|
69
|
+
invalid!(text, "arguments must be numbers, with $unit only as the last one")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
value, *rest = arguments
|
|
73
|
+
fields = ARGUMENT_FIELDS[category].find { |list| list.length == rest.length }
|
|
74
|
+
invalid!(text, "wrong number of arguments for #{match[:name]}") unless fields
|
|
75
|
+
|
|
76
|
+
options = { category: category }
|
|
77
|
+
fields.zip(rest).each { |field, number| options[field] = Math::Number.new(number) }
|
|
78
|
+
options.merge!(property_options(category, match[:property], text))
|
|
79
|
+
{ value: Math::Number.new(value), options: options, unit: unit }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Whitespace follows the MathML 4 intent grammar (section 5.1):
|
|
83
|
+
#
|
|
84
|
+
# application := expression '(' arguments? S ')'
|
|
85
|
+
# expression := S ( term property* ) S
|
|
86
|
+
# property := S ':' NCName
|
|
87
|
+
#
|
|
88
|
+
# so whitespace may come before a property's ':' and between the head
|
|
89
|
+
# (with its property) and '(', and around each argument, but not
|
|
90
|
+
# between ':' and the property name.
|
|
91
|
+
def marker_match(text)
|
|
92
|
+
name = Regexp.union(names.values)
|
|
93
|
+
pattern = /\A\s*(?<name>#{name})(?:\s*:(?<property>[^():,\s]+))?\s*\((?<arguments>[^()]*)\)\s*\z/
|
|
94
|
+
pattern.match(text) || invalid!(text, "it is not of the form name[:property](arguments)")
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def property_options(category, property, text)
|
|
98
|
+
case category
|
|
99
|
+
when :standard
|
|
100
|
+
return { notation: :absolute } if property.nil?
|
|
101
|
+
return { notation: :concise } if property == "concise"
|
|
102
|
+
when :expanded
|
|
103
|
+
return {} if property.nil?
|
|
104
|
+
when :coverage_interval
|
|
105
|
+
return {} if property.nil?
|
|
106
|
+
|
|
107
|
+
kind = Math::Function::Uncertainty::INTERVAL_KIND_NAMES.key(property)
|
|
108
|
+
return { interval_kind: kind } if kind
|
|
109
|
+
end
|
|
110
|
+
invalid!(text, "unknown property :#{property}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def invalid!(text, detail)
|
|
114
|
+
raise Errors::InvalidUncertainty.new("marker #{text.inspect}: #{detail}")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|