HomeFork fixes
Real renders, not mockups

What this fork fixes

Every pair below is rendered by this project's own renderer. The before side runs the code as it existed immediately before the fix landed — the tree at that commit's parent — so nothing here is hand-drawn or reconstructed. The generator fails the build if any pair renders identically, because a before/after that looks the same would claim a fix it does not demonstrate.

27 fixes shownMany more ship in the changelog.

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.

no-space-arrows.mmd
flowchart LR
A-->B
B-->C
Before
A-- B--
After
A B C
Look for

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.

quoted-brackets.mmd
flowchart LR
A["test [] brackets"] --> B["fine"]
Before
"test [
After
test [] brackets fine
Look for

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.

cjk-subgraph-id.mmd
flowchart TD
subgraph 柜体 [Cabinet]
A[Shelf] --> B[Door]
end
B --> C[Done]
Before
柜体 [Cabinet] Done Shelf Door
After
Cabinet Done Shelf Door
Look for

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.

cjk-box-width.mmd
flowchart TD
A[日本語テスト] --> B[終了]
Beforebefore terminal output of `zombie-mermaid render cjk-box-width.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render cjk-box-width.mmd --ascii`
Look for

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.

circle-cross-edges.mmd
flowchart LR
A --o B
B --x C
Beforebefore terminal output of `zombie-mermaid render circle-cross-edges.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render circle-cross-edges.mmd --ascii`
Look for

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.

er-cardinality.mmd
erDiagram
TAG }o--|| PRODUCT : labels
Before
Rendered nothing — the diagram was dropped entirely.
Afterafter terminal output of `zombie-mermaid render er-cardinality.mmd --ascii`
Look for

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.

PR #50e1a222cSVG output

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.

start-arrow-markers.mmd
flowchart LR
A <--> B
Before
<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>
After
<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>
Look for

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).

PR #93e989be1SVG output

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.

nested-subgraph-direction.mmd
flowchart TD
subgraph Outer
direction LR
subgraph Inner
direction TB
A --> B
end
C --> A
end
B --> D
Before
Outer Inner D C A B
After
Outer Inner D C A B
Look for

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.

fan-in-grouping.mmd
flowchart TD
A1 --> A
B1 --> B
A2 --> A
B2 --> B
A --> C
B --> C
Beforebefore terminal output of `zombie-mermaid render fan-in-grouping.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render fan-in-grouping.mmd --ascii`
Look for

Before: the top row reads A1, B1, A2, B2 — the two groups interleaved. After: A1, A2, B1, B2, each pair above its own target.

PR #92407355cASCII output

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.

class-blank-compartment.mmd
classDiagram
class Service {
+start() void
+stop() void
}
Beforebefore terminal output of `zombie-mermaid render class-blank-compartment.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render class-blank-compartment.mmd --ascii`
Look for

Before: an empty compartment between the name and the methods. After: none.

PR #77ffa9a85SVG output

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.

class-shorthand-label.mmd
flowchart LR
A:::external[External User] --> B[Internal]
Before
A
After
External User Internal
Look for

Before: the node shows its bare id. After: it shows its label.

PR #5350d8568SVG output

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".

class-trailing-semicolon.mmd
flowchart LR
A --> B
classDef highlight fill:#f9d5e5,stroke:#b5838d
class B highlight;
Before
A B class
After
A B
Look for

Before: a spurious "class" node appears. After: B is simply styled.

PR #72554d7b4ASCII output

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.

sequence-leading-note.mmd
sequenceDiagram
Note over Alice: Ready to begin
Alice->>Bob: Hello
Bob-->>Alice: Hi
Beforebefore terminal output of `zombie-mermaid render sequence-leading-note.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render sequence-leading-note.mmd --ascii`
Look for

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.

er-entity-alias.mmd
erDiagram
p[Person] { string firstName }
a["Customer Account"] { string email }
p ||--o| a : has
Before
p (no attributes) a (no attributes) has
After
Person string firstName Customer Account string email has
Look for

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.

er-direction.mmd
erDiagram
direction TB
CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ LINE_ITEM : contains
PRODUCT ||--o{ LINE_ITEM : includes
Before
CUSTOMER (no attributes) ORDER (no attributes) LINE_ITEM (no attributes) PRODUCT (no attributes) places contains includes
After
CUSTOMER (no attributes) ORDER (no attributes) LINE_ITEM (no attributes) PRODUCT (no attributes) places contains includes
Look for

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.

subgraph-frame-merge.mmd
flowchart TB
subgraph a["Frontend tier"]
a1["load balancer"]
end
subgraph b["Application tier"]
b1["worker pool"]
b2["api server"]
end
a1 --> b1
Beforebefore terminal output of `zombie-mermaid render subgraph-frame-merge.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render subgraph-frame-merge.mmd --ascii`
Look for

Before: the two subgraph frames merge into one box with a garbled title. After: two distinct, disjoint frames.

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.

class-attribute-on-svg.mmd
flowchart LR
A[Start]:::highlight --> B[End]
classDef highlight fill:#f9d5e5,stroke:#b5838d
Before
<g class="node" data-id="A"
After
<g class="node highlight" data-id="A"
Look for

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.

cjk-state-names.mmd
stateDiagram-v2
[*] --> 空闲
空闲 --> 处理中 : 提交
处理中 --> 完成
Before
After
提交 空闲 处理中 完成
Look for

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.

text-embedded-edge-labels.mmd
flowchart TD
A(Start) --> B{Is it sunny?}
B -- Yes --> C[Go to the park]
B -- No --> D[Stay indoors]
C --> E[Finish]
D --> E
Before
Start Is it sunny? C Finish D
After
Yes No Start Is it sunny? Go to the park Stay indoors Finish
Look for

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.

er-label-truncation.mmd
erDiagram
CUSTOMER ||--o{ ORDER : places
PRODUCT ||--o{ LINE_ITEM : "ordered in"
Beforebefore terminal output of `zombie-mermaid render er-label-truncation.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render er-label-truncation.mmd --ascii`
Look for

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.

er-stray-tee.mmd
flowchart LR
A{Decision} -->|Yes| B[Do thing]
A -->|No| C[Other thing]
Beforebefore terminal output of `zombie-mermaid render er-stray-tee.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render er-stray-tee.mmd --ascii`
Look for

Before: a stray ├ sits disconnected on the "Yes" edge’s row. After: a plain ─ line in its place.

PR #060a40d2ASCII output

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 .

class-label-column-width.mmd
classDiagram
A <|-- B : inheritance
C *-- D : composition
E o-- F : aggregation
G --> H : association
I ..> J : dependency
K ..|> L : realization
Beforebefore terminal output of `zombie-mermaid render class-label-column-width.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render class-label-column-width.mmd --ascii`
Look for

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.

PR #060a40d2ASCII output

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.

class-fanned-relationships.mmd
classDiagram
class A
class B
A --> B : one
A --> B : two
A --> B : three
A --> B : four
Beforebefore terminal output of `zombie-mermaid render class-fanned-relationships.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render class-fanned-relationships.mmd --ascii`
Look for

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.

PR #01bf8096ASCII output

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.

class-label-detour-routing.mmd
classDiagram
class Model {
-data Map
+getData() Map
+setData(key, val) void
+notify() void
}
class View {
-model Model
+render() void
+update() void
}
class Controller {
-model Model
-view View
+handleInput(event) void
+updateModel(data) void
}
Controller --> Model : updates
Controller --> View : refreshes
View --> Model : reads
Model ..> View : notifies
Before
┌────────────────────────────┐
│ 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 │
└──────────────────┘

After
┌────────────────────────────┐
│ 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 │
└──────────────────┘

Look for

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.

PR #02a06338ASCII output

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).

class-generic-types.mmd
classDiagram
class EventEmitter {
-List~Observer~ observers
+attach(Observer) void
}
class Observer {
<<interface>>
+update() void
}
EventEmitter --> Observer
Beforebefore terminal output of `zombie-mermaid render class-generic-types.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render class-generic-types.mmd --ascii`
Look for

Before: the EventEmitter attribute reads List~Observer~. After: it reads List<Observer>, the form real mermaid renders.

PR #064c8dd4ASCII output

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).

edge-label-diagonal-fallback.mmd
graph TD
A[Source] -->|solid| B[Target 1]
A -.->|dotted| C[Target 2]
A ==>|thick| D[Target 3]
Beforebefore terminal output of `zombie-mermaid render edge-label-diagonal-fallback.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render edge-label-diagonal-fallback.mmd --ascii`
Look for

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.

Pick a look

Live in every built-in theme.