Class: AsciiChem::Formatter::Mathml

Inherits:
Base
  • Object
show all
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

Methods inherited from Base

#render

Constructor Details

#initializeMathml

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("&#xA0;")) 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 visit_embedded_math(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 &#x21CC;.
  @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