Universe

typed-smiles renders SMILES strings as clean 2D molecular diagrams in Typst. It uses a small Rust/WASM plugin for parsing and layout, then draws the result with CeTZ.

The package is meant for chemistry notes, reaction schemes, reports, and teaching material where you want molecules to live directly in your Typst source instead of copying diagrams from a separate editor.

Full documentation: see docs/documentation.pdf in the typed-smiles repository for every argument, syntax extension, color option, and reaction-scheme feature with live examples.


Quick start

#import "@preview/typed-smiles:0.7.0": *

A wildcard import gives you the molecule renderer, reaction helpers, and mechanism helpers: smiles, ce, mol, rxn-arrow, reaction, atom, bond, lp, species, arrow, highlight, and brackets.

Basic molecule drawing

Pass a SMILES string to #smiles() and it draws the skeletal structure. Aromatic rings can be written either in lowercase aromatic notation (c1ccccc1) or in Kekulé form (C1=CC=CC=C1); aromatic input is kekulized on parse and both render identically.

#import "@preview/typed-smiles:0.7.0": smiles

#table(
  columns: (1fr, 1fr, 1fr, 1fr),
  gutter: 0em, row-gutter: 0em,
  align: center + horizon,
  stroke: 0.4pt + rgb("#d8d8d8"),

  [*Ethanol*], [*Alanine*], [*Chlorobenzene*], [*Furan*],

  [#smiles("CCO")],
  [#smiles("CC(N)C(=O)O")],
  [#smiles("ClC1=CC=CC=C1")],
  [#smiles("C1=CC=CO1")],
)

Basic molecule examples

Scaling

scale resizes bond length, atom label size, and stroke together. Individual overrides (bond-length, font-size, bond-stroke) let you tune one dimension on its own.

#table(
  columns: (1fr, 1fr, 1fr),
  gutter: 0em, row-gutter: 0em,
  align: center + horizon,
  stroke: 0.4pt + rgb("#d8d8d8"),

  [*Small*], [*Default*], [*Large*],

  [#smiles("C1=CC=CC=C1", scale: 0.8)],
  [#smiles("C1=CC=CC=C1")],
  [#smiles("C1=CC=CC=C1", scale: 1.4)],
)

Balanced scaling examples

Journal style presets

style fills in bond length, label size, line width, font, and monochrome atom labels from a journal’s published ChemDraw drawing settings — ACS 1996 (14.4 pt bonds, 10 pt labels), RSC (12.2 pt, 7 pt), Nature Portfolio (10.8 pt, 6 pt), and Wiley/Angewandte (17 pt, 12 pt), all with a Helvetica/Arial stack. Explicit arguments always win, scale multiplies the whole preset, and "default" applies nothing. Pass color: true if you want a journal size preset with CPK colors.

#smiles("CC(N)C(=O)O", style: "acs")
#smiles("CC(N)C(=O)O", style: "nature")
#smiles("CC(N)C(=O)O", style: "acs", color: true)
#smiles("CC(N)C(=O)O", style: "acs", font-size: 14pt)  // preset + override

Mirroring

mirror reflects a molecule horizontally or vertically. Wedges and hashes are exchanged whenever a single-axis reflection happens, keeping the depicted stereochemistry intact. The mirror direction is resolved in the final drawing, so mirror: "vertical" preserves left/right even when rotation is set. It works per molecule inside reaction() via mol("...", mirror: "horizontal").

#smiles("CC(=O)OC1=CC=CC=C1C(=O)O")
#smiles("CC(=O)OC1=CC=CC=C1C(=O)O", mirror: "horizontal")
#smiles("CC(=O)OC1=CC=CC=C1C(=O)O", mirror: "vertical")

Mirroring examples

Aromatic ring circles

Rings written in aromatic (lowercase) notation can draw as single bonds with an inscribed circle instead of alternating double bonds. Each fully aromatic ring of a fused system gets its own circle; Kekulé-written input keeps its explicit bonds.

#smiles("c1ccccc1", aromatic: "circle")
#smiles("c1ccc2ccccc2c1", aromatic: "circle")
#smiles("Cc1ccncc1", aromatic: "circle")

Hydrogens, labels, and fonts

Heteroatom hydrogens are shown by default; carbon hydrogens stay implicit. Use show-h: "all" for carbon hydrogens, [NH3] bracket syntax for explicit hydrogens, and {label} / {label|style} for custom group labels. Use > inside a custom label to choose the attachment glyph, e.g. {>PPh3}. For the cleanest result, rotate the molecule so the bond approaches the chosen glyph roughly perpendicular to the written label. Use _(...) and ^(...) for explicit label subscripts and superscripts: {PPh_(3)}, {NH_4^+}, and {SO_(4)^(2-)}. When both follow one glyph, they attach to that glyph, so {NH_4^+} renders the charge on H rather than 4, with script sizing and placement matching ce() notation. font sets the atom-label typeface.

#table(
  columns: (1fr, 1fr, 1fr, 1fr, 1fr),
  gutter: 0em, row-gutter: 0em,
  align: center + horizon,
  stroke: 0.4pt + rgb("#d8d8d8"),

  [*Default hetero H*], [*All H*], [*Explicit H*], [*Colored label*], [*Custom font*],

  [#smiles("CC(N)C(=O)O")],
  [#smiles("CCO", show-h: "all")],
  [#smiles("[NH3]")],
  [#smiles("{>PPh3|P}C=O")],
  [#smiles("CCN", font: "Libertinus Serif")],
)

Hydrogen and custom label examples

Atom annotations and per-atom hydrogens

atom-annotations places small gray side labels on the emptiest side of an atom — NMR numbering, Greek positions, footnote marks. Pass a tuple list where each entry is (index, content) or (index, content, offset). Values are content, so wrap them in text() to restyle. show-h labels selected carbon hydrogens with show-h: 1 or show-h: (1, 2), and labels every implicit hydrogen with show-h: "all".

#smiles(
  "N[C@@H](C)C(=O)O",
  atom-annotations: (
    (1, [$alpha$], (-0.4, -0.05)),
    (2, [$beta$]),
    (3, [$gamma$], (-0.05, -0.3)),
  ),
)
#smiles("CC(N)C(=O)O", show-h: 1)   // label just the central C-H

Atom annotation and per-atom hydrogen examples

Lone pairs

Set lone-pairs to "dots" or "lines" to annotate skeletal structures with non-bonding electron pairs on common organic heteroatoms and charged atoms.

#smiles("CCO", lone-pairs: "dots")
#smiles("CCN", lone-pairs: "lines")
#smiles("CC(=O)N", lone-pairs: "dots")

Lone pair examples

Colors

Atoms are colored with the Jmol CPK palette. Use atom-colors to override specific elements or labeled groups per call, or use .with() to set project-wide defaults. Label colors in {label|style} accept 17 named colors or any #RRGGBB hex code. See the documentation for the full color reference.

// Override an element and a specific label group:
#smiles("{>PPh3}C({OEt})=O",
  atom-colors: (O: rgb("#8B4513"), "{PPh3}": rgb("#7B2D8B")))

// Set defaults for the whole document in the preamble:
#let smiles = smiles.with(
  bond-length: 0.9,
  atom-colors: (O: rgb("#8B4513"), N: rgb("#008080")),
)

// Hex and extra named colors in labels:
#smiles("{Cat|teal}C(=O){Nuc|#E040FB}")

Color override examples

Note: color: false is a hard override — it makes everything black regardless of any atom-colors entries or inline label styles. To selectively highlight a group in an otherwise black-and-white diagram, set color: true and drive everything through atom-colors.

Dark mode and theming

Bond strokes and carbon labels follow fg, which defaults to auto and inherits the surrounding text color. On a dark slide theme, theme: auto switches to a dark CPK variant for hues that need more contrast.

#smiles("NC(Br)C(I)C(=O)O")

#block(fill: rgb("#1E1E24"), inset: 8pt, radius: 4pt)[
  #set text(fill: white)
  #smiles("NC(Br)C(I)C(=O)O")
]

Light and dark theme comparison

Bond customizations and opacity

bond-customizations restyles individual bonds — color, width, or opacity — keyed by the same bond(i, j) references used for mechanism arrows (turn on show-indices: true while writing them). opacity fades a whole molecule, labels and all, for ghost or de-emphasized species.

#smiles(
  "CC(=O)OCC",
  bond-customizations: (
    (bond(1, 3), (color: red, stroke: 1.4pt)),  // breaking bond
    (bond(3, 4), (opacity: 30%)),               // fading bond
  ),
)
#smiles("CCO", opacity: 30%)   // ghost molecule

Overrides apply to every part of a bond: both lines of a double bond, hash lines, waves, and dashes. Both options also work per molecule inside reaction() via mol("...", opacity: 30%).

Inline molecules

smiles-inline scales a structure to a target height (default 1.4em) and baseline-aligns it so it reads inline without disturbing line spacing. Extra arguments pass through to smiles().

Dehydrating ethanol #smiles-inline("CCO") gives
ethylene #smiles-inline("C=C"); toluene
#smiles-inline("Cc1ccccc1", height: 1.2em) is a common solvent.

CeTZ integration

smiles-cetz draws a molecule inside your own CeTZ canvas and registers atom-<i>, bond-<i>-<j>, and center anchors, so any CeTZ drawing attaches to real molecular positions — dashed hydrogen bonds between molecules, distance labels, coupling arcs, custom arrows. Wrap the canvas in context and use length: 30pt to match #smiles() sizing. CeTZ relative coordinates offset endpoints: (rel: (dx, dy), to: "A.atom-0"). Here is a Watson–Crick A–T base pair with its two hydrogen bonds nudged off the atom centers:

#align(center, context cetz.canvas(length: 30pt, {
  import cetz.draw: *

  smiles-cetz("Nc1ncnc2N(!s{})cnc12", name: "A")
  smiles-cetz("Cc1cN(!s{})c(=O)[nH]c1=O", name: "T", origin: (4.9, 0.42))

  let hb = (paint: rgb("#3A78C9"), thickness: 1pt, dash: "densely-dashed")
  let off(anchor, by) = (rel: by, to: anchor)
  line(off("A.atom-11", (0.4, -0.15)), off("T.atom-9", (-0.2, 0.06)), stroke: hb)
  line(off("A.atom-2", (0.15, 0)), off("T.atom-7", (-0.2, 0)), stroke: hb)

  content((rel: (0.2, 0.2), to: ("A.atom-11", 50%, "T.atom-9")), text(size: 7.5pt, fill: rgb("#3A78C9"))[2.9 Å])
  content((rel: (0, 0.28), to: ("A.atom-2", 50%, "T.atom-7")), text(size: 7.5pt, fill: rgb("#3A78C9"))[2.8 Å])
}))

Watson-Crick A-T base pair composed with CeTZ anchors

Chemical formulas and equations

ce is re-exported from chemformula, so one import covers both structures and formulas.

#import "@preview/typed-smiles:0.7.0": ce

#table(
  columns: (1fr, 1fr),
  gutter: 0em, row-gutter: 0em,
  align: center + horizon,
  stroke: 0.4pt + rgb("#d8d8d8"),

  [#stack(spacing: 0.35cm, strong[Formula], ce("H2SO4"))],
  [#stack(spacing: 0.35cm, strong[Ions], ce("(NH4)2SO4"))],
  [#stack(spacing: 0.35cm, strong[Combustion], ce("CH4 + 2O2 -> CO2 + 2H2O"))],
  [#stack(spacing: 0.35cm, strong[Equilibrium], ce("N2 + 3H2 <=> 2NH3"))],
)

Chemical formula and equation examples

Molecular weights

mol-weight(smiles) returns the molecular weight in g/mol as a float — the sum of IUPAC standard atomic weights over every atom, including implicit and explicit hydrogens. Dot-separated fragments (salts, hydrates) are summed together.

#import "@preview/typed-smiles:0.7.0": mol-weight

Ethanol: #calc.round(mol-weight("CCO"), digits: 2) g/mol // 46.07
Caffeine: #calc.round(mol-weight("CN1C=NC2=C1C(=O)N(C(=O)N2C)C"), digits: 2) g/mol // 194.19

Inputs whose weight is undefined fail with a descriptive error: wildcard * atoms, {label} abbreviations (no defined composition), and isotope-labeled atoms such as [2H] (a nuclide mass, not a standard atomic weight, would be needed).

Reaction schemes

reaction, rxn-arrow, and mol compose molecules, formulas, and arrows into schemes. reaction(scale: 0.8) shrinks the whole scheme uniformly. By default, reaction is non-breakable — the entire block moves to the next page as a unit if it does not fit.

#import "@preview/typed-smiles:0.7.0": smiles, ce, rxn-arrow, mol, reaction

#stack(
  spacing: 1cm,
  stack(
    spacing: 0.4cm,
    align(center, strong[Fischer esterification]),
    align(center, reaction(
      mol(smiles("CC(=O)O"), label: text(size: 8pt)[acetic acid]),
      [+],
      mol(smiles("CCO"), label: text(size: 8pt)[ethanol]),
      rxn-arrow(above: ce("H+"), below: [heat]),
      mol(smiles("CCOC(=O)C"), label: text(size: 8pt)[ethyl acetate]),
      [+],
      ce("H2O"),
    )),
  ),
  stack(
    spacing: 0.4cm,
    align(center, strong[Electrophilic aromatic bromination]),
    align(center, reaction(
      mol(smiles("C1=CC=CC=C1"), label: text(size: 8pt)[benzene]),
      rxn-arrow(above: ce("Br2"), below: ce("FeBr3")),
      mol(smiles("BrC1=CC=CC=C1"), label: text(size: 8pt)[bromobenzene]),
    )),
  ),
)

Reaction scheme examples

rxn-arrow(kind: "equilibrium") draws an open equilibrium arrow. Use kind: "equilibrium-filled" for filled half-heads.

#reaction(
  ce("A"),
  rxn-arrow(kind: "equilibrium", above: ce("H+"), below: [heat]),
  ce("B"),
  rxn-arrow(kind: "equilibrium-filled", above: [cat.]),
  ce("C"),
)

Multi-step mechanisms

Reaction arrows can point right, left, up, or down for compact wrap-around schemes.

#stack(
  spacing: 1.2em,
  align(center, strong[Bromination, nitration, and reduction sequence]),
  align(center, reaction(
    mol(smiles("C1=CC=CC=C1"), label: text(size: 8pt)[1]),
    rxn-arrow(above: ce("Br2"), below: ce("FeBr3")),
    mol(smiles("BrC1=CC=CC=C1"), label: text(size: 8pt)[A]),
    rxn-arrow(dir: "down", above: ce("HNO3"), below: ce("H2SO4")),
    mol(smiles("BrC1=CC(=CC=C1)[N+](=O)[O-]"), label: text(size: 8pt)[B]),
    rxn-arrow(dir: "left", above: ce("Fe"), below: ce("HCl")),
    mol(smiles("BrC1=CC(=CC=C1)N"), label: text(size: 8pt)[C]),
  )),
)

Wrap-around reaction scheme

Electron-pushing mechanisms

reaction() also draws curly-arrow mechanisms. Atoms are referenced by their writing-order index (0-based), so the SMILES string is never modified — pass show-indices: true to read the numbers off the diagram while you write arrows. On large mechanisms, reaction(show-indices: true) applies that overlay to all string mol("...") molecules in the reaction, with per-molecule opt-out via mol("...", show-indices: false). Pass a SMILES string to mol(...) (not smiles(...)) so the reaction renders it itself and its atoms become addressable; offset: nudges a species in page coordinates, so (0.5, 0) always moves it right even in vertical flows. A curly arrow() or highlight() switches reaction() from a grid into one shared canvas — plain schemes are unaffected.

#smiles(
  "N1CCN(CC1)C(C(F)=C2)=CC(=C2C4=O)N(C3CC3)C=C4C(=O)O",
  highlight((bond(0, 5), bond(5, 4), bond(4, 3), bond(3, 6), bond(6, 10), bond(10, 11), bond(11, 15), bond(15, 19), bond(19, 20), bond(20, 21), bond(21, 23), bond(23, 25)), fill: rgb(150, 191, 13), include-atoms: true),
  highlight((bond(15, 16), bond(16, 18), bond(18, 17), bond(17, 16)), fill: rgb(242, 148, 1), include-atoms: true),
  highlight((bond(3, 2), bond(2, 1), bond(1, 0)), fill: rgb(137, 199, 168), include-atoms: true),
  highlight((bond(6, 7), bond(7, 8), bond(7, 9), bond(9, 12)), fill: rgb(201, 143, 75), include-atoms: true),
  highlight((bond(11, 12), bond(12, 13), bond(13, 20), bond(13, 14)), fill: rgb(236, 119, 137), include-atoms: true),
  highlight((bond(21, 22)), fill: rgb(0, 134, 203), include-atoms: true),
  color: false,
  rotation: 90deg,
  bond-stroke: 0.8pt,
  scale: 0.5,
)

#reaction(
  mol("[OH-]", lone-pairs: "dots", offset: (0, 1)),
  mol("C(I)(C)C"),
  arrow(from: lp(0, 0, offset:(0.1, -0.2)), to: atom(1, 0, offset : (0.1, -0.1)),
        bend: "right", color : black),
)

#brackets(
  [#reaction(smiles("CC(=O)C"), rxn-arrow(), smiles("O=C=O"), scale: 0.55)],
  sup: [],
)

References: atom(s, i), bond(s, i, j), lp(s, i) (the species index s is optional inside a single smiles()), and species(k) for a whole ce()/content item. Every reference takes an optional offset: (dx, dy). Curly arrows accept heads: "end"/"both"/"none" and style: "solid"/"dashed"/"wavy", all working with bend — e.g. a double-headed dashed interaction arrow or a wavy photochemical one.

Electron-pushing mechanism examples

In mechanism mode reaction(scale:) sets the shared canvas scale and each mol(scale:) multiplies it for its own species, so molecules in one mechanism can be sized individually. A mol() item placed in a rxn-arrow(above:/below:) slot draws a molecule over or under the arrow; in mechanism mode it also becomes a species of its own, counted in written order at the arrow’s position (above before below), so curly arrows can connect it to any other species:

#reaction(
  mol("C=C"),
  rxn-arrow(above: mol("BrBr", scale: 0.8), below: [CCl#sub[4]]),
  mol("BrC(Br)C"),
  arrow(from: bond(0, 0, 1), to: atom(1, 0), color: red),
  arrow(from: atom(1, 0), to: atom(1, 1), color: red, bend: "right"),
)

Catalytic cycles

cycle arranges species on a circle with arc arrows between them. Items alternate species and step()s, like reaction() alternates molecules and arrows, but the sequence closes into a ring. step(label:) names a transformation, step(into:) adds a reagent merging in, and step(out:) a product leaving. cycle(reagent-bend:) sets the default side-arrow curvature and cycle(arc-gap:) how close the arc arrows sit to the species; per step, bend: overrides the curvature, merge: true fuses the side arrow tangentially with the main arc, rotation: rotates the step label ("straight", "auto", or an angle), and label-offset:/into-offset:/out-offset: nudge pieces like a mol offset.

#import "@preview/typed-smiles:0.7.0": cycle, step, mol, ce

#let cplx(body) = box(inset: 2pt, body)

#cycle(
  radius: 4.3,
  reagent-bend: 0.06,
  mol(cplx(ce("RhCl(PPh3)3"))),
  step(label: ce("-PPh3 + S")),
  mol(cplx(ce("RhCl(PPh3)2S"))),
  step(label: [oxidative addition], into: ce("H2"), bend: 0.02),
  mol(cplx(ce("RhH2Cl(PPh3)2"))),
  step(label: [coordination], into: ce("RHC=CH2"), rotation: "auto"),
  mol(cplx(ce("RhH2Cl(PPh3)2(\"alkene\")"))),
  step(label: [migratory insertion]),
  mol(cplx(ce("RhHCl(CH2CH2R)(PPh3)2"))),
  step(label: [reductive elimination], out: ce("RCH2CH3"), bend: 0.03),
)

Wilkinson-style catalytic cycle with vertical entry

Stereochemistry and drawing extensions

[C@H] / [C@@H] mark tetrahedral centers; / and \ describe cis/trans geometry. !w forces a solid wedge, !h a hashed wedge, !s a wavy (squiggly) bond for unspecified stereochemistry or attachment points, and !d a dashed bond for hydrogen bonds, partial bonds, and coordination.

#table(
  columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr),
  gutter: 0em, row-gutter: 0em,
  align: center + horizon,
  stroke: 0.4pt + rgb("#d8d8d8"),

  [*Manual wedge*], [*Manual hash*], [*Wavy*], [*Dashed*],
  [*Tetrahedral @@*], [*trans alkene*],

  [#smiles("C!wN")],
  [#smiles("C!hN")],
  [#smiles("C!sN")],
  [#smiles("C!dN")],
  [#smiles("N[C@@H](C)C(=O)O", scale: 0.7)],
  [#smiles("F/C=C/F", scale: 0.7)],
)

Stereochemistry and drawing extension examples

API summary

#smiles(smiles-str, …)

Parameter Default Description
smiles-str required OpenSMILES string
style "default" Journal preset: "acs", "rsc", "nature", "wiley"; explicit arguments win
scale 1.0 Balanced scale for bond length, labels, and stroke
bond-length none Bond length only (1.0 = 30 pt per bond)
font-size none Atom-label size only
font auto Atom-label font; auto is “New Computer Modern” or the preset’s font
bond-stroke none Bond width only
color auto CPK colors for "default"; monochrome for journal presets
fg auto Foreground for bonds/carbon labels; auto inherits the text color
theme auto CPK palette variant; auto goes dark when fg is light
rotation 0deg Rotate molecule; labels stay upright
mirror none Optional "horizontal" or "vertical" reflection
show-h () Label selected implicit hydrogens; use "all" for every atom
aromatic "kekule" Lowercase-aromatic rings as doubles or "circle"
atom-annotations () Small gray side labels as (index, content) or (index, content, offset) tuples
opacity 100% Fade the whole drawing (ghost molecules)
bond-customizations () Per-bond color, stroke, opacity keyed by bond(i, j)
lone-pairs none Draw lone pairs as "dots" or "lines"
atom-colors (:) Color overrides: element key O: red or label key "{PPh3}": blue
show-indices false Stamp atom indices for writing arrow references
…annotations arrow() / highlight() items on this molecule

SMILES string extensions:

Syntax Meaning
{label} Literal upright label at an atom position
{>label} Label anchored at the glyph after >; the marker is not shown
{label_(sub)^(sup)} Explicit label subscript and superscript; either group may be one character without parentheses
{label|N} Label and bonds colored like element N
{label|red} Label colored with a named color (17 names supported)
{label|#RRGGBB} Label colored with a hex code
!w Force a solid wedge on the next single bond
!h Force a hashed wedge on the next single bond
!s Force a wavy (squiggly) bond on the next single bond
!d Force a dashed bond on the next single bond

#reaction(gap-h, gap-v, scale, breakable, show-indices, flow, …items)

Lays out a scheme (grid) or, when any curly arrow()/highlight() is present, an electron-pushing mechanism (shared canvas).

Parameter Default Description
gap-h 1.5em Horizontal gap between items
gap-v 1.5em Vertical gap between rows
scale 1.0 Uniform scale applied to the entire scheme
breakable false Allow splitting across pages
flow "right" Writing direction: "right", "left", "up", "down"; "left"/"up" reflect the scheme so branches emerging left/bottom read naturally
show-indices false Default atom-index overlay for string SMILES molecules in this reaction

For vertical flows, ordinary non-arrow items stack vertically too, so reaction(flow: "down", mol("A"), [+], mol("B")) reads top-to-bottom.

A flow: "left" (or "up") sub-reaction() inside a cycle’s step(out:) grows a branch that reads away from the ring, while the outer reaction keeps its own direction — so a main reaction can embed a cycle whose branch runs its own sub-reaction, then continue.

#rxn-arrow(above, below, dir, kind, scale)

Parameter Default Description
above none Label above a horizontal arrow (or right of vertical); content or a mol() item
below none Label below a horizontal arrow (or left of vertical); content or a mol() item
dir auto "right", "left", "down", "up", or auto (follows reaction(flow:))
kind "single" "single", "equilibrium", "equilibrium-filled", "dashed", or "wavy"
scale 1.0 Uniform arrow scale, including condition labels
color auto Arrow color; auto inherits the surrounding text color

scale resizes the complete arrow component proportionally, including the shaft, arrowhead, spacing, and above/below labels.

#mol(spec, label: none, offset: (0,0), …opts)

A reaction item. spec is any content (smiles(...), ce(...), text) or a SMILES string — a string lets reaction() render it with addressable atoms. offset nudges it in page coordinates, in bond-length units: positive x moves right and positive y moves up regardless of reaction(flow:). String molecules accept common drawing options such as scale, font-size, font, bond-stroke, color, rotation, show-h, lone-pairs, opacity, bond-customizations, atom-colors, and show-indices; reaction(scale: ...) resizes a shared mechanism canvas, and each mol(scale: ...) multiplies it for that molecule alone. A mol() item also works inside rxn-arrow(above:/below:) slots. In ordinary schemes, the offset also updates the reaction’s layout bounds, so wrappers such as brackets() follow a shifted edge.

#cycle(radius: auto, start: 90deg, clockwise: true, scale: 1.0, reagent-bend: 0.12, arc-gap: 0.15, …items)

Arranges species on a circle with arc arrows for catalytic cycles. Items alternate species (mol()/content) and step(label:, into:, out:, bend:, merge:, rotation:, label-offset:, into-offset:, out-offset:)s; the sequence closes into a ring. arc-gap tunes arrow-to-species clearance; step(merge: true) fuses a side arrow with the arc; the *-offset arguments nudge pieces; step(out:) accepts any content, so a nested reaction() grows a branch out of a released molecule. Incoming content attaches by the last upstream item; outgoing content attaches by the first downstream item. step(rotation: "auto") angles labels along the circle while keeping them upright; an explicit angle such as rotation: 45deg is also accepted.

Mechanism helpers

Helper Purpose
atom(i) / atom(s, i) Atom center reference
bond(i, j) / bond(s, i, j) Bond-midpoint reference
lp(i) / lp(s, i) Lone-pair reference (pair: n to select)
species(k) Bounding-box edge of a whole item
arrow(from:, to:, label:, color:, bend:, angle:, half:, heads:, style:) Curly electron arrow; heads: "end"/"both"/"none", style: "solid"/"dashed"/"wavy"
highlight(ref, fill:, stroke:, radius:) Shade an atom (disk) or bond (capsule)
brackets(body, sup:, sub:) Square brackets with optional corner marks

All references accept an offset: (dx, dy) nudge.

#ce(chem, font: none, font-size: none, …)

Re-exports chemformula’s ch. Accepts font and font-size for local styling; other arguments pass through to chemformula.

#mol-weight(smiles-str)

Molecular weight in g/mol as a float. Errors on wildcards, abbreviations, and isotopes.

#smiles-inline(smiles-str, height: 1.4em, baseline: auto, …args)

A molecule scaled to height and baseline-aligned for running text. The scale is capped so flat molecules stay compact; extra arguments pass through to smiles().

#smiles-cetz(smiles-str, name:, origin: (0,0), fg: black, theme: "light", …opts)

The molecule as CeTZ draw elements with named anchors (atom-<i>, bond-<i>-<j>, center) for composing custom CeTZ diagrams. Use inside context cetz.canvas(length: 30pt, ...); …opts are smiles() drawing options.

SMILES support

The package uses the smiles-parser crate for parsing.

Aromatic lowercase notation (c1ccccc1, c1cc[nH]c1, …) is kekulized on parse following OpenSMILES; rings that cannot be kekulized are reported as errors.

Ring closures written next to branch points are supported, including forms like C1=CCCC(=O)1 where the closure digit follows the carbonyl branch.

Dot-disconnected SMILES (CC(=O)[O-].[Na+]) draw each fragment separately, side by side in writing order — salts, counterions, and hydrates render without a spurious bond between fragments.

Current limitations:

  • @/@@ and //\ stereochemistry is depicted but R/S and E/Z descriptors are not computed.
  • Square-planar @SP1@SP3 centers are depicted exactly (the geometry is planar); quadruple bonds ($) render as four parallel lines.
  • Trigonal-bipyramidal (@TB), octahedral (@OH), and allenal (@AL) centers are accepted and drawn with correct connectivity, but without stereo wedges.
  • Bridged bicyclics may overlap; template matching is not implemented.

License

MIT