Skip to content

The AsciiChem model

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) │
└──────────────────────────────────┘
output

The 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)
# ...
end
end

Adding 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
end
end
class Reaction
def children
reactants + products
end
end

Adding 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 # => true

Detailed reference for each class:

  • Formula — top-level container
  • Atom — element + isotope + charge + Lewis markers
  • Molecule — ordered atom list + coefficient + stereo
  • Group — parenthesised sub-formula
  • Bond — single / double / triple / quadruple / wedge / hash / dative / wavy
  • Reaction — reactants + arrow + products + conditions
  • ReactionCascade — multi-step chain
  • ElectronConfiguration — orbital occupancy
  • EmbeddedMath — Plurimath wrapper
  • Text — catch-all for untyped runs

Each per-class page has the attribute table, the grammar production that produces it, every formatter’s behaviour, and a round-trip example.