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.

59 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="10.5" markerHeight="12.8" refX="9.5" refY="6.4" markerUnits="userSpaceOnUse" orient="auto-start-reverse">
    <polygon points="0 0, 10.5 6.4, 0 12.8" 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.

A CSS `var(...)` font was quoted into a dead font name

{ font: 'var(--font-family-body)' } is the natural way to inherit a host page's font, but the value was wrapped in quotes (an inert font name, not a var() call) and also URL-encoded into a Google Fonts @import that can never resolve.

font-css-variable.mmd
flowchart LR
A --> B
Before
text { font-family: 'var(--font-family-body)', system-ui, sans-serif;
After
text { font-family: var(--font-family-body), system-ui, sans-serif;
Look for

The browser renders both the same until a host page defines the variable, so this pair shows the font-family rule. Before: the reference is quoted, an inert font name (the build also emitted a failing Google Fonts @import for it). After: it is unquoted, so the host page's variable resolves, and the @import is gone.

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 #51260a40d2ASCII 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 #51260a40d2ASCII 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 #5141bf8096ASCII 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                     ││
 ├───────────────────────────┤│
 │ - data Map                ││
 ├───────────────────────────┤│
 │ + getData(): Map          ││ refreshes
 │ + 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 #5052a06338ASCII 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 #50564c8dd4ASCII 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.

State diagrams ignored classDef, class, and ::: styling

State diagrams silently dropped classDef, class A,B name, and State:::name, and S1:::foo --> S2 swallowed S1 into a bogus ::foo --> S2 state, so styled states rendered unstyled with a stray label.

state-classdef-styling.mmd
stateDiagram-v2
classDef hot fill:#f00,color:#fff
classDef cool fill:#39f,color:#fff
[*] --> S1:::hot
S1 --> S2
S2:::cool --> S3
class S3 hot
Before
::hot S1 S2
After
S1 S2 S3
Look for

Before: a stray ::hot label on the first transition and no colors. After: S1 and S3 are red, S2 is blue.

A space before the edge label pipe dropped the target node

A --> |deploy| B required the |label| to touch the arrow, so with a space before the pipe the parser lost both the label and the node it pointed at.

edge-label-space-before-pipe.mmd
graph LR
A --> |deploy| B
B --> |verify| C
Before
A B
After
deploy verify A B C
Look for

Before: the labels and the nodes B and C are missing. After: three nodes, two labelled edges.

ASCII vertical edge labels were printed over the edge line

A label on a vertical edge was drawn on top of the stroke, so the line ran through the gaps between its words (long─label) or vanished under the text.

ascii-vertical-label-beside-stroke.mmd
graph TD
A -->|long label| B
B -->|ab| C
Before
┌────────────┐
│            │
│     A      │
│            │
└──────┬─────┘
       │
  long label
       │
       │
       ▼
┌────────────┐
│            │
│     B      │
│            │
└──────┬─────┘
       │
      ab
       │
       │
       ▼
┌────────────┐
│            │
│     C      │
│            │
└────────────┘
After
┌────────────┐
│            │
│     A      │
│            │
└──────┬─────┘
       │
       │
  long label
       │
       ▼
┌────────────┐
│            │
│     B      │
│            │
└──────┬─────┘
       │
       │
      ab
       │
       ▼
┌────────────┐
│            │
│     C      │
│            │
└────────────┘
Look for

Before: the label replaces the edge line on its row. After: the line runs unbroken and the label sits one cell beside it.

A cyclic top-down flowchart could be laid out upside down

The layout engine broke cycles with its own heuristic and could reverse the wrong edge, drawing the whole flow backwards (the last step at the top). Cycles are now broken the way mermaid.js does, depth-first from the first node.

flowchart-cycle-direction.mmd
flowchart TD
A[main] --> B[develop]
B --> C[Tests?]
C -->|fail| B
C -->|pass| A
Before
fail pass main develop Tests?
After
fail pass main develop Tests?
Look for

Before: the flow is inverted, starting from the wrong node. After: main is at the top and the flow reads downward.

A self-arrow inside alt/loop was drawn outside its block

The block’s right wall was sized from lifeline positions only, ignoring a self-arrow’s loop glyphs and label, so the loop’s corners were overwritten by the wall.

ascii-sequence-self-arrow-in-block.mmd
sequenceDiagram
participant A
participant B
loop retry
A->>A: a rather long self message
end
A->>B: done
Before
 ┌───┐     ┌───┐
 │ A │     │ B │
 └─┬─┘     └─┬─┘
   │         │
┌loop [┐     │
│  │   │     │
│  ├───│     │
│  │   │ a rather long self message
│  ◀───│     │
│  │   │     │
└──────┘     │
   │         │
   │  done   │
   │─────────▶
   │         │
 ┌─┴─┐     ┌─┴─┐
 │ A │     │ B │
 └───┘     └───┘
After
             ┌───┐              ┌───┐
             │ A │              │ B │
             └─┬─┘              └─┬─┘
               │                  │
┌─ loop ─────── [retry] ────────┐ │
│              │                │ │
│ a rather long self message    │ │
│              ├───┐            │ │
│              │   │            │ │
│              ◀───┘            │ │
│              │                │ │
└───────────────────────────────┘ │
               │                  │
               │      done        │
               │──────────────────▶
               │                  │
             ┌─┴─┐              ┌─┴─┐
             │ A │              │ B │
             └───┘              └───┘
Look for

Before: the loop wall clips the self-arrow. After: the block encloses the whole arrow and label.

ASCII fan-out edges bled their line style into each other

Edges with different styles leaving one node shared a routing corridor, and the later thick edge overwrote the dotted edge’s glyphs. Separately, two edges routed through a shared node could land on the same column and read as one line.

ascii-edge-line-style-bleed.mmd
graph LR
C(Mobile App) --> B(API Gateway)
B --> E(User Service)
B -.-> F(Cache)
B ==> G(Queue)
Before
╭────────────╮     ╭─────────────╮     ╭──────────────╮
│            │     │             │     │              │
│ Mobile App ├────►│ API Gateway ├────►│ User Service │
│            │     │             │     │              │
╰────────────╯     ╰──────┬━━━━━━━━━━━━━──────────────╯
                          ┆            ┃
                          ┆            ┃
                          ┆            ┃
                          ┆            ┃
                          ┆            ┃
                          ┆            ┃──────────────╮
                          ┆            ┃              │
                          └┄┄┄┄┄┄┄┄┄┄┄►┃    Cache     │
                                       ┃              │
                                       ┃──────────────╯
                                       ┃
                                       ┃
                                       ┃
                                       ┃
                                       ┃
                                       ┃──────────────╮
                                       ▼              │
                                       │    Queue     │
                                       │              │
                                       ╰──────────────╯
After
                                       ╭──────────────╮
                                       │              │
                          ┌───────────►│ User Service │
                          │            │              │
                          │            ╰──────────────╯
                          │
                          │
                          │
                          │
╭────────────╮     ╭──────┴──────╮     ╭──────────────╮
│            │     │             │     │              │
│ Mobile App ├────►│ API Gateway ├┄┄┄┄►│    Cache     │
│            │     │             │     │              │
╰────────────╯     ╰──────┬──────╯     ╰──────────────╯
                          ┃
                          ┃
                          ┃
                          ┃
                          ┃            ╭──────────────╮
                          ┃            │              │
                          └━━━━━━━━━━━►│    Queue     │
                                       │              │
                                       ╰──────────────╯
Look for

Before: the dotted edge’s leg is drawn with heavy glyphs. After: each edge keeps its own style.

An invalid colour in style/classDef painted the node black

A value that is not a CSS colour (fill:notacolor) was written into the SVG as an attribute, which browsers paint black (or stroke as none). Mermaid ignores the bad declaration, and so do we now.

invalid-style-colors.mmd
flowchart LR
A[Fill word] --> B[Stroke word]
style A fill:notacolor
style B fill:#ffe6cc,stroke:notacolor,stroke-width:4px
Before
Fill word Stroke word
After
Fill word Stroke word
Look for

Before: the first node is solid black and the second has no border. After: both fall back to the theme paint, and the valid fill and stroke width stay.

PR #191387336e06ASCII output

An ER relationship beside a shorter entity landed on its border

The relationship row was the left entity’s midpoint, so beside a shorter entity it fell on that box’s bottom border and the cardinality marker ran into the corner.

ascii-er-shorter-entity-row.mmd
erDiagram
CUSTOMER ||--o{ ORDER : places
CUSTOMER {
int id PK
string name
string email
string phone
string address
}
ORDER {
int id PK
}
Beforebefore terminal output of `zombie-mermaid render ascii-er-shorter-entity-row.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render ascii-er-shorter-entity-row.mmd --ascii`
Look for

Before: the ╟ marker collides with ORDER’s └ corner. After: the line meets ORDER at a ├ junction inside the box.

PR #2245436c50edASCII output

A narrower stacked ER entity sat off-center, so its edge jogged

Each stacked ER row started at the left edge, so a narrower entity was left-aligned under its neighbor while the relationship attaches at each box’s center, and the edge jogged sideways between two off-center stems.

ascii-er-stacked-center.mmd
erDiagram
INSTALL }o--|| PROJECT : scopes
INSTALL {
string scope
string installPath
}
Beforebefore terminal output of `zombie-mermaid render ascii-er-stacked-center.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render ascii-er-stacked-center.mmd --ascii`
Look for

Before: PROJECT is left-aligned and the edge jogs left halfway down. After: PROJECT is centered under INSTALL and one straight vertical edge joins them.

A C4 empty boundary frame ran past the right edge of the SVG

An empty deployment node got its default 216px width after the diagram width was measured, so its frame was drawn partly outside the SVG and clipped at the edge.

c4-empty-boundary-frame-width.mmd
C4Deployment
Deployment_Node(dc, "DC", "Ubuntu") {
Container(app, "App", "Java")
}
Deployment_Node(empty, "Empty", "x")
Before
DC [Ubuntu] Empty [x] App [Container: Java]
After
DC [Ubuntu] Empty [x] App [Container: Java]
Look for

Before: the Empty frame is cut off at the right edge. After: the SVG is wider and the frame is fully drawn. mermaid 12.2.0 rejects this input, so this shows only what our output does.

Disconnected pairs with wide edge labels crashed the layout

ELK’s post-compaction pass threw Invalid hitboxes for scanline constraint calculation when a graph had two or more components with wide inline edge labels, so nothing rendered. The layout now retries once without post-compaction.

elk-invalid-hitboxes-wide-labels.mmd
flowchart TD
A -->|a longer label here| B
C -->|a longer label here| D
E -->|a longer label here| F
G -->|a longer label here| H
Before
Threw: java.lang.IllegalStateException: Invalid hitboxes for scanline constraint calculation.
After
a longer label here a longer label here a longer label here a longer label here A B C D E F G H
Look for

Before: an error instead of a diagram. After: four labelled pairs.

PR #1989d028603dbASCII output

An ASCII fan-out arrowhead sat one column off its stem

Under an even-width box, the bundled arrowhead took its column from the box’s own centre while the stem was drawn in the middle grid column’s centre, so the head landed one column right of the line feeding it.

ascii-fanout-arrowhead-column.mmd
flowchart TD
A --> B
A -->|no| C
C --> D
D --> E
D --> F
F --> G
G --> H
G --> I
Beforebefore terminal output of `zombie-mermaid render ascii-fanout-arrowhead-column.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render ascii-fanout-arrowhead-column.mmd --ascii`
Look for

Before: the ▼ under G is shifted one column from the vertical line above it. After: head and stem line up.

Node text was unreadable on a dark classDef/style fill

A node with a custom fill but no explicit color always got the theme’s text colour, so dark text could land on a dark fill (and light text on a light one).

node-text-contrast-on-custom-fill.mmd
flowchart LR
A[Dark fill] --> B[Light fill]
classDef dark fill:#1e293b
classDef light fill:#fde68a
class A dark
class B light
Before
Dark fill Light fill
After
Dark fill Light fill
Look for

Before: both labels use the same theme text colour, so one of them is hard to read. After: each label is black or white, picked from its own fill. An explicit color still wins.

SVG coordinates carried float noise like 142.38427299999998

Layout and curve maths wrote full floating-point precision into every coordinate, bloating the SVG for no visible benefit. Numbers are now rounded to 2 decimal places and a non-finite value is written as 0, not NaN.

svg-coordinates-two-decimals.mmd
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Do thing]
B -->|No| D[Other thing]
C --> E((End))
D --> E
Before
<polyline class="edge" data-from="A" data-to="B" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="154.91878,76.9 154.91878,124.9" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
After
<polyline class="edge" data-from="A" data-to="B" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="154.92,76.9 154.92,124.9" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
Look for

The picture is the same, so this shows the first edge’s markup. Before: points carries long decimal tails. After: at most 2 decimal places.

ASCII put a node in the row of the parent that placed it first

A node took the level of whichever parent placed it first and was never revisited, so with A --> D and B --> D it landed beside B and B --> D ran sideways. Nodes are now ranked by longest path, with back edges ignored, as mermaid does.

ascii-longest-path-layering.mmd
graph TD
A -->|x| B
B -->|long label| A
A -->|long label| C
C -->|ab| A
B -->|mid| D
A -->|ab| D
D -->|x| A
Before
┌────────────────────────┐   ab ┌─────────┐
│                        │      │         │
│           A            ├◄long─label───┐ └──┬────┐
│                        │              │    │    │
└────────────┬┬──────────┘              │    │    │
            ▲││                         │    │    │
 long label │││                         │    │    │
            │└┼─────────────────┬───────┼────┤    │
            │ │ x               │       │    │    │
            │ ▼                 │       ▼   mid   │
┌───────────┴────────────┐      │     ┌───┐  │  ┌─┴─┐
│                        │      │     │   │  │  │   │
│           B            ├──────┤     │ C ├──┼─►│ D │
│                        │      │     │   │  │  │   │
└────────────────────────┘      │     └───┘  │  └───┘
                                │            │ ab
                                └────────────┘
After
┌────────────────────────┐
│                        ├──long label───┐
│           A            │               │
│                        │◄────ab──────┬─┼─────┐
└─────────────┬─┬────────┘             │ │     │
            ▲ │ │                      │ │     │
 long label │ │ │                      │ │     │
            │ │ └───────────────┐      │ │     │
            │ │                 │      │ │     x
            │ ▼ x               │      │ ▼     │
┌───────────┴────────────┐      │     ┌┴──┐    │
│                        │      │     │   │    │
│           B            │      │     │ C │    │
│                        │      │     │   │    │
└───────────┬────────────┘     ab     └───┘    │
            │                   │              │
           mid                  │              │
            │                   │              │
            │ ┌─────────────────┘              │
            ▼ ▼                                │
┌────────────────────────┐                     │
│                        │                     │
│           D            ├─────────────────────┘
│                        │
└────────────────────────┘
Look for

Before: D sits in the same row as B. After: D is below B and B --> D drops straight down.

Leaves of labelled edges drifted a layer past their siblings

With inline edge labels, ELK’s post-compaction slid a sink into the gap the label layer leaves, one layer past its siblings. mermaid keeps the sink level with them.

labeled-sink-layer.mmd
graph TD
A{Is it raining?} -->|Yes| B{Have umbrella?}
A -->|No| C([Go outside])
B -->|Yes| D([Go with umbrella])
B -->|No| E{Is it heavy?}
E -->|Yes| F([Stay inside])
E -->|No| G([Run for it])
Before
Yes No Yes No Yes No Is it raining? Have umbrella? Go outside Go with umbrella Is it heavy? Stay inside Run for it
After
Yes No Yes No Yes No Is it raining? Have umbrella? Go outside Go with umbrella Is it heavy? Stay inside Run for it
Look for

Before: Go outside hangs below Have umbrella?. After: it sits level with it, as in mermaid 12.1.0 and 12.2.0.

A class diagram’s parent was not centred over its children

Class layouts did not set ELK’s balanced node placement, which flowcharts and ER diagrams already did, so a parent sat off-centre from its children.

class-balanced-placement.mmd
classDiagram
class Animal {
+String name
+eat() void
}
class Dog {
+String breed
+bark() void
}
class Cat {
+bool isIndoor
+meow() void
}
Animal <|-- Dog
Animal <|-- Cat
Before
Animal + String name + eat(): void Dog + String breed + bark(): void Cat + bool isIndoor + meow(): void
After
Animal + String name + eat(): void Dog + String breed + bark(): void Cat + bool isIndoor + meow(): void
Look for

Before: Animal is off to one side. After: it is centred above Dog and Cat.

PR #208044dc4754ASCII output

Five or more parallel ASCII edges merged their arrowheads

Each parallel edge gets its own cell on a node’s face, but a default box face has three cells, so past that the extra edges shared one arrowhead. The face is now lengthened to one cell per edge, and cluster exits handle six siblings.

ascii-lane-faces-five-parallel.mmd
flowchart TD
A -->|first| B
A -->|second| B
A -->|third| B
A -->|fourth| B
A -->|fifth| B
Before
┌───────┐
│       ├──────────────────────────┐
│   A   ├───────────┬───────┐      │
│       ├────┐      │       │      │
└───┬───┘    │      │       │      │
    │        │      │       │      │
    │        │      │       │      │
  first   second  third  fourth  fifth
    │        │      │       │      │
    ▼        │      │       │      │
┌───────┐    │      │       │      │
│       │◄───┘      │       │      │
│   B   │◄──────────┴───────┘      │
│       │◄─────────────────────────┘
└───────┘
After
┌───────┐
│       ├──────────────────────────┐
│   A   ├───────────────────┐      │
│       ├───────────┐       │      │
│       ├────┐      │       │      │
└───┬───┘    │      │       │      │
    │        │      │       │      │
    │        │      │       │      │
  first   second  third  fourth  fifth
    │        │      │       │      │
    ▼        │      │       │      │
┌───────┐    │      │       │      │
│       │◄───┘      │       │      │
│   B   │◄──────────┘       │      │
│       │◄──────────────────┘      │
│       │◄─────────────────────────┘
└───────┘
Look for

Before: fewer arrowheads than edges. After: one arrowhead per labelled edge.

PR #209372e0eb78ASCII output

ASCII forward and back edge groups shared faces and merged heads

In TD/BT, the forward and back edge groups between one pair use the same side faces but were sized separately, so with more than three edges in all two arrowheads merged. A lone cluster exit group and eight or more cluster siblings had the same problem.

ascii-lane-faces-forward-and-back.mmd
flowchart TD
A -->|f1| B
A -->|f2| B
A -->|f3| B
A -->|f4| B
B -->|b1| A
B -->|b2| A
Before
┌────────┐
│        ├────────────────────┬───────┐
│   A    │◄───────────┐       │       │
│        ├────┐       │       │       │
└─────┬──┘    │       │       │       │
    ▲ │       │      b2       │       │
   b1 │       │       │       │       │
    │ │       │       │       │       │
    │ │       │       │       │       │
    │ │       │       │       │       │
    │f1      f2       │       │       │
    │ ▼       │       │      f3      f4
┌───┴────┐    │       │       │       │
│        │◄───┘       │       │       │
│   B    ├────────────┘       │       │
│        │◄───────────────────┴───────┘
└────────┘
After
┌────────┐
│        ├────────────────────────────┐
│   A    ├────────────────────┐       │
│        │◄───────────┐       │       │
│        ├────┐       │       │       │
└─────┬──┘    │       │       │       │
    ▲ │       │      b2       │       │
   b1 │       │       │       │       │
    │ │       │       │       │       │
    │ │       │       │       │       │
    │ │       │       │       │       │
    │f1      f2       │       │       │
    │ ▼       │       │      f3       │
┌───┴────┐    │       │       │      f4
│        │◄───┘       │       │       │
│   B    ├────────────┘       │       │
│        │◄───────────────────┘       │
│        │◄───────────────────────────┘
└────────┘
Look for

Before: two arrowheads merge on a shared face. After: each of the six edges ends in its own head.

PR #21373dea4e87ASCII output

ASCII back-edges looped round the side faces, unlike mermaid

A TD back-edge left and re-entered a node’s right face (LR: the bottom face). mermaid 12.1.0 draws it on the flow-axis faces: out of the source’s top, up a lane beside the column, into the target’s bottom, with its own port and arrowhead.

ascii-back-edges-flow-faces.mmd
flowchart TD
a --> b
b --> c
c --> a
Beforebefore terminal output of `zombie-mermaid render ascii-back-edges-flow-faces.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render ascii-back-edges-flow-faces.mmd --ascii`
Look for

Before: the back-edge hooks round the right side into a’s right face. After: it leaves c’s top and enters a’s bottom.

PR #2148b55b7faeASCII output

ASCII subgraph order and feedback edges followed mermaid 11, not 12

In the CI/CD pipeline the ASCII layout put Fix & Retry at the very bottom, below QA Approved?; mermaid 12.1.0 and 12.2.0 draw it beside Build Image. Sibling subgraphs tied by a shared parent came out in reversed order (West left of East); mermaid 12 keeps declaration order.

ascii-feedback-edge-sibling-order.mmd
graph TD
subgraph ci [CI Pipeline]
A[Push Code] --> B{Tests Pass?}
B -->|Yes| C[Build Image]
B -->|No| D[Fix & Retry]
D -.-> A
end
C --> E([Deploy Staging])
E --> F{QA Approved?}
F -->|Yes| G((Production))
F -->|No| D
Beforebefore terminal output of `zombie-mermaid render ascii-feedback-edge-sibling-order.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render ascii-feedback-edge-sibling-order.mmd --ascii`
Look for

Before: Fix & Retry hangs below QA Approved?. After: it sits beside Build Image, and the QA No edge rises into it.

An ER attribute with two keys lost one of them

An attribute written PK, FK showed only FK, and PK,FK showed no key at all. Mermaid shows PK,FK for both spellings.

er-multi-key-attributes.mmd
erDiagram
T {
string a PK, FK "c"
string b PK,FK
string d PK
}
Before
T c FK string a string b PK string d
After
T c PK,FK string a PK,FK string b PK string d
Look for

Before: row a shows only FK and row b shows no key. After: both show PK,FK.

ER style, classDef and class were ignored

An ER entity given style, classDef plus class, or the ::: shorthand rendered unstyled, although mermaid paints it.

er-style-classdef.mmd
erDiagram
CUSTOMER ||--o{ ORDER : places
classDef hot fill:#fecaca,stroke:#dc2626
class ORDER hot
style CUSTOMER fill:#dbeafe,stroke:#2563eb
Before
CUSTOMER (no attributes) ORDER (no attributes) places
After
CUSTOMER (no attributes) ORDER (no attributes) places
Look for

Before: both entities use the theme colours. After: CUSTOMER is blue and ORDER is red.

PR #21422615a669ASCII output

ER ASCII edges crossed without a junction and labels were ambiguous

In ASCII, crossing ER edges drew no junction, a label could sit between two verticals or against another edge’s corner, and an edge could run along its box’s border.

er-ascii-crossings-labels.mmd
erDiagram
ORDER ||--|{ LINE_ITEM : contains
ORDER ||..o{ SHIPMENT : ships-via
PRODUCT ||--o{ LINE_ITEM : includes
PRODUCT ||..o{ REVIEW : receives
Beforebefore terminal output of `zombie-mermaid render er-ascii-crossings-labels.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render er-ascii-crossings-labels.mmd --ascii`
Look for

Before: a plain │ where two edges cross and labels beside two lines. After: a ┼ at the crossing and each label on or beside its own edge.

PR #218612c90faaASCII output

ASCII dense duplicate edges and self-loops shared one cell

A node’s side face was sized for reciprocal and duplicate edges only. Self-loop ends and back-edges also take a cell, so the face came out short and two edge ends shared a cell: merged arrowheads and ├/┤ tick marks.

ascii-face-capacity-loops.mmd
flowchart RL
n0["n0"]
n1["n1"]
n1 --> n0
n0 --> n1
n1 --> n0
n1 --> n0
n1 --> n1
n0 --> n0
n0 --> n1
n1 --> n0
n0 --> n1
n0 --> n1
Beforebefore terminal output of `zombie-mermaid render ascii-face-capacity-loops.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render ascii-face-capacity-loops.mmd --ascii`
Look for

Before: arrowheads merge and tick marks are lost where edges share a cell. After: each edge end has its own cell.

Sequence `rect <color>` highlights were drawn as labelled frames

The colour after rect was parsed as a block label, so it was never applied and its text was printed in a frame header (rect [rgb(255,0,0)]) instead of painting the background.

sequence-rect-color.mmd
sequenceDiagram
participant A
participant B
rect rgba(200,150,255,0.5)
A->>B: hi
B-->>A: ok
end
A->>B: after
Before
rect [rgba(200,150,255,0.5)] hi ok after A B
After
hi ok after A B
Look for

Before: a grey frame with the colour text in its header tab. After: a purple background behind the two messages, with no frame.

ASCII edge labels lost the spaces between their words

An edge label drawn over a stroke treated its spaces as transparent, so the stroke showed through the gap between words: long label printed as long─label, altering the user’s text.

ascii-edge-label-spaces.mmd
graph TD
A -->|x| B
B -->|long label| A
A -->|long label| C
C -->|ab| A
B -->|mid| D
A -->|ab| D
D -->|x| A
Beforebefore terminal output of `zombie-mermaid render ascii-edge-label-spaces.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render ascii-edge-label-spaces.mmd --ascii`
Look for

Before: the label on A’s right side reads long─label, with a stroke through the gap. After: it reads long label.

Mermaid entity codes in labels printed literally

Labels written with Mermaid entity codes (#quot;, #lt;, #gt;, #35;) showed the raw code instead of the character, so a label meant to read “Say” <hi> came out as #quot;Say#quot; #lt;hi#gt;. Ported from upstream beautiful-mermaid #158 (thiccyoda).

entity-codes.mmd
flowchart LR
A["#quot;Say#quot; #lt;hi#gt;"] --> B["x #35;1"]
Before
#quot;Say#quot; #lt;hi#gt; x #35;1
After
"Say" <hi> x #1
Look for

Before: the literal text #quot;Say#quot; #lt;hi#gt; and x #35;1. After: "Say" <hi> and x #1.

PR #2510ee1a26dASCII output

Nested sequence blocks were drawn stacked, not nested, in ASCII

A block inside another block (alt containing loop, opt) was laid out as if it were a sibling: its header and walls landed on the container’s walls, so the nesting was unreadable.

sequence-nested-blocks.mmd
sequenceDiagram
participant A
participant B
alt cache hit
loop every retry
A->>B: fetch
end
else cache miss
opt warm up
B->>B: rebuild
end
end
Beforebefore terminal output of `zombie-mermaid render sequence-nested-blocks.mmd --ascii`
Afterafter terminal output of `zombie-mermaid render sequence-nested-blocks.mmd --ascii`
Look for

Before: the inner loop and opt headers collide with the alt frame. After: each sits inside the alt, one column clear of its walls.

XY chart bar series were drawn side by side, not overlapping

Two bar series on the same categories were split into narrow adjacent bars, while mermaid 12.2.0 draws each series centred on the category with later series on top.

xychart-bar-series-overlap.mmd
xychart-beta
title "2023 vs 2024 Sales"
x-axis [Q1, Q2, Q3, Q4]
bar [50, 60, 70, 80]
bar [60, 70, 80, 90]
Before
Q1 Q2 Q3 Q4 50 55 60 65 70 75 80 85 90 2023 vs 2024 Sales Bar 1 Bar 2
After
Q1 Q2 Q3 Q4 50 55 60 65 70 75 80 85 90 2023 vs 2024 Sales Bar 1 Bar 2
Look for

Before: each quarter shows two thin bars next to each other. After: the two bars share one centred, full-width slot and overlap.

PR #2509d725006cASCII output

ASCII opposing edges merged into one route at a diamond

Two edges between the same pair (B -->|no| D and D --> B) in a left-to-right chart drew a single merged route when B was a diamond or D a parallelogram, so the loop-back looked like one two-way line.

ascii-opposing-edges-diamond.mmd
graph LR
A[/Read Input/] --> B{Valid?}
B -->|yes| C[\Write Output\]
B -->|no| D[/Ask Again/]
D --> B
Before
    ╱────────────╱         ╱╲         ╲──────────────╲
   ╱            ╱        ╱    ╲        ╲              ╲
  ╱ Read Input ╱──────►< Valid? >─yes──►╲ Write Output ╲
 ╱            ╱          ╲    ╱          ╲              ╲
╱────────────╱             ╲╱             ╲──────────────╲
                            ▲
                            │
                            │
                            │
                            │             ╱──────────────╱
                            │   no       ╱              ╱
                            └────────►──╱  Ask Again   ╱
                                       ╱              ╱
                                      ╱──────────────╱
After
                                      ╲──────────────╲
                                       ╲              ╲
                                   ┌───►╲ Write Output ╲
                                   │     ╲              ╲
    ╱────────────╱         ╱╲      │      ╲──────────────╲
   ╱            ╱        ╱    ╲   yes
  ╱ Read Input ╱──────►< Valid? >──┤
 ╱            ╱          ╲    ╱ ▲  │
╱────────────╱             ╲╱   │ no      ╱──────────────╱
                                │  └────►╱              ╱
                                │       ╱  Ask Again   ╱
                                └──────╱              ╱
                                      ╱──────────────╱
Look for

Before: one stroke leaves the diamond’s bottom vertex. After: no leaves the right corner and drops into the parallelogram; the return runs under it into the diamond’s lower-right face, without crossing.