@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.
- package/README.md +82 -53
- package/dist/bin.js +202 -44
- package/dist/index.js +206 -45
- package/dist/testing.js +86 -23
- package/examples/architecture_beta_iconify_logos.mmd +27 -0
- package/examples/flowchart_fontawesome_icons.mmd +31 -0
- package/examples/layout_cosebilkent_mindmap.mmd +19 -0
- package/examples/layout_dagre_classic.mmd +22 -0
- package/examples/layout_dagre_handdrawn.mmd +23 -0
- package/examples/layout_dagre_neo.mmd +26 -0
- package/examples/layout_elk_default.mmd +22 -0
- package/examples/layout_elk_tuned.mmd +25 -0
- package/examples/layout_tidytree_mindmap.mmd +19 -0
- package/package.json +2 -2
- package/src/check.ts +2 -3
- package/src/cli.ts +45 -23
- package/src/config.ts +3 -2
- package/src/errors.ts +18 -3
- package/src/files.ts +3 -1
- package/src/fix/engine.ts +4 -4
- package/src/fix/verify.ts +2 -2
- package/src/index.ts +1 -1
- package/src/model.ts +10 -0
- package/src/parse/color.ts +2 -2
- package/src/parse/contrast.ts +59 -5
- package/src/parse/metrics.ts +16 -8
- package/src/parse/stats.ts +45 -19
- package/src/plugins/contract.ts +1 -1
- package/src/plugins/discover.ts +5 -5
- package/src/plugins/load.ts +5 -3
- package/src/project.ts +11 -4
- package/src/rules/complexity-node-count-hard-limit.ts +1 -1
- package/src/rules/complexity-visual-score-critical.ts +2 -2
- package/src/rules/contrast-text-below-aa.ts +6 -2
- package/src/rules/index.ts +2 -1
- package/src/rules/parse-diagram-unparseable.ts +29 -14
- package/src/rules/registry.ts +7 -4
- package/src/rules/shared.ts +1 -1
- package/src/rules/types.ts +2 -2
- 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](
|
|
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
|
-
|
|
16
|
-
A
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
Three tenets are guarantees about every diagram the tool passes, and the fourth is a commitment about the tool.
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
A
|
|
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
|
-
|
|
|
36
|
-
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
## Requirements and compatibility
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
##
|
|
62
|
+
## Configure and extend
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
272
|
+
let coreError;
|
|
198
273
|
try {
|
|
199
274
|
return await extractCoreStats(content, keyword);
|
|
200
275
|
} catch (err) {
|
|
201
|
-
|
|
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
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
1501
|
-
found.push(finding(ctx, diagram, RULE.UNPARSEABLE,
|
|
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([
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2318
|
-
|
|
2319
|
-
|
|
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).
|
|
2323
|
-
|
|
2324
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2669
|
+
console.error(SYNOPSIS);
|
|
2670
|
+
console.error("run mermaidjs-lint --help for more");
|
|
2513
2671
|
return 2;
|
|
2514
2672
|
}
|
|
2515
2673
|
};
|