Class: AsciiChem::Formatter::Mathml
- Defined in:
- lib/asciichem/formatter/mathml.rb
Overview
Renders a Model tree as presentation MathML.
The headline semantic fix lives here: prefix isotopes bind to the
atom, so ^14C renders as <msup><mi>C</mi><mn>14</mn></msup>
(Carbon with 14 as superscript). AsciiMath would emit
<msup><mi></mi><mn>14</mn></msup><mi>C</mi> — a phantom base
followed by a sibling atom, which loses the binding.
Constant Summary collapse
- MATHML_NS =
"http://www.w3.org/1998/Math/MathML".freeze
Instance Method Summary collapse
- #attach_isotope_prefix(base, atom) ⇒ Object
- #charge_row(charge) ⇒ Object
-
#initialize ⇒ Mathml
constructor
A new instance of Mathml.
-
#super_element(atom) ⇒ Object
Build the MathML element for whichever superscript-style marker is set (charge > oxidation_state > raw superscript).
- #visit_atom(atom) ⇒ Object
- #visit_bond(bond) ⇒ Object
- #visit_electron_configuration(ec) ⇒ Object
- #visit_embedded_math(em) ⇒ Object
-
#visit_formula(formula) ⇒ Object
Model entry point.
- #visit_group(group) ⇒ Object
- #visit_molecule(molecule) ⇒ Object
- #visit_reaction(reaction) ⇒ Object
- #visit_reaction_cascade(cascade) ⇒ Object
- #visit_text(text) ⇒ Object
- #wrap_lewis_prefix(base, atom) ⇒ Object
- #wrap_lewis_suffix(base, atom) ⇒ Object
- #wrap_ring_closures(base, atom) ⇒ Object
-
#wrap_sub_and_super(base, atom) ⇒ Object
Combine subscript + superscript-style marker into the right MathML element.
Methods inherited from Base
Constructor Details
#initialize ⇒ Mathml
Returns a new instance of Mathml.
17 18 19 |
# File 'lib/asciichem/formatter/mathml.rb', line 17 def initialize @doc = Nokogiri::XML::Document.new end |
Instance Method Details
#attach_isotope_prefix(base, atom) ⇒ Object
69 70 71 72 73 74 75 76 77 78 |
# File 'lib/asciichem/formatter/mathml.rb', line 69 def attach_isotope_prefix(base, atom) multi = el("mmultiscripts") multi.add_child(base) multi.add_child(el("none")) multi.add_child(el("none")) multi.add_child(Nokogiri::XML::Element.new("mprescripts", @doc)) multi.add_child(el("none")) multi.add_child(mn(atom.isotope)) multi end |
#charge_row(charge) ⇒ Object
137 138 139 140 141 142 143 144 145 146 |
# File 'lib/asciichem/formatter/mathml.rb', line 137 def charge_row(charge) match = charge.match(/\A(?<n>\d*)(?<sign>[+-])\z/) || charge.match(/\A(?<sign>[+-])(?<n>\d*)\z/) return nil unless match mrow = el("mrow") mrow.add_child(mn(match[:n])) unless match[:n].empty? mrow.add_child(mo(match[:sign])) mrow end |
#super_element(atom) ⇒ Object
Build the MathML element for whichever superscript-style marker is set (charge > oxidation_state > raw superscript). Returns nil if none is set.
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/asciichem/formatter/mathml.rb', line 121 def super_element(atom) if atom.charge row = charge_row(atom.charge) return row if row end if atom.oxidation_state mrow = el("mrow") mrow.add_child(mi(atom.oxidation_state)) return mrow end if atom.superscript return mn(atom.superscript) end nil end |
#visit_atom(atom) ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/asciichem/formatter/mathml.rb', line 43 def visit_atom(atom) base = mi(atom.element) # Isotope is a LEFT superscript per IUPAC (¹⁴C). Use # <mmultiscripts> with <mprescripts/> so the binding stays on # the atom — the AsciiMath `<msup><mi></mi>...<mi>C</mi>` # pattern (empty base + sibling) loses the binding. base = attach_isotope_prefix(base, atom) if atom.isotope base = wrap_lewis_prefix(base, atom) # When the atom has BOTH subscript and a superscript-style # marker (charge, oxidation state, or raw superscript), emit # a single <msubsup> rather than nesting <msub> inside <msup>. base = wrap_sub_and_super(base, atom) base = wrap_lewis_suffix(base, atom) base = wrap_ring_closures(base, atom) base end |
#visit_bond(bond) ⇒ Object
161 162 163 |
# File 'lib/asciichem/formatter/mathml.rb', line 161 def visit_bond(bond) mo(bond.entity) end |
#visit_electron_configuration(ec) ⇒ Object
186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/asciichem/formatter/mathml.rb', line 186 def visit_electron_configuration(ec) mrow = el("mrow") ec.orbitals.each_with_index do |(orbital, occupancy), index| mrow.add_child(mo(" ")) if index.positive? msup = el("msup") msup.add_child(mi(orbital)) msup.add_child(mn(occupancy)) mrow.add_child(msup) end mrow end |
#visit_embedded_math(em) ⇒ Object
198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/asciichem/formatter/mathml.rb', line 198 def (em) # Strip the outer <math> wrapper so the embedded fragment slots # into our surrounding <mrow>. fragment = em.formula.to_mathml parsed = Nokogiri::XML(fragment) math = parsed.at_xpath("//m:math", m: MATHML_NS) return el("mrow") unless math # Detach children into a fresh <mrow>. mrow = el("mrow") math.children.each { |c| mrow.add_child(c.dup) } mrow end |
#visit_formula(formula) ⇒ Object
Model entry point. Returns a <math> element as a string.
22 23 24 25 26 27 28 29 30 31 |
# File 'lib/asciichem/formatter/mathml.rb', line 22 def visit_formula(formula) math = el("math", xmlns: MATHML_NS) mrow = el("mrow") formula.nodes.each { |n| mrow.add_child(render_node(n)) } math.add_child(mrow) @doc.root = math # Native UTF-8 output preserves unicode arrow entities so specs # and downstream consumers see ⇌ and → instead of ⇌. @doc.to_xml(encoding: "UTF-8") end |
#visit_group(group) ⇒ Object
148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/asciichem/formatter/mathml.rb', line 148 def visit_group(group) mrow = el("mrow") mrow.add_child(mo(group.open_char)) group.nodes.each { |n| mrow.add_child(render_node(n)) } mrow.add_child(mo(group.close_char)) return mrow unless group.multiplicity msub = el("msub") msub.add_child(mrow) msub.add_child(mn(group.multiplicity)) msub end |
#visit_molecule(molecule) ⇒ Object
33 34 35 36 37 38 39 40 41 |
# File 'lib/asciichem/formatter/mathml.rb', line 33 def visit_molecule(molecule) mrow = el("mrow") if molecule.stereo mrow.add_child(mtext("(#{molecule.stereo_letter})-")) end mrow.add_child(mn(molecule.coefficient)) if molecule.coefficient molecule.nodes.each { |n| mrow.add_child(render_node(n)) } mrow end |
#visit_reaction(reaction) ⇒ Object
165 166 167 168 169 170 171 |
# File 'lib/asciichem/formatter/mathml.rb', line 165 def visit_reaction(reaction) mrow = el("mrow") add_terms(mrow, reaction.reactants) mrow.add_child(render_arrow(reaction)) add_terms(mrow, reaction.products) mrow end |
#visit_reaction_cascade(cascade) ⇒ Object
173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/asciichem/formatter/mathml.rb', line 173 def visit_reaction_cascade(cascade) mrow = el("mrow") return mrow if cascade.steps.empty? head = cascade.steps.first add_terms(mrow, head.reactants) cascade.steps.each do |step| mrow.add_child(render_arrow(step)) add_terms(mrow, step.products) end mrow end |
#visit_text(text) ⇒ Object
212 213 214 |
# File 'lib/asciichem/formatter/mathml.rb', line 212 def visit_text(text) mtext(text.content) end |
#wrap_lewis_prefix(base, atom) ⇒ Object
80 81 82 83 84 85 86 87 |
# File 'lib/asciichem/formatter/mathml.rb', line 80 def wrap_lewis_prefix(base, atom) return base unless atom.lone_pairs mrow = el("mrow") mrow.add_child(mtext(":" * atom.lone_pairs)) mrow.add_child(base) mrow end |
#wrap_lewis_suffix(base, atom) ⇒ Object
89 90 91 92 93 94 95 96 |
# File 'lib/asciichem/formatter/mathml.rb', line 89 def wrap_lewis_suffix(base, atom) return base unless atom.radical_electrons mrow = el("mrow") mrow.add_child(base) mrow.add_child(mtext("." * atom.radical_electrons)) mrow end |
#wrap_ring_closures(base, atom) ⇒ Object
60 61 62 63 64 65 66 67 |
# File 'lib/asciichem/formatter/mathml.rb', line 60 def wrap_ring_closures(base, atom) return base unless atom.ring_closures mrow = el("mrow") mrow.add_child(base) mrow.add_child(mn(atom.ring_closures)) mrow end |
#wrap_sub_and_super(base, atom) ⇒ Object
Combine subscript + superscript-style marker into the right MathML element. Cases:
sub + super -> <msubsup>
sub only -> <msub>
super only -> <msup> (with charge/oxidation/raw super)
neither -> base unchanged
104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/asciichem/formatter/mathml.rb', line 104 def wrap_sub_and_super(base, atom) has_sub = atom.subscript && !atom.subscript.empty? super_node = super_element(atom) return base unless has_sub || super_node return wrap_in_sub(base, mn(atom.subscript)) if has_sub && !super_node return wrap_in_sup(base, super_node) if !has_sub && super_node msubsup = el("msubsup") msubsup.add_child(base) msubsup.add_child(mn(atom.subscript)) msubsup.add_child(super_node) msubsup end |