Formula
Top-level container. The root of every parse.
Every AsciiChem parse produces a tree of model instances. The model is the contract — every formatter consumes the same tree, so adding a new output format never touches the parser.
Formula
Top-level container. The root of every parse.
Atom
A chemical atom with element, isotope, charge, oxidation state, Lewis markers.
Molecule
Ordered sequence of atoms/groups with coefficient and stereo.
Group
Parenthesised sub-formula with multiplicity.
Bond
A connection between two adjacent atoms in a chain.
Reaction
Reactants + arrow + products, with optional conditions.
ReactionCascade
A multi-step reaction chain.
ElectronConfiguration
Orbital occupancy list plus optional term symbol.
EmbeddedMath
A Plurimath formula embedded in chemistry text.
Text
Catch-all for runs not promoted to a typed node.
text ──► Grammar ──► parse tree ──► Transform ──► Model │ ▼ ┌──────────────────────────────────┐ │ Formatter (visitor) │ │ MathML · Text · HTML · LaTeX · │ │ SVG · (your new format here) │ └──────────────────────────────────┘ │ ▼ outputThe model is in the middle. New grammar productions add model classes; new output formats add formatter visitors. Existing code stays untouched.
Every model class implements accept(visitor) which calls
visitor.visit_<class>(self). The dispatch is double-dispatch:
class Atom < Node # accept on the base class does: # visitor.public_send(:"visit_atom", self)end
class Formatter::Mathml < Formatter::Base def visit_atom(atom) # ... endendAdding a new model class means defining visit_<class> on every
formatter. The base Formatter::Base raises NotImplementedError
for unimplemented visits, so gaps surface immediately.
Every container class implements #children returning an array of
its child nodes. The linter uses this to walk the tree without
switching on type:
class Molecule def children nodes endend
class Reaction def children reactants + products endendAdding a new container class means defining children on it; the
linter and any future tree-walker pick it up automatically.
Every model class implements value_attributes returning the hash of
fields that participate in equality. Two nodes are equal iff their
classes match and their value_attributes hashes match.
atom_a = Atom.new(element: "C", isotope: "14")atom_b = Atom.new(element: "C", isotope: "14")atom_a == atom_b # => trueDetailed reference for each class:
Each per-class page has the attribute table, the grammar production that produces it, every formatter’s behaviour, and a round-trip example.