@jpeakai/mermaidjs-lint 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +82 -53
  2. package/dist/bin.js +202 -44
  3. package/dist/index.js +206 -45
  4. package/dist/testing.js +86 -23
  5. package/examples/architecture_beta_iconify_logos.mmd +27 -0
  6. package/examples/flowchart_fontawesome_icons.mmd +31 -0
  7. package/examples/layout_cosebilkent_mindmap.mmd +19 -0
  8. package/examples/layout_dagre_classic.mmd +22 -0
  9. package/examples/layout_dagre_handdrawn.mmd +23 -0
  10. package/examples/layout_dagre_neo.mmd +26 -0
  11. package/examples/layout_elk_default.mmd +22 -0
  12. package/examples/layout_elk_tuned.mmd +25 -0
  13. package/examples/layout_tidytree_mindmap.mmd +19 -0
  14. package/package.json +2 -2
  15. package/src/check.ts +2 -3
  16. package/src/cli.ts +45 -23
  17. package/src/config.ts +3 -2
  18. package/src/errors.ts +18 -3
  19. package/src/files.ts +3 -1
  20. package/src/fix/engine.ts +4 -4
  21. package/src/fix/verify.ts +2 -2
  22. package/src/index.ts +1 -1
  23. package/src/model.ts +10 -0
  24. package/src/parse/color.ts +2 -2
  25. package/src/parse/contrast.ts +59 -5
  26. package/src/parse/metrics.ts +16 -8
  27. package/src/parse/stats.ts +45 -19
  28. package/src/plugins/contract.ts +1 -1
  29. package/src/plugins/discover.ts +5 -5
  30. package/src/plugins/load.ts +5 -3
  31. package/src/project.ts +11 -4
  32. package/src/rules/complexity-node-count-hard-limit.ts +1 -1
  33. package/src/rules/complexity-visual-score-critical.ts +2 -2
  34. package/src/rules/contrast-text-below-aa.ts +6 -2
  35. package/src/rules/index.ts +2 -1
  36. package/src/rules/parse-diagram-unparseable.ts +29 -14
  37. package/src/rules/registry.ts +7 -4
  38. package/src/rules/shared.ts +1 -1
  39. package/src/rules/types.ts +2 -2
  40. package/src/testing.ts +4 -3
package/README.md CHANGED
@@ -3,7 +3,10 @@
3
3
  Diagrams in documentation should be readable.
4
4
  Deterministic checks and verified fixes for Mermaid diagrams in markdown.
5
5
 
6
- **[Rules](RULES.md)** | **[GitHub](https://github.com/jpeakai/jpai-mermaidjs-lint)** | **[npm](https://www.npmjs.com/package/@jpeakai/mermaidjs-lint)**
6
+ **[Docs](https://jpeakai.github.io/jpai-mermaidjs-lint/)** | **[Rules](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/)** | **[Changelog](CHANGELOG.md)** | **[GitHub](https://github.com/jpeakai/jpai-mermaidjs-lint)** | **[npm](https://www.npmjs.com/package/@jpeakai/mermaidjs-lint)**
7
+
8
+ This README and the docs site describe the `main` branch.
9
+ A feature on `main` but not yet on npm is listed under `Unreleased` in the [changelog](CHANGELOG.md).
7
10
 
8
11
  ## Usage
9
12
 
@@ -12,73 +15,72 @@ bunx @jpeakai/mermaidjs-lint README.md docs/ # report findings, exit 1 if an
12
15
  npx -y @jpeakai/mermaidjs-lint --fix docs/ # apply every fix proven safe, report the rest
13
16
  ```
14
17
 
15
- A path is a `.mmd` file, a markdown file whose mermaid fences are checked, or a directory.
16
- A directory is walked for both, and it skips `node_modules`, `dist`, `tmp` and dot-directories.
18
+ Pass your own paths: every path you name must exist, or the run stops with a usage error and exit 2.
19
+ A `.mmd` or `.mermaid` file is read as one diagram, and any other file you name is read as markdown.
20
+ A directory is walked for `.mmd`, `.mermaid`, `.md` and `.markdown` files.
21
+ The walk skips dot-folders and any folder named `node_modules`, `vendor`, `dist`, `build`, `tmp` or `__pycache__`, at any depth.
22
+ Installed as a dev dependency, the command is `mermaidjs-lint`, and `--help` lists every flag.
17
23
 
18
- Installed as a dev dependency, the command is `mermaidjs-lint`, and `--json` gives machine-readable output with a remediation for each finding.
24
+ [Get started](https://jpeakai.github.io/jpai-mermaidjs-lint/get-started/) walks through a first run, a fix, and adding the check to a project.
19
25
 
20
- `--preset NAME` sets the density for every complexity rule, as `low`, `medium` or `high`.
21
- `--profile NAME` sets how contrast is read, as `auto`, `github` or `mkdocs-material`.
22
- `--config FILE` reads a config other than the one at the project root.
23
- `--no-plugins` runs the built-in rules only, and `--lenient-plugins` skips a plugin that fails to load and says so.
24
- `--validate-plugins` loads and checks every plugin, reports every failure, and runs no check or fix.
25
- `--list-rules` prints every active rule by category, including plugin rules.
26
- Exit codes are 0 for clean, 1 for findings, and 2 for a usage error.
26
+ ## The four tenets
27
27
 
28
- ## The rules
28
+ Three tenets are guarantees about every diagram the tool passes, and the fourth is a commitment about the tool.
29
29
 
30
- Eleven rules in three categories.
31
- [RULES.md](RULES.md) shows a failing example for each one, and the fix where there is one.
32
- There is no warning level, so every finding fails the run.
33
- A rule you do not want is switched off in the config.
30
+ - **Readable in light and dark.** Declared fills, label colours and borders are scored as the host draws them, on nodes, erDiagram rows and edge labels, in both themes.
31
+ - **Renders.** Every diagram parses with the mermaid parsers this package ships, and every mermaid fence is closed.
32
+ - **Simple enough to follow.** Node count and visual complexity are scored against a density preset, so an oversized diagram is flagged to be split.
33
+ - **Extensible.** A plugin adds rules of your own, which run through the same config, fix engine and verification as the built-ins.
34
34
 
35
- | Category | Rules | What it guards |
36
- |---|---|---|
37
- | Parse | parse-diagram-unparseable, parse-fence-unterminated | Whether mermaid can read the diagram, and whether its fence is whole |
38
- | Complexity | complexity-node-count-hard-limit, complexity-node-count-cognitive-limit, complexity-node-count-acceptable-exceeded, complexity-visual-score-critical, complexity-visual-score-acceptable-exceeded, complexity-subgraph-nesting-too-deep | How much a reader must hold in mind at once |
39
- | Contrast | contrast-text-below-aa, contrast-border-below-ui-minimum, contrast-directive-unauditable | Whether labels and borders can be read against their fills |
35
+ | Tenet | Delivered by |
36
+ |---|---|
37
+ | Readable in light and dark | The contrast rules |
38
+ | Renders | The parse rules |
39
+ | Simple enough to follow | The complexity rules |
40
+ | Extensible | Local rules and plugin packages |
40
41
 
41
- Structure comes from mermaid's own parsers, never from patterns over the text.
42
- Every fix is verified by parsing the document again.
43
- A fix that would change the markdown or the structure of a diagram is refused, so the finding stays for a human.
44
- The reasoning behind each choice is recorded in [`adrs/`](adrs/index.md).
42
+ The [concepts page](https://jpeakai.github.io/jpai-mermaidjs-lint/concepts/) states the limit of each tenet.
43
+ `mermaidjs-lint --list-rules` prints every rule.
44
+ The [rules page](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/) draws each one before and after `--fix`, and [RULES.md](RULES.md) holds the same examples as plain markdown.
45
45
 
46
- ## Density presets
46
+ Every finding fails the run, and a rule you do not want is switched off in the config.
47
+ Node, edge and subgraph counts come from mermaid's own parsers.
48
+ Style directives are read line by line, so a style joined to other statements by a semicolon is not seen.
49
+ Every fix is verified by parsing the document again, and a fix that fails the check is refused, so the finding stays for a human.
50
+ The check holds the markdown outside the diagrams unchanged, and each diagram's type, node ids, subgraph names and counts.
51
+ It does not compare edge endpoints, edge labels or node text, so review the diff a fix writes.
47
52
 
48
- Complexity rules measure the nodes and a visual complexity score, and compare them with a preset.
53
+ ## Requirements and compatibility
49
54
 
50
- | Preset | Acceptable nodes | Cognitive limit | Hard limit |
51
- |---|---|---|---|
52
- | low-density | 12 | 20 | 35 |
53
- | medium-density | 20 | 35 | 60 |
54
- | high-density | 35 | 50 | 100 |
55
+ - **Runtime:** Bun, or Node `>=20` as the `engines` field of `package.json` states.
56
+ - **Mermaid:** the parse rules use the `mermaid` package, at `^11.14.0`, and `@mermaid-js/parser`, both installed with this package.
57
+ - **Your host's mermaid may differ.** GitHub and other hosts draw diagrams with their own mermaid version. A diagram type newer than the host's may parse here and still fail to draw there.
55
58
 
56
- The default is `high-density`, which follows the research that 50 nodes is where a diagram becomes hard to read.
57
- Each threshold is also a rule option, such as `nodeAcceptable` or `vcsCritical`.
59
+ `npm ls mermaid` in your project prints the mermaid version a run uses.
60
+ [Reference](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/#requirements-and-compatibility) lists which parser reads each diagram type.
58
61
 
59
- ## Plugins and local rules
62
+ ## Configure and extend
60
63
 
61
- A project adds its own rules without forking this package.
62
- Both sources load on their own, with no config, and every run prints a line saying what it loaded.
64
+ - [Configure the rules](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/configure/): switch a rule off, change the complexity preset, set options.
65
+ - [Lint a MkDocs Material site](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/mkdocs/): score diagrams the way the theme renders them.
66
+ - [Write a local rule](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/local-rule/) or [publish a plugin package](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/plugin-package/): add rules of your own, which load with no config.
67
+ - [Troubleshooting](https://jpeakai.github.io/jpai-mermaidjs-lint/guides/troubleshooting/): a README that fails after adding `mkdocs.yml`, skipping a diagram, and other common questions.
63
68
 
64
- - **Local rules.** A `.js`, `.mjs`, `.ts` or `.mts` file in `.mermaidjs-lint/rules/` exports one rule, and its id is `local/` followed by the file name.
65
- - **Plugin packages.** A dependency named `mermaidjs-lint-plugin-name` or `@scope/mermaidjs-lint-plugin-name` loads under the namespace its own `meta.namespace` declares.
66
- - **New categories.** A plugin declares a category of its own, and its rules use it.
67
- - **Same treatment.** A plugin rule is switched off, given options and listed exactly like a built-in, and its fixer is verified like a built-in fixer.
69
+ ## Use it from an AI agent
68
70
 
69
- A config at the project root switches rules off and sets options.
71
+ [`skills/mermaidjs-lint/`](skills/mermaidjs-lint/SKILL.md) is an agent skill that runs the linter, applies the safe fixes, then fixes what remains by judgement.
72
+ It is not in the npm package, so copy it from this repository.
73
+ For Claude Code, run these in your project:
70
74
 
71
- ```json
72
- {
73
- "rules": {
74
- "complexity-subgraph-nesting-too-deep": "off",
75
- "complexity-node-count-acceptable-exceeded": ["error", { "preset": "medium" }],
76
- "contrast-text-below-aa": ["error", { "profile": "mkdocs-material" }]
77
- }
78
- }
75
+ ```sh
76
+ git clone --depth 1 https://github.com/jpeakai/jpai-mermaidjs-lint.git mermaidjs-lint-src
77
+ mkdir -p .claude/skills
78
+ cp -R mermaidjs-lint-src/skills/mermaidjs-lint .claude/skills/
79
79
  ```
80
80
 
81
- [docs/plugins.md](docs/plugins.md) is the guide to writing a rule.
81
+ Copy it to `~/.claude/skills/` instead to use it in every project, then delete the clone.
82
+ Another agent can follow `SKILL.md` as plain instructions.
83
+ [Agent evals](https://jpeakai.github.io/jpai-mermaidjs-lint/gallery/agent-evals/) shows how well agents fix the fixtures with it.
82
84
 
83
85
  ## Library use
84
86
 
@@ -89,13 +91,40 @@ const findings = await checkMarkdown(src, "README.md");
89
91
  const fixed = await fixSource(src, "README.md");
90
92
  ```
91
93
 
92
- A plugin author tests a rule with `@jpeakai/mermaidjs-lint/testing`, which runs the same loader checks and verified fixer as a real run.
94
+ These calls run the built-in rules only, and read no config file and no plugin.
95
+ The [Library API](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/library-api/) page lists every export, and shows how to run with the config and plugins as the command does.
96
+
97
+ ## Versioning
98
+
99
+ The package follows semantic versioning, and while it is at `0.x` a minor release may break the public surface.
100
+ [Versioning](https://jpeakai.github.io/jpai-mermaidjs-lint/reference/#versioning) lists what that surface is.
101
+ [CHANGELOG.md](CHANGELOG.md) lists what changed in each release, and what is on `main` but not yet released.
102
+
103
+ ## Security
104
+
105
+ A plugin is code that runs on your machine, and plugins load on their own once a project depends on them.
106
+ [SECURITY.md](SECURITY.md) covers the supported versions, the trust model and how to report a vulnerability privately.
107
+
108
+ ## Support
109
+
110
+ Report a bug or ask a question in [GitHub issues](https://github.com/jpeakai/jpai-mermaidjs-lint/issues).
111
+ A bug report needs:
112
+
113
+ - The version, from `mermaidjs-lint --version`, or from `npm ls @jpeakai/mermaidjs-lint` on 0.1.1 and earlier.
114
+ - The command you ran.
115
+ - The diagram, or the smallest file that shows the problem.
116
+ - The output of the same command with `--json`.
93
117
 
94
118
  ## Origin
95
119
 
96
- The checks port the logic of the `mermaidjs-diagrams` skill scripts, which measured complexity and contrast for Mermaid diagrams.
120
+ The checks port an earlier set of diagram-checking scripts, which measured complexity and contrast for Mermaid diagrams.
97
121
  The package shape, the plugin mechanism and the fix verification follow [jpai-prose-gates](https://github.com/jpeakai/jpai-prose-gates).
122
+ The reasoning behind each choice is recorded in [`adrs/`](adrs/index.md).
98
123
 
99
124
  ## Contributing
100
125
 
101
126
  [CONTRIBUTING.md](CONTRIBUTING.md) covers setup and how a change is accepted.
127
+
128
+ ## Licence
129
+
130
+ MIT, as stated in [LICENSE](LICENSE).
package/dist/bin.js CHANGED
@@ -6,6 +6,77 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
6
6
  import { readFile as readFile3, writeFile } from "node:fs/promises";
7
7
  import { relative } from "node:path";
8
8
  import { parseArgs } from "node:util";
9
+ // package.json
10
+ var package_default = {
11
+ name: "@jpeakai/mermaidjs-lint",
12
+ version: "0.2.0",
13
+ type: "module",
14
+ description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
15
+ license: "MIT",
16
+ keywords: [
17
+ "mermaid",
18
+ "mermaidjs",
19
+ "lint",
20
+ "diagram",
21
+ "markdown",
22
+ "documentation"
23
+ ],
24
+ repository: {
25
+ type: "git",
26
+ url: "git+https://github.com/jpeakai/jpai-mermaidjs-lint.git"
27
+ },
28
+ homepage: "https://jpeakai.github.io/jpai-mermaidjs-lint/",
29
+ bugs: "https://github.com/jpeakai/jpai-mermaidjs-lint/issues",
30
+ packageManager: "bun@1.3.12",
31
+ publishConfig: {
32
+ access: "public"
33
+ },
34
+ scripts: {
35
+ prepublishOnly: "make check",
36
+ prepack: "make build"
37
+ },
38
+ bin: {
39
+ "mermaidjs-lint": "dist/bin.js"
40
+ },
41
+ exports: {
42
+ ".": {
43
+ bun: "./src/index.ts",
44
+ types: "./src/index.ts",
45
+ default: "./dist/index.js"
46
+ },
47
+ "./testing": {
48
+ bun: "./src/testing.ts",
49
+ types: "./src/testing.ts",
50
+ default: "./dist/testing.js"
51
+ }
52
+ },
53
+ engines: {
54
+ node: ">=20"
55
+ },
56
+ files: [
57
+ "src",
58
+ "dist",
59
+ "examples"
60
+ ],
61
+ dependencies: {
62
+ "@mermaid-js/parser": "^1.1.0",
63
+ "colorjs.io": "^0.6.1",
64
+ "happy-dom": "^20.9.0",
65
+ "mdast-util-from-markdown": "^2.0.3",
66
+ "mdast-util-frontmatter": "^2.0.1",
67
+ "mdast-util-gfm": "^3.1.0",
68
+ mermaid: "^11.14.0",
69
+ "micromark-extension-frontmatter": "^2.0.0",
70
+ "micromark-extension-gfm": "^3.0.0"
71
+ },
72
+ devDependencies: {
73
+ "@biomejs/biome": "^2",
74
+ "@jpeakai/prose-gates": "^0.2.0",
75
+ "@types/bun": "latest",
76
+ "@types/mdast": "^4",
77
+ typescript: "^5"
78
+ }
79
+ };
9
80
 
10
81
  // src/model.ts
11
82
  import { fromMarkdown } from "mdast-util-from-markdown";
@@ -189,17 +260,20 @@ function detectKeyword(content) {
189
260
  }
190
261
  async function extractStats(content) {
191
262
  const keyword = detectKeyword(content) ?? "unknown";
192
- if (keyword in LANGIUM_TYPES) {
263
+ const langium = keyword in LANGIUM_TYPES;
264
+ let langiumError = null;
265
+ if (langium) {
193
266
  try {
194
267
  return await extractLangiumStats(content, keyword);
195
- } catch {}
268
+ } catch (err) {
269
+ langiumError = parserReason(err);
270
+ }
196
271
  }
197
- let error = null;
272
+ let coreError;
198
273
  try {
199
274
  return await extractCoreStats(content, keyword);
200
275
  } catch (err) {
201
- error = (err instanceof Error ? err.message : String(err)).split(`
202
- `)[0] ?? null;
276
+ coreError = parserReason(err);
203
277
  }
204
278
  return {
205
279
  nodes: 0,
@@ -208,11 +282,20 @@ async function extractStats(content) {
208
282
  max_subgraph_depth: 0,
209
283
  node_ids: [],
210
284
  subgraph_names: [],
211
- parser_used: "mermaid-core",
285
+ parser_used: langium ? "langium" : "mermaid-core",
212
286
  diagram_type: keyword,
213
- error
287
+ error: langiumError ?? coreError
214
288
  };
215
289
  }
290
+ var POINTER = /^-*\^$/;
291
+ var parserReason = (err) => {
292
+ const message = err instanceof Error ? err.message : String(err);
293
+ const lines = message.split(`
294
+ `);
295
+ if (message.startsWith("No diagram type detected"))
296
+ return (lines[0] ?? "").trim();
297
+ return lines.filter((line, i) => !POINTER.test(line.trim()) && !POINTER.test((lines[i + 1] ?? "").trim())).join(" ").replace(/\s+/g, " ").trim();
298
+ };
216
299
  async function langiumParseAny(type, content) {
217
300
  switch (type) {
218
301
  case "architecture":
@@ -698,6 +781,11 @@ var collectMermaid = (node, out) => {
698
781
  for (const child of node.children ?? [])
699
782
  collectMermaid(child, out);
700
783
  };
784
+ var DIAGRAM_EXTS = [".mmd", ".mermaid"];
785
+ var documentKind = (file) => {
786
+ const lower = file.toLowerCase();
787
+ return DIAGRAM_EXTS.some((ext) => lower.endsWith(ext)) ? "diagram" : "markdown";
788
+ };
701
789
  var buildDocModel = (src, kind = "markdown") => {
702
790
  const lineStarts = lineStartsOf(src);
703
791
  if (kind === "diagram") {
@@ -775,6 +863,14 @@ class ConfigNotFoundError extends UsageError {
775
863
  }
776
864
  }
777
865
 
866
+ class ConfigCodeError extends UsageError {
867
+ file;
868
+ constructor(file) {
869
+ super(`${file}: a .mjs config is code, and --no-plugins runs no project code; pass --config with a .json config, or drop --no-plugins`, "config-code");
870
+ this.file = file;
871
+ }
872
+ }
873
+
778
874
  class AmbiguousConfigError extends UsageError {
779
875
  files;
780
876
  constructor(root, files) {
@@ -887,6 +983,14 @@ var MKDOCS_MATERIAL = {
887
983
  };
888
984
  var GITHUB = { light: { bg: "#ffffff" }, dark: { bg: "#0d1117" } };
889
985
  var THEMES = ["light", "dark"];
986
+ var MERMAID_EDGE_LABEL = {
987
+ light: { box: "rgba(232,232,232,0.8)", backing: "rgba(232,232,232,0.5)" },
988
+ dark: { box: "#585858", backing: "rgba(88,88,88,0.5)" }
989
+ };
990
+ var edgeLabelBackground = (theme) => {
991
+ const { box, backing } = MERMAID_EDGE_LABEL[theme];
992
+ return compositeOver(box, compositeOver(box, compositeOver(backing, GITHUB[theme].bg)));
993
+ };
890
994
  var MERMAID_ER = {
891
995
  light: { odd: "#ffffff", even: "#f1f1ff", label: "#333333" },
892
996
  dark: { odd: "#2c2d2d", even: "#060606", label: "#cccccc" }
@@ -907,10 +1011,27 @@ var scorePair = (kind, d, fg, bg) => {
907
1011
  };
908
1012
  };
909
1013
  var message = (err) => err instanceof Error ? err.message : String(err);
1014
+ var scoreEdgeLabel = (d, pairs, skipped) => {
1015
+ const { color } = d.properties;
1016
+ if (!color)
1017
+ return;
1018
+ try {
1019
+ pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t))));
1020
+ } catch (err) {
1021
+ skipped.push({
1022
+ selector: d.selector,
1023
+ line: d.line,
1024
+ reason: `edge label text unparseable: ${message(err)}`,
1025
+ blocking: true
1026
+ });
1027
+ }
1028
+ };
910
1029
  var scoreGithub = (directives) => {
911
1030
  const pairs = [];
912
1031
  const skipped = [];
913
1032
  for (const d of directives) {
1033
+ if (d.kind === "linkStyle")
1034
+ scoreEdgeLabel(d, pairs, skipped);
914
1035
  if (d.kind !== "classDef" && d.kind !== "style")
915
1036
  continue;
916
1037
  const { fill, color, stroke } = d.properties;
@@ -975,6 +1096,13 @@ var scoreMkdocs = (directives) => {
975
1096
  const pairs = [];
976
1097
  const skipped = [];
977
1098
  for (const d of directives) {
1099
+ if (d.kind === "linkStyle" && d.properties.color) {
1100
+ skipped.push({
1101
+ selector: d.selector,
1102
+ line: d.line,
1103
+ reason: "color: ignored, because the host theme forces edge label text"
1104
+ });
1105
+ }
978
1106
  if (d.kind !== "classDef" && d.kind !== "style")
979
1107
  continue;
980
1108
  const { fill, stroke, color } = d.properties;
@@ -1197,15 +1325,17 @@ var calculateComplexity = (stats, config) => {
1197
1325
  };
1198
1326
  };
1199
1327
  var parseFailed = (content, stats) => {
1200
- const lines = content.split(`
1201
- `).filter((l) => {
1202
- const t = l.trim();
1203
- return t && !t.startsWith("%%") && t !== "---";
1204
- });
1205
- if (lines.length <= 1)
1328
+ if (stats.error !== null)
1329
+ return true;
1330
+ if (stats.parser_used === "langium" || stats.nodes > 0)
1206
1331
  return false;
1207
- return stats.nodes === 0;
1332
+ return sourceLines(content) > 1;
1208
1333
  };
1334
+ var sourceLines = (content) => content.split(`
1335
+ `).filter((l) => {
1336
+ const t = l.trim();
1337
+ return t && !t.startsWith("%%") && t !== "---";
1338
+ }).length;
1209
1339
 
1210
1340
  // src/rules/shared.ts
1211
1341
  var COMPLEXITY_OPTIONS = {
@@ -1331,7 +1461,7 @@ var complexityNodeCountCognitiveLimit = complexityRule({
1331
1461
  // src/rules/complexity-node-count-hard-limit.ts
1332
1462
  var complexityNodeCountHardLimit = complexityRule({
1333
1463
  id: RULE.NODES_HARD,
1334
- summary: "more nodes than any diagram can hold, above the hard limit",
1464
+ summary: "more nodes than the preset's hard limit",
1335
1465
  hit: ({ stats }, c) => stats.nodes > c.node_hard_limit ? {
1336
1466
  message: `${stats.nodes} nodes > ${c.node_hard_limit} hard limit`,
1337
1467
  remediation: "Split immediately. This diagram is beyond any comprehensible size. " + `${boundaryHint(stats.subgraph_names).replace(/^along /, "Split along ")}. ${splitTarget(c)}`
@@ -1362,9 +1492,9 @@ var complexityVisualScoreAcceptableExceeded = complexityRule({
1362
1492
  // src/rules/complexity-visual-score-critical.ts
1363
1493
  var complexityVisualScoreCritical = complexityRule({
1364
1494
  id: RULE.VCS_CRITICAL,
1365
- summary: "a visual complexity score above the critical threshold",
1495
+ summary: "a visual complexity score above the preset's complex threshold",
1366
1496
  hit: ({ stats, metrics }, c) => metrics.visual_complexity_score > c.vcs_complex ? {
1367
- message: `VCS ${metrics.visual_complexity_score.toFixed(1)} > ${c.vcs_complex} critical threshold`,
1497
+ message: `VCS ${metrics.visual_complexity_score.toFixed(1)} > ${c.vcs_complex} complex threshold`,
1368
1498
  remediation: `Reduce diagram complexity. Split ${boundaryHint(stats.subgraph_names)}, remove redundant edges, ` + `or collapse leaf nodes into labelled groups. ${splitTarget(c)}`
1369
1499
  } : null
1370
1500
  });
@@ -1450,7 +1580,7 @@ var contrastTextBelowAa = {
1450
1580
  endLine: line,
1451
1581
  rule: RULE.TEXT,
1452
1582
  message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1453
- remediation: profile === "mkdocs-material" ? "The host theme forces the label text, so change the fill until the host text reads on it in both themes." : "Darken or lighten the fill, or change color:, until the ratio reaches 4.5:1 in every theme."
1583
+ remediation: profile === "mkdocs-material" ? "The host theme forces the label text, so change the fill until the host text reads on it in both themes." : p.directive.kind === "linkStyle" ? "An edge label sits on Mermaid's own label box, light in one theme and grey in the other, and no one colour reads on both, so drop color: from the linkStyle and let the theme set the label text." : "Darken or lighten the fill, or change color:, until the ratio reaches 4.5:1 in every theme."
1454
1584
  });
1455
1585
  }
1456
1586
  }
@@ -1487,18 +1617,30 @@ var contrastTextBelowAa = {
1487
1617
  };
1488
1618
 
1489
1619
  // src/rules/parse-diagram-unparseable.ts
1620
+ var EXTERNAL_TYPES = new Set(["zenuml"]);
1621
+ var messageOf = (stats) => {
1622
+ const type = stats.diagram_type;
1623
+ if (EXTERNAL_TYPES.has(type)) {
1624
+ return `${type} is drawn by an external mermaid add-on that mermaidjs-lint does not include, so it cannot be parsed or checked`;
1625
+ }
1626
+ if (stats.error !== null) {
1627
+ const lines = /\bline \d/.test(stats.error) ? " (its line numbers count within the diagram, not the file)" : "";
1628
+ return `${type} did not parse (parser: ${stats.parser_used})${lines}: ${stats.error}`;
1629
+ }
1630
+ return `${type} parsed to 0 nodes from multi-line source with no error (parser: ${stats.parser_used}), a silent parse failure`;
1631
+ };
1490
1632
  var parseDiagramUnparseable = {
1491
1633
  id: RULE.UNPARSEABLE,
1492
1634
  category: "parse",
1493
- summary: "a multi-line diagram that mermaid's parser reads as zero nodes",
1635
+ summary: "a diagram mermaid's parser rejects, or reads as zero nodes from several lines",
1494
1636
  check: async (ctx) => {
1495
1637
  const found = [];
1496
1638
  for (const diagram of ctx.doc.diagrams) {
1497
1639
  const stats = await diagram.stats();
1498
1640
  if (!parseFailed(diagram.source, stats))
1499
1641
  continue;
1500
- const why = stats.error ? `: ${stats.error}` : "";
1501
- found.push(finding(ctx, diagram, RULE.UNPARSEABLE, `${stats.diagram_type} yielded 0 nodes from multi-line source (parser: ${stats.parser_used})${why}`, "Verify the diagram renders in mermaid.js and fix its syntax. Complexity rules are skipped for this diagram while it cannot be parsed."));
1642
+ const remediation = EXTERNAL_TYPES.has(stats.diagram_type) ? `No fixer can help: mermaidjs-lint has no ${stats.diagram_type} parser. Redraw it as a type mermaid core includes, such as sequenceDiagram, or turn this rule off in the config.` : "Verify the diagram renders in mermaid.js and fix its syntax. Complexity rules are skipped for this diagram while it cannot be parsed.";
1643
+ found.push(finding(ctx, diagram, RULE.UNPARSEABLE, messageOf(stats), remediation));
1502
1644
  }
1503
1645
  return found;
1504
1646
  }
@@ -1644,7 +1786,7 @@ var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
1644
1786
  // src/files.ts
1645
1787
  import { readdirSync, statSync } from "node:fs";
1646
1788
  import { extname, join as join2, resolve as resolve2 } from "node:path";
1647
- var DIAGRAM_FILE_EXTS = new Set([".mmd", ".md", ".markdown"]);
1789
+ var DIAGRAM_FILE_EXTS = new Set([...DIAGRAM_EXTS, ".md", ".markdown"]);
1648
1790
  var SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tmp", "__pycache__"]);
1649
1791
  var skipDir = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
1650
1792
  var collectFiles = (paths) => {
@@ -1756,7 +1898,7 @@ var overlaps = (edits) => {
1756
1898
  };
1757
1899
  var wellFormed = (e, length) => Number.isInteger(e.start) && Number.isInteger(e.end) && e.start >= 0 && e.start <= e.end && e.end <= length && typeof e.text === "string";
1758
1900
  var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
1759
- const kind = file.toLowerCase().endsWith(".mmd") ? "diagram" : "markdown";
1901
+ const kind = documentKind(file);
1760
1902
  const applied = [];
1761
1903
  const refused = [];
1762
1904
  const refusedKeys = new Set;
@@ -1814,7 +1956,7 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
1814
1956
 
1815
1957
  // src/project.ts
1816
1958
  import { access as access2 } from "node:fs/promises";
1817
- import { dirname as dirname3, join as join5, resolve as resolve4 } from "node:path";
1959
+ import { dirname as dirname3, extname as extname5, join as join5, resolve as resolve4 } from "node:path";
1818
1960
 
1819
1961
  // src/config.ts
1820
1962
  import { access, readFile } from "node:fs/promises";
@@ -2283,6 +2425,8 @@ var setUp = async ({
2283
2425
  } else {
2284
2426
  file = await findConfig(root);
2285
2427
  }
2428
+ if (noPlugins && file !== null && extname5(file) === ".mjs")
2429
+ throw new ConfigCodeError(file);
2286
2430
  const config = file ? await loadConfig(file) : NO_CONFIG;
2287
2431
  let mode = "strict";
2288
2432
  if (noPlugins || config.plugins === false)
@@ -2298,39 +2442,47 @@ var setUp = async ({
2298
2442
  categories: plugins.categories,
2299
2443
  sources: plugins.sources,
2300
2444
  config,
2301
- ignoreUnknownPluginIds: mode === "lenient" || mode === "collect"
2445
+ ignoreUnknownPluginIds: mode !== "strict"
2302
2446
  });
2303
2447
  return { registry, skipped: plugins.skipped, failures: plugins.failures };
2304
2448
  };
2305
2449
 
2306
2450
  // src/cli.ts
2307
- var USAGE = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
2451
+ var SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
2308
2452
  [--no-plugins | --lenient-plugins]
2309
2453
  mermaidjs-lint --list-rules
2310
2454
  mermaidjs-lint --validate-plugins [--json] [--config FILE]
2455
+ mermaidjs-lint --version | -h, --help`;
2456
+ var USAGE = `${SYNOPSIS}
2311
2457
 
2312
2458
  Deterministic Mermaid diagram gates (* = autofixable):
2313
2459
  ${CATEGORIES.map((c) => [` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${r.id}${r.fix ? "*" : " "} ${r.summary}`)].join(`
2314
2460
  `)).join(`
2315
2461
  `)}
2316
2462
 
2317
- A path is a .mmd file, a markdown file whose mermaid fences are checked, or a
2318
- directory, which is walked for both.
2319
- Examples of every rule, with fixes: RULES.md
2463
+ A path is a file or a directory, and every path must exist. A file ending in
2464
+ ${DIAGRAM_EXTS.join(" or ")} is read as one raw diagram, and any other file is read as markdown
2465
+ whose mermaid fences are checked. A directory is walked for ${[...DIAGRAM_FILE_EXTS].join(", ")} files,
2466
+ skipping dot-folders and ${[...SKIP_DIRS].join(", ")}.
2467
+ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/
2320
2468
 
2321
- --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
2322
- (aliases low, medium, high). Rule options and the config can set it per rule.
2323
- --profile NAME sets how contrast is read: ${PROFILES.join(", ")}.
2324
- auto reads mkdocs-material when an ancestor holds mkdocs.yml, else github.
2469
+ -p, --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
2470
+ (aliases low, medium, high). It overrides a preset set in the config, and a
2471
+ numeric threshold set in the config overrides both.
2472
+ --profile NAME sets how contrast is read: ${PROFILES.join(", ")}. It overrides a profile set in the config.
2473
+ auto reads mkdocs-material when any parent folder holds mkdocs.yml or mkdocs.yaml, else github.
2325
2474
  --fix applies every fix it can prove safe, then reports what remains.
2326
2475
  A fixer that is unsure leaves the text alone and the finding stays.
2327
2476
  --config FILE reads that config instead of mermaidjs-lint.config.json or .mjs at the project root.
2328
2477
  Plugins load on their own from .mermaidjs-lint/rules and from dependencies named mermaidjs-lint-plugin-*.
2329
- --no-plugins runs the built-in rules only, for a baseline or a repo you do not trust.
2478
+ --no-plugins runs the built-in rules only and loads no plugin. It reads a .json config as usual,
2479
+ and stops with a usage error rather than import a .mjs config, which is code.
2330
2480
  --lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
2331
2481
  --validate-plugins loads and checks every plugin, reports every failure, and runs no check or fix.
2332
2482
  --list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
2333
- Exits 1 when findings remain or a plugin fails to load, 2 on usage error.`;
2483
+ --version prints the version, to quote in a bug report.
2484
+ Exits 0 when no findings remain, 1 when findings remain or a plugin fails to load (a lenient run
2485
+ skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
2334
2486
  var parse2 = (argv) => parseArgs({
2335
2487
  args: argv,
2336
2488
  options: {
@@ -2343,7 +2495,8 @@ var parse2 = (argv) => parseArgs({
2343
2495
  "no-plugins": { type: "boolean", default: false },
2344
2496
  "lenient-plugins": { type: "boolean", default: false },
2345
2497
  "validate-plugins": { type: "boolean", default: false },
2346
- "list-rules": { type: "boolean", default: false }
2498
+ "list-rules": { type: "boolean", default: false },
2499
+ version: { type: "boolean", default: false }
2347
2500
  },
2348
2501
  allowPositionals: true,
2349
2502
  strict: true
@@ -2405,6 +2558,10 @@ var validate = (registry, failures, json) => {
2405
2558
  const line = sources.length === 0 ? "mermaidjs-lint: no plugins found" : `mermaidjs-lint: plugins valid: ${namesOf(registry.sources)}`;
2406
2559
  return { code, stdout: line, stderr: [] };
2407
2560
  };
2561
+ var reportLine = (f, file) => {
2562
+ const where = f.endLine > f.line ? `${f.line}-${f.endLine}` : `${f.line}`;
2563
+ return `${file}:${where} ${f.rule} ${f.message}`;
2564
+ };
2408
2565
  var display = (file, cwd) => {
2409
2566
  const rel = relative(cwd, file);
2410
2567
  return !rel || rel.startsWith("..") ? file : rel;
@@ -2421,6 +2578,10 @@ var run = async (argv, cwd) => {
2421
2578
  console.log(USAGE);
2422
2579
  return 0;
2423
2580
  }
2581
+ if (values.version) {
2582
+ console.log(package_default.version);
2583
+ return 0;
2584
+ }
2424
2585
  const listing = values["list-rules"];
2425
2586
  const validating = values["validate-plugins"];
2426
2587
  if (values.preset !== undefined && canonicalPreset(values.preset) === null) {
@@ -2479,9 +2640,7 @@ var run = async (argv, cwd) => {
2479
2640
  src = fixed;
2480
2641
  }
2481
2642
  }
2482
- const kind = file.toLowerCase().endsWith(".mmd") ? "diagram" : "markdown";
2483
- const found = await checkModel(buildDocModel(src, kind), display(file, cwd), flags, registry);
2484
- return found;
2643
+ return checkModel(buildDocModel(src, documentKind(file)), display(file, cwd), flags, registry);
2485
2644
  }));
2486
2645
  const all = perFile.flat();
2487
2646
  if (values.json) {
@@ -2493,10 +2652,8 @@ var run = async (argv, cwd) => {
2493
2652
  }));
2494
2653
  console.log(JSON.stringify({ findings: all, files: files.length, plugins: registry.sources, skippedPlugins }, null, 2));
2495
2654
  } else {
2496
- for (const f of all) {
2497
- const where = f.endLine > f.line ? `${f.line}-${f.endLine}` : `${f.line}`;
2498
- console.log(`${f.file}:${where} ${f.rule} ${f.message}`);
2499
- }
2655
+ for (const f of all)
2656
+ console.log(reportLine(f, f.file));
2500
2657
  console.log(`${all.length} finding(s) in ${files.length} file(s)`);
2501
2658
  }
2502
2659
  return all.length > 0 ? 1 : 0;
@@ -2509,7 +2666,8 @@ var main = async (argv = process.argv.slice(2), cwd = process.cwd()) => {
2509
2666
  if (!(err instanceof UsageError))
2510
2667
  throw err;
2511
2668
  console.error(describeError(err));
2512
- console.error(USAGE);
2669
+ console.error(SYNOPSIS);
2670
+ console.error("run mermaidjs-lint --help for more");
2513
2671
  return 2;
2514
2672
  }
2515
2673
  };