How a run works¶
A diagram linter has two hard problems. Text patterns disagree with the renderer about what a diagram contains, and an automatic fix can quietly break a document. This page explains how a run avoids both.
One setup, then one model per file¶
A run sets up once, then reads each file into a document model that every rule shares.
flowchart LR
R["Find project<br/>root"] --> C["Load<br/>config"] --> P["Load<br/>plugins"] --> G["Build<br/>registry"]
G --> F["Collect<br/>files"] --> M["Build document<br/>model"]
M --> Q{"--fix?"}
Q -->|yes| X["Fix to fixpoint,<br/>write file"]
X --> K
Q -->|no| K["Check with<br/>all rules"]
K --> O["Report findings,<br/>exit 0 or 1"]
classDef setupTint fill:#1d4ed836,stroke:#3b82f6,stroke-width:2px
classDef docTint fill:#15803d36,stroke:#22c55e,stroke-width:2px
classDef fixTint fill:#6d28d936,stroke:#8b5cf6,stroke-width:2px
classDef reportTint fill:#b4530936,stroke:#d97706,stroke-width:2px
classDef gateTint fill:#52525b36,stroke:#71717a,stroke-width:1px
class R,C,P,G setupTint
class F,M,K docTint
class X fixTint
class O reportTint
class Q gateTint
Detailed: setup, the fixpoint loop and the check
Every accepted edit restarts the pass on a fresh model, because offsets only hold for the source they came from.
flowchart TB
subgraph Setup["Setup, once per run"]
R["findProjectRoot: nearest package.json,<br/>config or .mermaidjs-lint"]
C["Load config, or none"]
P["loadPlugins in the run's mode"]
G["buildRegistry with config applied"]
end
F["Collect files, each processed concurrently"]
subgraph Fix["Fix, only with --fix"]
B["Build model from current source"]
FX["Next fixer in fixOrder<br/>proposes edits"]
W["Drop malformed, no-op<br/>or already refused edits"]
SP["Splice the batch, re-parse"]
V{"Verify each edit's<br/>expectation"}
RF["Refuse: retry one edit at<br/>a time, remember refusal"]
DN["No fixer progressed:<br/>write the file"]
end
subgraph Check["Check"]
CM["checkModel: every rule's<br/>check started at once"]
SO["Sort by line, then rule id"]
end
O["Print findings or JSON"]
E{"Any finding?"}
R --> C --> P --> G --> F
F -->|"--fix"| B
F -->|"no --fix"| CM
B --> FX --> W --> SP --> V
V -->|"accept: restart pass"| B
V -->|refuse| RF --> FX
FX -->|"no edits left"| DN --> CM
CM --> SO --> O --> E
E -->|yes| X1["Exit 1"]
E -->|no| X0["Exit 0"]
classDef setupTint fill:#1d4ed836,stroke:#3b82f6,stroke-width:2px
classDef docTint fill:#15803d36,stroke:#22c55e,stroke-width:2px
classDef fixTint fill:#6d28d936,stroke:#8b5cf6,stroke-width:2px
classDef refuseTint fill:#dc262636,stroke:#ef4444,stroke-width:2px
classDef reportTint fill:#b4530936,stroke:#d97706,stroke-width:2px
classDef gateTint fill:#52525b36,stroke:#71717a,stroke-width:1px
class R,C,P,G setupTint
class F,CM,SO docTint
class B,FX,W,SP,DN fixTint
class RF refuseTint
class O,X1,X0 reportTint
class V,E gateTint
Setup happens before any file is read.
The run first finds the project root.
That is the nearest ancestor of the working directory holding a package.json, a config file or a .mermaidjs-lint folder.
It reads the config there, loads the plugins, and builds one registry of active rules.
A bad config or a broken plugin therefore stops the run before it touches a document.
Setup turns the flags and the config into one registry, and a category flag has the last word over the config.
sequenceDiagram
participant CLI as Command
participant S as setUp
participant P as Plugin loader
participant G as buildRegistry
rect rgba(29, 78, 216, 0.21)
CLI->>S: Plugin flags, category flags, --config
S->>S: Find the project root, read the config
S->>P: Load plugins in the plugin mode
P-->>S: Plugin rules and categories
S->>G: Built-ins, plugin rules, config, category flags
G->>G: Config turns rules off, then category flags decide
G-->>S: Registry of active rules
S-->>CLI: Project root and registry
end
Detailed: config, plugin mode and registry
The config turns rules off and sets their options, then --category and --disable-category switch whole categories for this run.
sequenceDiagram
participant CLI as Command
participant S as setUp
participant C as Config file
participant P as loadPlugins
participant G as buildRegistry
CLI->>CLI: Parse flags, refuse a contradiction with error[flag]
CLI->>S: cwd, --config, plugin flags, category flags
S->>S: findProjectRoot: nearest package.json, config or .mermaidjs-lint
rect rgba(29, 78, 216, 0.21)
alt --config given
S->>C: Resolve from cwd, or error[config-not-found]
else no --config
S->>C: Find at the project root, or error[config-ambiguous] for two
end
opt --no-plugins with a .mjs config
S-->>CLI: error[config-code], nothing is imported
end
C-->>S: Plugins list, pluginLoading and rule settings, or no config
S->>S: Plugin mode: off, then collect, then lenient, else strict
S->>P: Project root, config, plugin mode
P-->>S: Plugin rules, categories, sources, skipped, failures
end
rect rgba(180, 83, 9, 0.21)
S->>G: Built-ins plus plugin rules, config, category flags
G->>G: Unknown category, or one named by both flags, is error[flag]
loop Each rule setting in the config
alt Rule id not loaded
G->>G: Plugin id outside strict mode is set aside, else error[unknown-rule]
else Rule id loaded
G->>G: Type-check options or error[rule-option], keep severity off
end
end
G->>G: --category turns its rules on, --disable-category turns them off
G->>G: No rule left to run is error[flag]
G-->>S: Active rules, fix order, options, sources, set-aside settings
end
S-->>CLI: Project root, registry, skipped plugins, failures
CLI->>CLI: Print the loaded or skipped plugins line on standard error
Structure comes from mermaid's own parsers¶
Each file is parsed to a markdown tree once.
The mermaid fences in that tree become the diagrams every rule reads.
A check and a fix therefore never disagree about which text is a diagram.
A .mmd or .mermaid file is one diagram with no markdown around it.
A rule that needs structure asks the diagram for its stats. For the newer diagram types, the parse engine reads them from mermaid's Langium grammars. For the rest, it reads them from mermaid's own core, under a DOM shim. A regular expression would miscount subgraphs, ports and multi-line labels, and the counts would differ from what a reader sees. The parse happens once per diagram, on first use, and every rule shares the result.
A diagram whose parser reports an error is reported as parse-diagram-unparseable, on one line or many.
The finding carries the parser's reason, and its line numbers count from the diagram's first line.
Having no nodes is not an error on its own, so a valid pie with only a title line, or info, passes.
A zenuml diagram is always reported, because it is drawn by a mermaid add-on this package does not include.
A diagram that opens with the flowchart-elk keyword is a flowchart drawn with the ELK layout, so it is read and measured as a flowchart.
The diagram type comes from mermaid's own detection, which looks past frontmatter, directives and comments, and it picks which parser runs.
A diagram whose type mermaid does not detect is reported as parse-diagram-unparseable.
Whether a fence is closed, and the line that would close it, come from micromark's reading of the markdown.
Style directives are read from the parser's result: the classes, node styles and link styles mermaid's DB holds, with the classes each node carries. Each statement is placed by the lexer token that opens it, so a fixer can rewrite a value in place. A value the parser cannot place is never rewritten. Text inside a label is never read as a directive, and a style joined to other statements by a semicolon is read like any other.
The document model reference lists every field.
Checking runs every rule at once¶
No rule reads another rule's result, so a check starts every rule together and waits for all of them. A rule may return its findings directly or as a promise. The findings are then sorted by line and rule id, so output is stable from run to run.
Every file is read, modelled and checked at the same time, and any finding left makes the run exit 1.
sequenceDiagram
participant CLI as Command
participant F as File walk
participant M as Document model
participant R as Active rules
CLI->>F: Paths from the command line
F-->>CLI: Markdown and raw diagram files
rect rgba(21, 128, 61, 0.21)
loop Every file, all at once
CLI->>M: Read the file, build the model
M-->>CLI: Markdown tree and its diagrams
CLI->>R: Start every rule's check together
R-->>CLI: Findings, sorted by line, then rule id
end
end
rect rgba(180, 83, 9, 0.21)
CLI->>CLI: Print findings, exit 1 if any remain, else 0
end
Detailed: file walk, shared stats and the report
A rule asks the parse engine for stats on first use, and every other rule reads the same result.
sequenceDiagram
participant CLI as Command
participant F as collectFiles
participant M as buildDocModel
participant R as Each active rule
participant E as Parse engine
CLI->>F: Every path, resolved from cwd
F->>F: Walk folders for .md, .markdown, .mmd, .mermaid
F-->>CLI: Files, unreadable paths, skipped symbolic links
CLI->>CLI: Unreadable path or no files is error[flag], exit 2
CLI->>CLI: Name each skipped link on standard error
rect rgba(21, 128, 61, 0.21)
loop Every file, all at once
CLI->>CLI: Read the source, and fix it first under --fix
CLI->>M: Source, and markdown or diagram by extension
M-->>CLI: Markdown tree once, mermaid fences as diagrams
CLI->>R: Model, file and options, all rules started together
R->>E: Stats, on first use only
E-->>R: Counts from mermaid's own parsers, shared by every rule
R-->>CLI: Findings, directly or as a promise
CLI->>CLI: Sort by line, then rule id
end
end
rect rgba(180, 83, 9, 0.21)
alt --json
CLI->>CLI: One JSON object with findings, files and plugins
else text
CLI->>CLI: One line per finding, then the count
end
CLI->>CLI: Exit 1 if any finding remains, else 0
end
Fixing is a loop with a check¶
--fix does not trust a fixer.
A fixer proposes edits: a span of the source, its replacement, and an expectation of what must stay the same.
| Expectation | What must hold after the edit |
|---|---|
same-diagrams |
The markdown is unchanged outside the mermaid fences, once whitespace in text is collapsed, and the document holds the same number of diagrams. Each keeps its keyword, whether it parses, its type, counts, rows, deepest nesting and tree depth, node ids and subgraph names. |
same-tree |
The markdown tree is identical, except for whitespace in prose text. Mermaid fence bodies must match exactly, and no diagram that parsed may stop parsing. A raw diagram file has an empty tree, so there the diagram must also pass the same-diagrams check. |
The engine splices the edits, parses the whole document again, and keeps the change only when every expectation holds.
Neither expectation lets an edit turn a diagram that parses into one that does not. same-diagrams is a structural check, and it does not show that nothing else changed.
It does not compare edges, labels, node text or styles, so a fixer that declares it must keep those intact itself.
Edits lists exactly what each expectation compares.
A rule's edits are first tried as one batch. When the batch fails, each edit is tried alone, so one bad edit never blocks the others. An edit that fails is refused, remembered, and never proposed again for the same text.
After any accepted change the engine starts a new pass on the new source, because every offset a fixer computed belongs to the old one.
The loop ends when no fixer makes progress.
The file is then written, and checked like any other.
A fixer that never stops proposing edits stops the run after 1000 passes with error[fix-not-converging], and that file is not written.
Each pass keeps an edit only when the verifier accepts it, and a refused edit leaves the text alone.
sequenceDiagram
participant E as Fix engine
participant X as Fixer
participant V as Verifier
rect rgba(109, 40, 217, 0.21)
loop One pass, up to 1000
E->>E: Build the model from the current source
E->>X: Propose edits, in fix order
X-->>E: Edits, each with an expectation
E->>E: Splice the edits, parse the document again
E->>V: Model before and model after
alt Every expectation holds
V-->>E: Accept, restart the pass on the new source
else An expectation fails
V-->>E: Refuse, the text stays and so does the finding
end
end
end
E->>E: No fixer made progress, write the file
Detailed: batches, both expectations and what is not compared
The verifier compares only what each expectation lists, so edge endpoints, edge labels and node text are the fixer's own responsibility.
sequenceDiagram
participant CLI as Command
participant E as fixSourceReport
participant X as Next fixer in fix order
participant V as verify
CLI->>E: Source, file, flags, registry
rect rgba(109, 40, 217, 0.21)
loop One pass, up to 1000
E->>E: buildDocModel from the current source
E->>X: Model, file and the rule's options
X-->>E: Proposed edits: span, replacement, expectation
E->>E: Refuse malformed offsets once, drop no-ops and known refusals
alt Several edits that do not overlap
E->>E: Splice the whole batch, build the model again
else One edit, or the batch failed
E->>E: Splice one edit at a time, build the model again
end
E->>V: Each edit, model before, model after
alt same-tree
V->>V: Tree identical but for prose whitespace, fence bodies exact
V->>V: No diagram that parsed stops parsing
else same-diagrams, or same-tree on a raw diagram file
V->>V: Tree outside fences unchanged but for prose whitespace
V->>V: Same number of diagrams, each with the same keyword
V->>V: Same parse result, type, counts and depths
V->>V: Same node ids and subgraph names
end
Note over V: Not compared: edge endpoints, edge labels, node text, styles
alt Accept
V-->>E: Keep the new source, restart the pass
else Refuse
V-->>E: Remember the refusal for this text, try the next edit
end
end
end
rect rgba(220, 38, 38, 0.21)
opt Still editing after 1000 passes
E-->>CLI: error[fix-not-converging], the file is not written
end
end
E-->>CLI: No fixer progressed: fixed source, applied and refused edits
CLI->>CLI: Write the file if it changed, then check it
A refused edit leaves the finding in place
When a fixer is unsure, or its edit fails the check, the text is left alone and the finding is still reported. That is deliberate: a fix that might change what a diagram says needs a human.
What a run does not do¶
- It never renders a diagram, so layout problems a renderer would show are out of reach.
- It never splits or restructures a diagram, because that changes what the diagram says.
- It never picks a brand colour. The only colour fix swaps a declared label colour for black or white.