Arrows with no surrounding space dropped the edge
The bare-node-id scanner greedily ate the arrow’s leading dashes, producing a node literally named A-- and no edge at all — silently, with no error.
Before: one mis-named node and no arrows. After: three nodes, two edges.
Brackets inside a quoted label corrupted the label
The shape-delimiter scanner treated the first ] inside a quoted string as the node’s closing bracket, truncating the label mid-word.
Before: the label is cut off at the inner bracket. After: it reads in full.
CJK subgraph ids produced an empty id
The subgraph-id pattern was ASCII-only ([\w-]+), so a non-ASCII id fell through to title slugification — which stripped every character, leaving an empty id.
Before: the subgraph frame is missing or mislabelled. After: it renders with its title.
CJK labels overflowed their own ASCII box
Box width was measured in UTF-16 code units, but a CJK glyph occupies two terminal columns — so the right border landed inside the label it was supposed to enclose.


Before: the right border sits inside the text and rows are ragged. After: every row is the same width.
Circle and cross edge terminators dropped the target node
A --o B and A --x B were not recognised by the arrow regex, so parsing broke out of the edge loop early — losing the edge and the node it pointed at.


Before: only A and B render, with no edges at all — C is gone. After: all three nodes and both edges.
ER “zero or more” cardinality was silently dropped
Cardinality strings were normalised by sorting their characters, which conflated }o with the unrelated {o/o{ and left it unrecognised. The relationship was dropped, and with only one relationship in the diagram, so were both entities.

Before: nothing renders at all — losing the cardinality dropped the only relationship, and with it both entities. After: the full diagram, crow’s-foot marker included.
Bidirectional arrows rendered invisible in some renderers
orient="auto-start-reverse" already rotates the start arrowhead 180°, but its polygon was also pre-reversed — a double reversal that pointed the head into the line. librsvg and Inkscape drew it as degenerate or invisible.
<marker id="arrowhead-start" markerWidth="8" markerHeight="5" refX="1" refY="2.5" orient="auto-start-reverse">
<polygon points="8 0, 0 2.5, 8 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
</marker><marker id="arrowhead-start" markerWidth="8" markerHeight="5" refX="7" refY="2.5" orient="auto-start-reverse">
<polygon points="0 0, 8 2.5, 0 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
</marker>Chrome tolerates the double reversal, so both sides look the same in a browser — which is why this pair shows the marker definition instead of the picture. Compare the points: before they run 8 0, 0 2.5, 8 5 (already reversed), after 0 0, 8 2.5, 0 5 (shared with the forward marker, letting auto-start-reverse do the rotation once).
Nested subgraph direction overrides were ignored
A direction inside a nested subgraph had no effect, and edges crossing a subgraph boundary fell back to a naive path or failed to route.
Before: the inner group ignores its direction and the crossing edge routes poorly.
Fan-in roots were interleaved instead of grouped
Root nodes were placed without regard to what they fed into, so A1, A2 → A and B1, B2 → B interleaved on the grid instead of sitting together above their own target. The same PR also bounded A* pathfinding and made root detection order-independent, fixing two heap-exhaustion crashes on dense graphs.


Before: the top row reads A1, B1, A2, B2 — the two groups interleaved. After: A1, A2, B1, B2, each pair above its own target.
Class diagrams grew a blank compartment
A class with methods but no attributes rendered an empty middle compartment where the attribute list would have been.


Before: an empty compartment between the name and the methods. After: none.
A class shorthand before the brackets discarded the label
A:::external[External User] — the shape patterns require the id to sit immediately before its brackets, so the ::: token in between made every pattern miss and the label was thrown away.
Before: the node shows its bare id. After: it shows its label.
A trailing semicolon on `class` produced a stray node
class B highlight; is valid Mermaid, but the regex was anchored without the semicolon — so the statement fell through to node parsing and rendered a node labelled "class".
Before: a spurious "class" node appears. After: B is simply styled.
Sequence notes before the first message vanished
A note written before any message parses with afterIndex: -1, but layout only ever looked notes up by a real message index — so it was never positioned or drawn.


Before: the leading note is missing entirely. After: it renders above the first message.
ER entity aliases discarded the alias and every attribute
The entity-header pattern did not recognize id[Alias] bodies at all, so the line fell back to matching only the bare id — losing the alias and the entire attribute block that followed it.
Before: two boxes labeled only "p" and "a", each showing "(no attributes)" — the alias line failed to parse past the id. After: "Person" and "Customer Account", each with its attribute.
ER `direction` directive had no effect on layout
The top-level direction statement was parsed but never threaded into the ELK layout options, so direction TB rendered identically to the default left-to-right layout.
Before: entities lay out left-to-right regardless of the direction TB statement. After: they stack top-to-bottom.
An edge-less subgraph member merged two sibling subgraph frames
Root-node placement treated an edge-less node as a shared "root" regardless of which subgraph it belonged to, so one subgraph’s bounding box could balloon out to enclose an unrelated sibling — corrupting both frames and interleaving their titles.


Before: the two subgraph frames merge into one box with a garbled title. After: two distinct, disjoint frames.
`~~~` invisible-link syntax was not recognized
The arrow regex had no alternative for ~~~, so Mermaid’s invisible-link syntax — used to connect or order nodes without drawing a connector — failed to parse as an edge at all.


Before: C is disconnected from the rest of the graph — the ~~~ link is dropped entirely. After: C sits in the layout slot the invisible link puts it in, with no visible connector drawn to it.
A node’s custom class never reached the rendered SVG
The class name from :::className or class A,B className was resolved for inline fill/stroke, but never written onto the element itself, so external CSS had nothing to select.
<g class="node" data-id="A"
<g class="node highlight" data-id="A"
Both sides look identical on screen — the fill color already worked before this fix. Compare the markup: before, node A’s group carries only class="node"; after, class="node highlight", so a stylesheet rule like .highlight { } can finally target it.
CJK state names failed to parse in state diagrams
stateDiagram transition and state-name patterns were ASCII-only (\w), so a diagram made entirely of Chinese, Japanese, or Korean state names failed to match a single one of them.
Before: a blank canvas — every state name failed to parse, leaving nothing to draw. After: all three states and both transitions render.
Text-embedded edge labels (`-- Yes -->`) were not parsed
Mermaid’s inline label syntax on an arrow body — as opposed to the -->|Yes| pipe form — had no parser support. The label vanished, and the mis-tokenization also swallowed the bracketed label off the node the arrow pointed at.
Before: the "Yes"/"No" edge labels are gone, and C and D render as bare ids instead of "Go to the park" / "Stay indoors". After: every edge and every node keeps its label.
Long ER relationship labels were truncated in ASCII output
Labels longer than a fixed 6-character inter-entity gap were silently cut off — "ordered in" rendered as "ordere" — and even short labels sat flush against both entity boxes.


Before: the second relationship label reads "ordere". After: it reads "ordered in" in full, with padding from both boxes.
A decision node’s edge label could land on a stray tee character
The box-start connector always drew a tee/junction glyph based on the edge’s exit direction alone, with no check that a real perpendicular border line existed at that cell — so a label that shifted the attachment point left a disconnected ├ floating with blank cells on both sides.


Before: a stray ├ sits disconnected on the "Yes" edge’s row. After: a plain ─ line in its place.
Long class relationship labels on narrow classes were truncated
A class’s column was sized from its box alone, never from the relationship label that had to fit beside it — so single-letter classes with labels like inheritance had every label squeezed into the box’s own width and cut to ….


Before: five of the six labels read inheri…, composi…, and so on. After: the columns spread apart just enough for all six to render in full.
More than two relationships between narrow classes overwrote each other
Each relationship in a group got its own column offset, but the offsets were then clamped back inside the box — a width-5 box only has room for three — so several relationships collapsed onto one connection point and the last one drawn silently overwrote the rest.


Before: only four survives, on two lines. After: one, two, three, and four each sit on their own lane, joined to the boxes by a short jog, with four distinct arrowheads.
A detoured relationship’s label ignored its own routed path
A relationship’s label always anchored on the straight-line midpoint between its source and target boxes, even when its line detoured around an intermediate box to avoid it — so the label sat wherever the unrouted line would have gone, not the actual one, reading as though it terminated at the wrong box.
┌────────────────────────────┐
│ Controller │
├────────────────────────────┤
│ - Model: model │
│ - View: view │
├────────────────────────────┤
│ + handleInput(event): void │
│ + updateModel(data): void │
└────────────────────────────┘
refreshes ───────────┐
updates │
▼ │
┌───────────────────────────┐│
│ Model ││
├───────────────────────────┤│
│ - Map: data ││
├───────────────────────────┤│
│ + getData(): Map ││
│ + setData(key, val): void ││
│ + notify(): void ││
└───────────────────────────┘│
▲ ┊ │
reads notifies │
│ ▼─────▼────────────┘
┌──────────────────┐
│ View │
├──────────────────┤
│ - Model: model │
├──────────────────┤
│ + render(): void │
│ + update(): void │
└──────────────────┘
┌────────────────────────────┐
│ Controller │
├────────────────────────────┤
│ - Model: model │
│ - View: view │
├────────────────────────────┤
│ + handleInput(event): void │
│ + updateModel(data): void │
└────────────────────────────┘
└─────────────┐
updates │
▼ │
┌───────────────────────────┐│
│ Model ││
├───────────────────────────┤│
│ - Map: data ││
├───────────────────────────┤│ refreshes
│ + getData(): Map ││
│ + setData(key, val): void ││
│ + notify(): void ││
└───────────────────────────┘│
▲ ┊ │
reads notifies │
│ ▼─────▼────────────┘
┌──────────────────┐
│ View │
├──────────────────┤
│ - Model: model │
├──────────────────┤
│ + render(): void │
│ + update(): void │
└──────────────────┘
Before: refreshes and updates sit stacked on adjacent rows above the single arrowhead entering Model, reading as though both terminate there. After: refreshes sits beside its own detour line, past Model’s right border, clearly distinct from updates.
Class member generics kept mermaid’s raw `~T~` tildes
Mermaid converts List~Observer~ to List<Observer> before rendering; the class parser here kept the tildes, so both the SVG and ASCII output showed List~Observer~ — flagged by the weekly form-judge audit (#418).


Before: the EventEmitter attribute reads List~Observer~. After: it reads List<Observer>, the form real mermaid renders.
An edge label could float next to a different edge’s line
When an edge’s route fell back to a direct path (every L-shaped route and A* blocked — e.g. the third edge leaving the same side of a node), its label was centered on the never-drawn diagonal between the two legs that actually get drawn, landing in open grid next to an unrelated edge’s connector: ┆thick two columns away from the thick edge’s own ┃. Flagged by the weekly form-judge audit (#418).


Before: "thick" sits glued to the dotted edge’s ┆ column. After: it sits on the thick edge’s own ┃ line, like "dotted" does on its ┆ line.