@jpeakai/mermaidjs-lint 0.1.1 → 0.3.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 +84 -53
- package/dist/bin.js +369 -92
- package/dist/index.js +376 -93
- package/dist/testing.js +145 -46
- package/dist/types/bin.d.ts +2 -0
- package/dist/types/check.d.ts +10 -0
- package/dist/types/cli.d.ts +9 -0
- package/dist/types/config.d.ts +20 -0
- package/dist/types/errors.d.ts +88 -0
- package/dist/types/files.d.ts +8 -0
- package/dist/types/fix/engine.d.ts +10 -0
- package/dist/types/fix/verify.d.ts +12 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/model.d.ts +27 -0
- package/dist/types/parse/color.d.ts +28 -0
- package/dist/types/parse/contrast.d.ts +35 -0
- package/dist/types/parse/metrics.d.ts +48 -0
- package/dist/types/parse/stats.d.ts +24 -0
- package/dist/types/plugins/contract.d.ts +24 -0
- package/dist/types/plugins/discover.d.ts +20 -0
- package/dist/types/plugins/index.d.ts +19 -0
- package/dist/types/plugins/load.d.ts +14 -0
- package/dist/types/plugins/runtime.d.ts +6 -0
- package/dist/types/project.d.ts +18 -0
- package/dist/types/rules/complexity-node-count-acceptable-exceeded.d.ts +1 -0
- package/dist/types/rules/complexity-node-count-cognitive-limit.d.ts +1 -0
- package/dist/types/rules/complexity-node-count-hard-limit.d.ts +1 -0
- package/dist/types/rules/complexity-subgraph-nesting-too-deep.d.ts +2 -0
- package/dist/types/rules/complexity-visual-score-acceptable-exceeded.d.ts +1 -0
- package/dist/types/rules/complexity-visual-score-critical.d.ts +1 -0
- package/dist/types/rules/complexity.d.ts +12 -0
- package/dist/types/rules/contrast-border-below-ui-minimum.d.ts +2 -0
- package/dist/types/rules/contrast-directive-unauditable.d.ts +2 -0
- package/dist/types/rules/contrast-text-below-aa.d.ts +2 -0
- package/dist/types/rules/index.d.ts +3 -0
- package/dist/types/rules/parse-diagram-unparseable.d.ts +2 -0
- package/dist/types/rules/parse-fence-unterminated.d.ts +2 -0
- package/dist/types/rules/registry.d.ts +29 -0
- package/dist/types/rules/shared.d.ts +18 -0
- package/dist/types/rules/types.d.ts +67 -0
- package/dist/types/testing.d.ts +15 -0
- 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 +6 -5
- package/src/check.ts +2 -3
- package/src/cli.ts +88 -34
- package/src/config.ts +3 -2
- package/src/errors.ts +64 -3
- package/src/files.ts +24 -3
- package/src/fix/engine.ts +26 -14
- package/src/fix/verify.ts +8 -3
- package/src/index.ts +6 -4
- 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 +84 -30
- package/src/plugins/contract.ts +5 -3
- package/src/plugins/discover.ts +40 -17
- package/src/plugins/index.ts +5 -3
- package/src/plugins/load.ts +37 -4
- package/src/project.ts +14 -5
- 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/dist/bin.js
CHANGED
|
@@ -3,9 +3,81 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/cli.ts
|
|
6
|
-
import { readFile as readFile3, writeFile } from "node:fs/promises";
|
|
7
|
-
import { relative } from "node:path";
|
|
6
|
+
import { lstat, readFile as readFile3, realpath, writeFile } from "node:fs/promises";
|
|
7
|
+
import { isAbsolute as isAbsolute2, relative, resolve as resolve5 } 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.3.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
|
+
types: "./dist/types/index.d.ts",
|
|
42
|
+
exports: {
|
|
43
|
+
".": {
|
|
44
|
+
types: "./dist/types/index.d.ts",
|
|
45
|
+
bun: "./src/index.ts",
|
|
46
|
+
default: "./dist/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./testing": {
|
|
49
|
+
types: "./dist/types/testing.d.ts",
|
|
50
|
+
bun: "./src/testing.ts",
|
|
51
|
+
default: "./dist/testing.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
engines: {
|
|
55
|
+
node: ">=20"
|
|
56
|
+
},
|
|
57
|
+
files: [
|
|
58
|
+
"src",
|
|
59
|
+
"dist",
|
|
60
|
+
"examples"
|
|
61
|
+
],
|
|
62
|
+
dependencies: {
|
|
63
|
+
"@mermaid-js/parser": "^1.1.0",
|
|
64
|
+
"@types/mdast": "^4",
|
|
65
|
+
"colorjs.io": "^0.6.1",
|
|
66
|
+
"happy-dom": "^20.9.0",
|
|
67
|
+
"mdast-util-from-markdown": "^2.0.3",
|
|
68
|
+
"mdast-util-frontmatter": "^2.0.1",
|
|
69
|
+
"mdast-util-gfm": "^3.1.0",
|
|
70
|
+
mermaid: "^11.14.0",
|
|
71
|
+
"micromark-extension-frontmatter": "^2.0.0",
|
|
72
|
+
"micromark-extension-gfm": "^3.0.0"
|
|
73
|
+
},
|
|
74
|
+
devDependencies: {
|
|
75
|
+
"@biomejs/biome": "^2",
|
|
76
|
+
"@jpeakai/prose-gates": "^0.2.0",
|
|
77
|
+
"@types/bun": "latest",
|
|
78
|
+
typescript: "^5"
|
|
79
|
+
}
|
|
80
|
+
};
|
|
9
81
|
|
|
10
82
|
// src/model.ts
|
|
11
83
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
@@ -189,17 +261,20 @@ function detectKeyword(content) {
|
|
|
189
261
|
}
|
|
190
262
|
async function extractStats(content) {
|
|
191
263
|
const keyword = detectKeyword(content) ?? "unknown";
|
|
192
|
-
|
|
264
|
+
const langium = keyword in LANGIUM_TYPES;
|
|
265
|
+
let langiumError = null;
|
|
266
|
+
if (langium) {
|
|
193
267
|
try {
|
|
194
268
|
return await extractLangiumStats(content, keyword);
|
|
195
|
-
} catch {
|
|
269
|
+
} catch (err) {
|
|
270
|
+
langiumError = parserReason(err);
|
|
271
|
+
}
|
|
196
272
|
}
|
|
197
|
-
let
|
|
273
|
+
let coreError;
|
|
198
274
|
try {
|
|
199
275
|
return await extractCoreStats(content, keyword);
|
|
200
276
|
} catch (err) {
|
|
201
|
-
|
|
202
|
-
`)[0] ?? null;
|
|
277
|
+
coreError = parserReason(err);
|
|
203
278
|
}
|
|
204
279
|
return {
|
|
205
280
|
nodes: 0,
|
|
@@ -208,11 +283,20 @@ async function extractStats(content) {
|
|
|
208
283
|
max_subgraph_depth: 0,
|
|
209
284
|
node_ids: [],
|
|
210
285
|
subgraph_names: [],
|
|
211
|
-
parser_used: "mermaid-core",
|
|
286
|
+
parser_used: langium ? "langium" : "mermaid-core",
|
|
212
287
|
diagram_type: keyword,
|
|
213
|
-
error
|
|
288
|
+
error: langiumError ?? coreError
|
|
214
289
|
};
|
|
215
290
|
}
|
|
291
|
+
var POINTER = /^-*\^$/;
|
|
292
|
+
var parserReason = (err) => {
|
|
293
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
294
|
+
const lines = message.split(`
|
|
295
|
+
`);
|
|
296
|
+
if (message.startsWith("No diagram type detected"))
|
|
297
|
+
return (lines[0] ?? "").trim();
|
|
298
|
+
return lines.filter((line, i) => !POINTER.test(line.trim()) && !POINTER.test((lines[i + 1] ?? "").trim())).join(" ").replace(/\s+/g, " ").trim();
|
|
299
|
+
};
|
|
216
300
|
async function langiumParseAny(type, content) {
|
|
217
301
|
switch (type) {
|
|
218
302
|
case "architecture":
|
|
@@ -541,12 +625,10 @@ function adaptMindmap(db) {
|
|
|
541
625
|
}
|
|
542
626
|
function adaptQuadrantChart(db) {
|
|
543
627
|
const data = callGetter(db, "getQuadrantData");
|
|
544
|
-
const
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
});
|
|
549
|
-
return { nodeIds: labels, edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
628
|
+
const textOf = (t) => typeof t === "string" ? t : t?.text ?? "";
|
|
629
|
+
const quadrants = (data?.quadrants ?? []).map((q) => textOf(q.text)).filter((t) => t.trim() !== "");
|
|
630
|
+
const points = (data?.points ?? []).map((p, i) => textOf(p.text) || `point_${i}`);
|
|
631
|
+
return { nodeIds: [...quadrants, ...points], edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
550
632
|
}
|
|
551
633
|
function adaptRequirement(db) {
|
|
552
634
|
const requirements = db.requirements instanceof Map ? db.requirements : new Map;
|
|
@@ -572,18 +654,30 @@ function adaptSankey(db) {
|
|
|
572
654
|
function adaptState(db) {
|
|
573
655
|
const nodes = db.nodes ?? [];
|
|
574
656
|
const edges = db.edges ?? [];
|
|
657
|
+
const groups = nodes.filter((n) => n.isGroup === true);
|
|
658
|
+
const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
|
|
659
|
+
let maxDepth = 0;
|
|
660
|
+
for (const g of groups) {
|
|
661
|
+
let depth = 1;
|
|
662
|
+
const seen = new Set;
|
|
663
|
+
for (let cur = g.parentId;cur && !seen.has(cur); cur = parentOf.get(cur)) {
|
|
664
|
+
seen.add(cur);
|
|
665
|
+
depth++;
|
|
666
|
+
}
|
|
667
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
668
|
+
}
|
|
575
669
|
return {
|
|
576
670
|
nodeIds: nodes.map((n) => n.id ?? ""),
|
|
577
671
|
edges: edges.length,
|
|
578
|
-
subgraphNames:
|
|
579
|
-
maxDepth
|
|
672
|
+
subgraphNames: groups.map((g) => g.id ?? ""),
|
|
673
|
+
maxDepth
|
|
580
674
|
};
|
|
581
675
|
}
|
|
582
676
|
function adaptTimeline(db) {
|
|
583
677
|
const tasks = callGetter(db, "getTasks") ?? [];
|
|
584
678
|
const sections = callGetter(db, "getSections") ?? [];
|
|
585
679
|
return {
|
|
586
|
-
nodeIds: tasks.
|
|
680
|
+
nodeIds: tasks.flatMap((t) => [t.task ?? "", ...(t.events ?? []).map((e) => `${t.task ?? ""}: ${e}`)]),
|
|
587
681
|
edges: 0,
|
|
588
682
|
subgraphNames: sections.map((s) => s.sectionName ?? s.section ?? ""),
|
|
589
683
|
maxDepth: sections.length > 0 ? 1 : 0
|
|
@@ -613,7 +707,10 @@ function adaptVenn(db) {
|
|
|
613
707
|
function adaptXychart(db) {
|
|
614
708
|
const data = callGetter(db, "getXYChartData");
|
|
615
709
|
const categories = data?.xAxis?.categories ?? [];
|
|
616
|
-
|
|
710
|
+
if (categories.length > 0)
|
|
711
|
+
return { nodeIds: categories, edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
712
|
+
const longest = Math.max(0, ...(data?.plots ?? []).map((p) => p.data?.length ?? 0));
|
|
713
|
+
return { nodeIds: Array.from({ length: longest }, (_, i) => `x_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
617
714
|
}
|
|
618
715
|
function subgraphNestingDepth(subGraphs) {
|
|
619
716
|
if (subGraphs.length === 0)
|
|
@@ -698,6 +795,11 @@ var collectMermaid = (node, out) => {
|
|
|
698
795
|
for (const child of node.children ?? [])
|
|
699
796
|
collectMermaid(child, out);
|
|
700
797
|
};
|
|
798
|
+
var DIAGRAM_EXTS = [".mmd", ".mermaid"];
|
|
799
|
+
var documentKind = (file) => {
|
|
800
|
+
const lower = file.toLowerCase();
|
|
801
|
+
return DIAGRAM_EXTS.some((ext) => lower.endsWith(ext)) ? "diagram" : "markdown";
|
|
802
|
+
};
|
|
701
803
|
var buildDocModel = (src, kind = "markdown") => {
|
|
702
804
|
const lineStarts = lineStartsOf(src);
|
|
703
805
|
if (kind === "diagram") {
|
|
@@ -775,6 +877,14 @@ class ConfigNotFoundError extends UsageError {
|
|
|
775
877
|
}
|
|
776
878
|
}
|
|
777
879
|
|
|
880
|
+
class ConfigCodeError extends UsageError {
|
|
881
|
+
file;
|
|
882
|
+
constructor(file) {
|
|
883
|
+
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");
|
|
884
|
+
this.file = file;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
778
888
|
class AmbiguousConfigError extends UsageError {
|
|
779
889
|
files;
|
|
780
890
|
constructor(root, files) {
|
|
@@ -827,6 +937,34 @@ class RuleOptionError extends UsageError {
|
|
|
827
937
|
}
|
|
828
938
|
}
|
|
829
939
|
|
|
940
|
+
class FixOutsideRootError extends UsageError {
|
|
941
|
+
file;
|
|
942
|
+
target;
|
|
943
|
+
constructor(file, target, root) {
|
|
944
|
+
super(`${file} is a symbolic link to ${target}, outside the project root ${root}; --fix does not write through it. Fix the target in its own project, or name it directly`, "fix-outside-root");
|
|
945
|
+
this.file = file;
|
|
946
|
+
this.target = target;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
class PackageJsonError extends MermaidLintError {
|
|
951
|
+
file;
|
|
952
|
+
constructor(file) {
|
|
953
|
+
super("package-json", `${file} is not a JSON object; fix or remove it`);
|
|
954
|
+
this.file = file;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
class FixNotConvergingError extends MermaidLintError {
|
|
959
|
+
file;
|
|
960
|
+
rules;
|
|
961
|
+
constructor(file, passes, rules) {
|
|
962
|
+
super("fix-not-converging", `${file}: --fix did not converge after ${passes} passes; ${rules.join(", ")} kept proposing edits. A fixer must propose nothing once its edit is applied`);
|
|
963
|
+
this.file = file;
|
|
964
|
+
this.rules = rules;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
830
968
|
class PluginLoadError extends MermaidLintError {
|
|
831
969
|
source;
|
|
832
970
|
constructor(source, reason, code = "plugin") {
|
|
@@ -887,6 +1025,14 @@ var MKDOCS_MATERIAL = {
|
|
|
887
1025
|
};
|
|
888
1026
|
var GITHUB = { light: { bg: "#ffffff" }, dark: { bg: "#0d1117" } };
|
|
889
1027
|
var THEMES = ["light", "dark"];
|
|
1028
|
+
var MERMAID_EDGE_LABEL = {
|
|
1029
|
+
light: { box: "rgba(232,232,232,0.8)", backing: "rgba(232,232,232,0.5)" },
|
|
1030
|
+
dark: { box: "#585858", backing: "rgba(88,88,88,0.5)" }
|
|
1031
|
+
};
|
|
1032
|
+
var edgeLabelBackground = (theme) => {
|
|
1033
|
+
const { box, backing } = MERMAID_EDGE_LABEL[theme];
|
|
1034
|
+
return compositeOver(box, compositeOver(box, compositeOver(backing, GITHUB[theme].bg)));
|
|
1035
|
+
};
|
|
890
1036
|
var MERMAID_ER = {
|
|
891
1037
|
light: { odd: "#ffffff", even: "#f1f1ff", label: "#333333" },
|
|
892
1038
|
dark: { odd: "#2c2d2d", even: "#060606", label: "#cccccc" }
|
|
@@ -907,10 +1053,27 @@ var scorePair = (kind, d, fg, bg) => {
|
|
|
907
1053
|
};
|
|
908
1054
|
};
|
|
909
1055
|
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
1056
|
+
var scoreEdgeLabel = (d, pairs, skipped) => {
|
|
1057
|
+
const { color } = d.properties;
|
|
1058
|
+
if (!color)
|
|
1059
|
+
return;
|
|
1060
|
+
try {
|
|
1061
|
+
pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t))));
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
skipped.push({
|
|
1064
|
+
selector: d.selector,
|
|
1065
|
+
line: d.line,
|
|
1066
|
+
reason: `edge label text unparseable: ${message(err)}`,
|
|
1067
|
+
blocking: true
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
910
1071
|
var scoreGithub = (directives) => {
|
|
911
1072
|
const pairs = [];
|
|
912
1073
|
const skipped = [];
|
|
913
1074
|
for (const d of directives) {
|
|
1075
|
+
if (d.kind === "linkStyle")
|
|
1076
|
+
scoreEdgeLabel(d, pairs, skipped);
|
|
914
1077
|
if (d.kind !== "classDef" && d.kind !== "style")
|
|
915
1078
|
continue;
|
|
916
1079
|
const { fill, color, stroke } = d.properties;
|
|
@@ -975,6 +1138,13 @@ var scoreMkdocs = (directives) => {
|
|
|
975
1138
|
const pairs = [];
|
|
976
1139
|
const skipped = [];
|
|
977
1140
|
for (const d of directives) {
|
|
1141
|
+
if (d.kind === "linkStyle" && d.properties.color) {
|
|
1142
|
+
skipped.push({
|
|
1143
|
+
selector: d.selector,
|
|
1144
|
+
line: d.line,
|
|
1145
|
+
reason: "color: ignored, because the host theme forces edge label text"
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
978
1148
|
if (d.kind !== "classDef" && d.kind !== "style")
|
|
979
1149
|
continue;
|
|
980
1150
|
const { fill, stroke, color } = d.properties;
|
|
@@ -1197,15 +1367,17 @@ var calculateComplexity = (stats, config) => {
|
|
|
1197
1367
|
};
|
|
1198
1368
|
};
|
|
1199
1369
|
var parseFailed = (content, stats) => {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
return t && !t.startsWith("%%") && t !== "---";
|
|
1204
|
-
});
|
|
1205
|
-
if (lines.length <= 1)
|
|
1370
|
+
if (stats.error !== null)
|
|
1371
|
+
return true;
|
|
1372
|
+
if (stats.parser_used === "langium" || stats.nodes > 0)
|
|
1206
1373
|
return false;
|
|
1207
|
-
return
|
|
1374
|
+
return sourceLines(content) > 1;
|
|
1208
1375
|
};
|
|
1376
|
+
var sourceLines = (content) => content.split(`
|
|
1377
|
+
`).filter((l) => {
|
|
1378
|
+
const t = l.trim();
|
|
1379
|
+
return t && !t.startsWith("%%") && t !== "---";
|
|
1380
|
+
}).length;
|
|
1209
1381
|
|
|
1210
1382
|
// src/rules/shared.ts
|
|
1211
1383
|
var COMPLEXITY_OPTIONS = {
|
|
@@ -1331,7 +1503,7 @@ var complexityNodeCountCognitiveLimit = complexityRule({
|
|
|
1331
1503
|
// src/rules/complexity-node-count-hard-limit.ts
|
|
1332
1504
|
var complexityNodeCountHardLimit = complexityRule({
|
|
1333
1505
|
id: RULE.NODES_HARD,
|
|
1334
|
-
summary: "more nodes than
|
|
1506
|
+
summary: "more nodes than the preset's hard limit",
|
|
1335
1507
|
hit: ({ stats }, c) => stats.nodes > c.node_hard_limit ? {
|
|
1336
1508
|
message: `${stats.nodes} nodes > ${c.node_hard_limit} hard limit`,
|
|
1337
1509
|
remediation: "Split immediately. This diagram is beyond any comprehensible size. " + `${boundaryHint(stats.subgraph_names).replace(/^along /, "Split along ")}. ${splitTarget(c)}`
|
|
@@ -1362,9 +1534,9 @@ var complexityVisualScoreAcceptableExceeded = complexityRule({
|
|
|
1362
1534
|
// src/rules/complexity-visual-score-critical.ts
|
|
1363
1535
|
var complexityVisualScoreCritical = complexityRule({
|
|
1364
1536
|
id: RULE.VCS_CRITICAL,
|
|
1365
|
-
summary: "a visual complexity score above the
|
|
1537
|
+
summary: "a visual complexity score above the preset's complex threshold",
|
|
1366
1538
|
hit: ({ stats, metrics }, c) => metrics.visual_complexity_score > c.vcs_complex ? {
|
|
1367
|
-
message: `VCS ${metrics.visual_complexity_score.toFixed(1)} > ${c.vcs_complex}
|
|
1539
|
+
message: `VCS ${metrics.visual_complexity_score.toFixed(1)} > ${c.vcs_complex} complex threshold`,
|
|
1368
1540
|
remediation: `Reduce diagram complexity. Split ${boundaryHint(stats.subgraph_names)}, remove redundant edges, ` + `or collapse leaf nodes into labelled groups. ${splitTarget(c)}`
|
|
1369
1541
|
} : null
|
|
1370
1542
|
});
|
|
@@ -1450,7 +1622,7 @@ var contrastTextBelowAa = {
|
|
|
1450
1622
|
endLine: line,
|
|
1451
1623
|
rule: RULE.TEXT,
|
|
1452
1624
|
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."
|
|
1625
|
+
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
1626
|
});
|
|
1455
1627
|
}
|
|
1456
1628
|
}
|
|
@@ -1487,18 +1659,30 @@ var contrastTextBelowAa = {
|
|
|
1487
1659
|
};
|
|
1488
1660
|
|
|
1489
1661
|
// src/rules/parse-diagram-unparseable.ts
|
|
1662
|
+
var EXTERNAL_TYPES = new Set(["zenuml"]);
|
|
1663
|
+
var messageOf = (stats) => {
|
|
1664
|
+
const type = stats.diagram_type;
|
|
1665
|
+
if (EXTERNAL_TYPES.has(type)) {
|
|
1666
|
+
return `${type} is drawn by an external mermaid add-on that mermaidjs-lint does not include, so it cannot be parsed or checked`;
|
|
1667
|
+
}
|
|
1668
|
+
if (stats.error !== null) {
|
|
1669
|
+
const lines = /\bline \d/.test(stats.error) ? " (its line numbers count within the diagram, not the file)" : "";
|
|
1670
|
+
return `${type} did not parse (parser: ${stats.parser_used})${lines}: ${stats.error}`;
|
|
1671
|
+
}
|
|
1672
|
+
return `${type} parsed to 0 nodes from multi-line source with no error (parser: ${stats.parser_used}), a silent parse failure`;
|
|
1673
|
+
};
|
|
1490
1674
|
var parseDiagramUnparseable = {
|
|
1491
1675
|
id: RULE.UNPARSEABLE,
|
|
1492
1676
|
category: "parse",
|
|
1493
|
-
summary: "a
|
|
1677
|
+
summary: "a diagram mermaid's parser rejects, or reads as zero nodes from several lines",
|
|
1494
1678
|
check: async (ctx) => {
|
|
1495
1679
|
const found = [];
|
|
1496
1680
|
for (const diagram of ctx.doc.diagrams) {
|
|
1497
1681
|
const stats = await diagram.stats();
|
|
1498
1682
|
if (!parseFailed(diagram.source, stats))
|
|
1499
1683
|
continue;
|
|
1500
|
-
const
|
|
1501
|
-
found.push(finding(ctx, diagram, RULE.UNPARSEABLE,
|
|
1684
|
+
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.";
|
|
1685
|
+
found.push(finding(ctx, diagram, RULE.UNPARSEABLE, messageOf(stats), remediation));
|
|
1502
1686
|
}
|
|
1503
1687
|
return found;
|
|
1504
1688
|
}
|
|
@@ -1644,12 +1828,20 @@ var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
|
|
|
1644
1828
|
// src/files.ts
|
|
1645
1829
|
import { readdirSync, statSync } from "node:fs";
|
|
1646
1830
|
import { extname, join as join2, resolve as resolve2 } from "node:path";
|
|
1647
|
-
var DIAGRAM_FILE_EXTS = new Set([
|
|
1831
|
+
var DIAGRAM_FILE_EXTS = new Set([...DIAGRAM_EXTS, ".md", ".markdown"]);
|
|
1648
1832
|
var SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tmp", "__pycache__"]);
|
|
1649
1833
|
var skipDir = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
|
|
1834
|
+
var linksToDir = (path) => {
|
|
1835
|
+
try {
|
|
1836
|
+
return statSync(path).isDirectory();
|
|
1837
|
+
} catch {
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1650
1841
|
var collectFiles = (paths) => {
|
|
1651
1842
|
const files = [];
|
|
1652
1843
|
const unreadable = [];
|
|
1844
|
+
const symlinks = [];
|
|
1653
1845
|
const seen = new Set;
|
|
1654
1846
|
const walk = (dir) => {
|
|
1655
1847
|
let entries;
|
|
@@ -1661,7 +1853,11 @@ var collectFiles = (paths) => {
|
|
|
1661
1853
|
}
|
|
1662
1854
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1663
1855
|
const abs = join2(dir, entry.name);
|
|
1664
|
-
if (entry.
|
|
1856
|
+
if (entry.isSymbolicLink()) {
|
|
1857
|
+
if (!skipDir(entry.name) && (DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase()) || linksToDir(abs))) {
|
|
1858
|
+
symlinks.push(abs);
|
|
1859
|
+
}
|
|
1860
|
+
} else if (entry.isDirectory()) {
|
|
1665
1861
|
if (!skipDir(entry.name))
|
|
1666
1862
|
walk(abs);
|
|
1667
1863
|
} else if (entry.isFile() && DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase())) {
|
|
@@ -1686,7 +1882,7 @@ var collectFiles = (paths) => {
|
|
|
1686
1882
|
unreadable.push(p);
|
|
1687
1883
|
}
|
|
1688
1884
|
}
|
|
1689
|
-
return { files, unreadable };
|
|
1885
|
+
return { files, unreadable, symlinks };
|
|
1690
1886
|
};
|
|
1691
1887
|
|
|
1692
1888
|
// src/fix/verify.ts
|
|
@@ -1737,13 +1933,17 @@ var sameDiagrams = async (before, after) => {
|
|
|
1737
1933
|
};
|
|
1738
1934
|
var verify = async (edit, before, after) => {
|
|
1739
1935
|
if (edit.expect.kind === "same-tree") {
|
|
1740
|
-
|
|
1936
|
+
if (!sameTree(before.tree, after.tree, {}))
|
|
1937
|
+
return "refuse";
|
|
1938
|
+
if (before.kind === "markdown" && after.kind === "markdown")
|
|
1939
|
+
return "accept";
|
|
1741
1940
|
}
|
|
1742
1941
|
return await sameDiagrams(before, after) ? "accept" : "refuse";
|
|
1743
1942
|
};
|
|
1744
1943
|
|
|
1745
1944
|
// src/fix/engine.ts
|
|
1746
1945
|
var MAX_PASSES = 1000;
|
|
1946
|
+
var RECENT_PASSES = 50;
|
|
1747
1947
|
var splice = (src, edits) => {
|
|
1748
1948
|
let out = src;
|
|
1749
1949
|
for (const e of [...edits].sort((a, b) => b.start - a.start))
|
|
@@ -1756,11 +1956,12 @@ var overlaps = (edits) => {
|
|
|
1756
1956
|
};
|
|
1757
1957
|
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
1958
|
var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
1759
|
-
const kind = file
|
|
1959
|
+
const kind = documentKind(file);
|
|
1760
1960
|
const applied = [];
|
|
1761
1961
|
const refused = [];
|
|
1762
1962
|
const refusedKeys = new Set;
|
|
1763
1963
|
let current = src;
|
|
1964
|
+
const keptBy = [];
|
|
1764
1965
|
for (let pass = 0;pass < MAX_PASSES; pass++) {
|
|
1765
1966
|
const doc = buildDocModel(current, kind);
|
|
1766
1967
|
const keyOf = (e) => `${e.rule}\x00${current.slice(e.start, e.end)}\x00${e.text}`;
|
|
@@ -1793,28 +1994,31 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
|
1793
1994
|
continue;
|
|
1794
1995
|
if (edits.length > 1 && !overlaps(edits) && await attempt(edits)) {
|
|
1795
1996
|
progressed = true;
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1997
|
+
} else {
|
|
1998
|
+
for (const edit of edits) {
|
|
1999
|
+
if (await attempt([edit])) {
|
|
2000
|
+
progressed = true;
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
refusedKeys.add(keyOf(edit));
|
|
2004
|
+
refused.push(edit);
|
|
1802
2005
|
}
|
|
1803
|
-
refusedKeys.add(keyOf(edit));
|
|
1804
|
-
refused.push(edit);
|
|
1805
2006
|
}
|
|
1806
|
-
if (progressed)
|
|
2007
|
+
if (progressed) {
|
|
2008
|
+
keptBy.push(rule.id);
|
|
1807
2009
|
break;
|
|
2010
|
+
}
|
|
1808
2011
|
}
|
|
1809
2012
|
if (!progressed)
|
|
1810
2013
|
return { output: current, applied, refused };
|
|
1811
2014
|
}
|
|
1812
|
-
|
|
2015
|
+
const lingering = [...new Set(keptBy.slice(-RECENT_PASSES))].sort();
|
|
2016
|
+
throw new FixNotConvergingError(file, MAX_PASSES, lingering);
|
|
1813
2017
|
};
|
|
1814
2018
|
|
|
1815
2019
|
// src/project.ts
|
|
1816
2020
|
import { access as access2 } from "node:fs/promises";
|
|
1817
|
-
import { dirname as dirname3, join as join5, resolve as resolve4 } from "node:path";
|
|
2021
|
+
import { dirname as dirname3, extname as extname5, join as join5, resolve as resolve4 } from "node:path";
|
|
1818
2022
|
|
|
1819
2023
|
// src/config.ts
|
|
1820
2024
|
import { access, readFile } from "node:fs/promises";
|
|
@@ -1911,7 +2115,7 @@ var readJson = async (path) => {
|
|
|
1911
2115
|
if (isRecord2(value))
|
|
1912
2116
|
return value;
|
|
1913
2117
|
} catch {}
|
|
1914
|
-
throw new
|
|
2118
|
+
throw new PackageJsonError(path);
|
|
1915
2119
|
};
|
|
1916
2120
|
var LOCAL_EXTENSIONS = new Set([".js", ".mjs", ".ts", ".mts", ".cts"]);
|
|
1917
2121
|
var localSources = async (root) => {
|
|
@@ -1955,7 +2159,14 @@ var entryOf = (pkg) => {
|
|
|
1955
2159
|
var resolvePackage = async (root, name) => {
|
|
1956
2160
|
for (let dir = resolve3(root);; dir = dirname2(dir)) {
|
|
1957
2161
|
const folder = join4(dir, "node_modules", name);
|
|
1958
|
-
|
|
2162
|
+
let pkg;
|
|
2163
|
+
try {
|
|
2164
|
+
pkg = await readJson(join4(folder, "package.json"));
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
if (!(err instanceof PackageJsonError))
|
|
2167
|
+
throw err;
|
|
2168
|
+
throw new PluginSourceError(name, `its package.json at ${err.file} is not a JSON object`);
|
|
2169
|
+
}
|
|
1959
2170
|
if (pkg)
|
|
1960
2171
|
return join4(folder, entryOf(pkg));
|
|
1961
2172
|
if (dirname2(dir) === dir)
|
|
@@ -1963,8 +2174,15 @@ var resolvePackage = async (root, name) => {
|
|
|
1963
2174
|
}
|
|
1964
2175
|
throw new PluginNotInstalledError(name, root);
|
|
1965
2176
|
};
|
|
1966
|
-
var declaredPlugins = async (root) => {
|
|
1967
|
-
|
|
2177
|
+
var declaredPlugins = async (root, tolerateBadManifest) => {
|
|
2178
|
+
let pkg;
|
|
2179
|
+
try {
|
|
2180
|
+
pkg = await readJson(join4(root, "package.json"));
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
if (tolerateBadManifest && err instanceof PackageJsonError)
|
|
2183
|
+
return [];
|
|
2184
|
+
throw err;
|
|
2185
|
+
}
|
|
1968
2186
|
if (!pkg)
|
|
1969
2187
|
return [];
|
|
1970
2188
|
const names = ["dependencies", "devDependencies", "optionalDependencies"].flatMap((field) => {
|
|
@@ -1973,19 +2191,19 @@ var declaredPlugins = async (root) => {
|
|
|
1973
2191
|
});
|
|
1974
2192
|
return [...new Set(names)].filter((name) => PACKAGE_PATTERN.test(name)).sort();
|
|
1975
2193
|
};
|
|
1976
|
-
var discoverSources = async (root, config) => {
|
|
2194
|
+
var discoverSources = async (root, config, pluginsOff = false) => {
|
|
1977
2195
|
const sources = await localSources(root);
|
|
1978
2196
|
const failures = [];
|
|
1979
2197
|
const add = async (kind, spec, resolveIt) => {
|
|
1980
2198
|
try {
|
|
1981
2199
|
sources.push({ kind, spec, path: await resolveIt() });
|
|
1982
2200
|
} catch (err) {
|
|
1983
|
-
if (!(err instanceof
|
|
2201
|
+
if (!(err instanceof PluginLoadError))
|
|
1984
2202
|
throw err;
|
|
1985
|
-
failures.push(err);
|
|
2203
|
+
failures.push({ kind, error: err });
|
|
1986
2204
|
}
|
|
1987
2205
|
};
|
|
1988
|
-
for (const name of await declaredPlugins(root))
|
|
2206
|
+
for (const name of await declaredPlugins(root, pluginsOff))
|
|
1989
2207
|
await add("package", name, () => resolvePackage(root, name));
|
|
1990
2208
|
for (const spec of config.plugins || []) {
|
|
1991
2209
|
const path = spec.startsWith(".") || isAbsolute(spec);
|
|
@@ -2002,8 +2220,8 @@ import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
|
2002
2220
|
// src/plugins/contract.ts
|
|
2003
2221
|
var API_VERSION = 1;
|
|
2004
2222
|
var CATEGORY_WORD = /^[a-z]+$/;
|
|
2005
|
-
var NAMESPACE = /^[a-z][a-z0-9-]*$/;
|
|
2006
|
-
var RULE_KEY = /^[a-z][a-z0-9-]*$/;
|
|
2223
|
+
var NAMESPACE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2224
|
+
var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2007
2225
|
|
|
2008
2226
|
// src/plugins/runtime.ts
|
|
2009
2227
|
var canImportTypeScript = (runtime = process) => typeof runtime.versions.bun === "string" || Boolean(runtime.features.typescript);
|
|
@@ -2030,6 +2248,7 @@ var importModule = async (source, runtime) => {
|
|
|
2030
2248
|
}
|
|
2031
2249
|
};
|
|
2032
2250
|
var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]"]);
|
|
2251
|
+
var RESERVED_OPTIONS = ["preset", "profile"];
|
|
2033
2252
|
var checkRule = (spec, key, raw, own) => {
|
|
2034
2253
|
const where = `rule "${key}"`;
|
|
2035
2254
|
if (!isRecord3(raw))
|
|
@@ -2059,6 +2278,11 @@ var checkRule = (spec, key, raw, own) => {
|
|
|
2059
2278
|
if (!valid) {
|
|
2060
2279
|
throw new PluginContractError(spec, `${where} options must map names to number, string, boolean or string[]`);
|
|
2061
2280
|
}
|
|
2281
|
+
for (const name of RESERVED_OPTIONS) {
|
|
2282
|
+
if (name in options && options[name] !== "string") {
|
|
2283
|
+
throw new PluginContractError(spec, `${where} declares option "${name}" as ${String(options[name])}, but "${name}" is reserved for the built-in ${name}, which is a string set by --${name}; declare it as "string" to follow the run's ${name}, or pick another name`);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2062
2286
|
}
|
|
2063
2287
|
return raw;
|
|
2064
2288
|
};
|
|
@@ -2110,7 +2334,18 @@ var loadLocal = async (sources, runtime) => {
|
|
|
2110
2334
|
const categories = {};
|
|
2111
2335
|
const owner = {};
|
|
2112
2336
|
const loaded = [];
|
|
2337
|
+
const keyOf = (source) => basename(source.path, extname4(source.path));
|
|
2338
|
+
const byKey = new Map;
|
|
2339
|
+
for (const source of sources)
|
|
2340
|
+
byKey.set(keyOf(source), [...byKey.get(keyOf(source)) ?? [], source]);
|
|
2341
|
+
for (const [key, group] of byKey) {
|
|
2342
|
+
if (group.length < 2)
|
|
2343
|
+
continue;
|
|
2344
|
+
fail(new PluginContractError(group[0].spec, `rule "${key}" is defined by more than one file: ${group.map((s) => s.spec).join(", ")}; each rule comes from exactly one file, so rename or remove all but one`));
|
|
2345
|
+
}
|
|
2113
2346
|
for (const source of sources) {
|
|
2347
|
+
if ((byKey.get(keyOf(source))?.length ?? 0) > 1)
|
|
2348
|
+
continue;
|
|
2114
2349
|
try {
|
|
2115
2350
|
const mod = await importModule(source, runtime);
|
|
2116
2351
|
if (mod.default === undefined) {
|
|
@@ -2124,7 +2359,7 @@ var loadLocal = async (sources, runtime) => {
|
|
|
2124
2359
|
categories[word] = description;
|
|
2125
2360
|
owner[word] = source.spec;
|
|
2126
2361
|
}
|
|
2127
|
-
loaded.push({ key:
|
|
2362
|
+
loaded.push({ key: keyOf(source), source, raw: mod.default });
|
|
2128
2363
|
} catch (err) {
|
|
2129
2364
|
fail(err);
|
|
2130
2365
|
}
|
|
@@ -2188,7 +2423,7 @@ var toRule = (namespace, key, rule) => {
|
|
|
2188
2423
|
|
|
2189
2424
|
// src/plugins/index.ts
|
|
2190
2425
|
var loadPlugins = async (root, config, mode) => {
|
|
2191
|
-
const { sources: found, failures: unresolved } = await discoverSources(root, config);
|
|
2426
|
+
const { sources: found, failures: unresolved } = await discoverSources(root, config, mode === "off");
|
|
2192
2427
|
const set = {
|
|
2193
2428
|
rules: [],
|
|
2194
2429
|
categories: new Map(BUILTIN_CATEGORIES),
|
|
@@ -2196,16 +2431,17 @@ var loadPlugins = async (root, config, mode) => {
|
|
|
2196
2431
|
skipped: [],
|
|
2197
2432
|
failures: []
|
|
2198
2433
|
};
|
|
2199
|
-
if (mode === "off")
|
|
2200
|
-
return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((
|
|
2434
|
+
if (mode === "off") {
|
|
2435
|
+
return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((u) => u.error.source)] };
|
|
2436
|
+
}
|
|
2201
2437
|
const settle = (error, kind) => {
|
|
2202
2438
|
if (mode === "strict")
|
|
2203
2439
|
throw error;
|
|
2204
2440
|
if (!set.failures.some((f) => f.source === error.source))
|
|
2205
2441
|
set.failures.push({ source: error.source, kind, error });
|
|
2206
2442
|
};
|
|
2207
|
-
for (const error of unresolved)
|
|
2208
|
-
settle(error,
|
|
2443
|
+
for (const { error, kind } of unresolved)
|
|
2444
|
+
settle(error, kind);
|
|
2209
2445
|
const loaded = [];
|
|
2210
2446
|
const locals = found.filter((s) => s.kind === "local");
|
|
2211
2447
|
if (locals.length > 0) {
|
|
@@ -2283,6 +2519,8 @@ var setUp = async ({
|
|
|
2283
2519
|
} else {
|
|
2284
2520
|
file = await findConfig(root);
|
|
2285
2521
|
}
|
|
2522
|
+
if (noPlugins && file !== null && extname5(file) === ".mjs")
|
|
2523
|
+
throw new ConfigCodeError(file);
|
|
2286
2524
|
const config = file ? await loadConfig(file) : NO_CONFIG;
|
|
2287
2525
|
let mode = "strict";
|
|
2288
2526
|
if (noPlugins || config.plugins === false)
|
|
@@ -2298,39 +2536,53 @@ var setUp = async ({
|
|
|
2298
2536
|
categories: plugins.categories,
|
|
2299
2537
|
sources: plugins.sources,
|
|
2300
2538
|
config,
|
|
2301
|
-
ignoreUnknownPluginIds: mode
|
|
2539
|
+
ignoreUnknownPluginIds: mode !== "strict"
|
|
2302
2540
|
});
|
|
2303
|
-
return { registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
2541
|
+
return { root, registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
2304
2542
|
};
|
|
2305
2543
|
|
|
2306
2544
|
// src/cli.ts
|
|
2307
|
-
var
|
|
2545
|
+
var SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
|
|
2308
2546
|
[--no-plugins | --lenient-plugins]
|
|
2309
2547
|
mermaidjs-lint --list-rules
|
|
2310
2548
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
2549
|
+
mermaidjs-lint --version | -h, --help`;
|
|
2550
|
+
var ruleRow = (r, width) => `${`${r.id}${r.fix ? "*" : ""}`.padEnd(width)} ${r.summary}`;
|
|
2551
|
+
var idWidth = (rules) => Math.max(0, ...rules.map((r) => r.id.length + 1));
|
|
2552
|
+
var USAGE = `${SYNOPSIS}
|
|
2311
2553
|
|
|
2312
2554
|
Deterministic Mermaid diagram gates (* = autofixable):
|
|
2313
|
-
${CATEGORIES.map((c) => [` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${r
|
|
2555
|
+
${CATEGORIES.map((c) => [` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${ruleRow(r, idWidth(RULES))}`)].join(`
|
|
2314
2556
|
`)).join(`
|
|
2315
2557
|
`)}
|
|
2316
2558
|
|
|
2317
|
-
A path is a
|
|
2318
|
-
|
|
2319
|
-
|
|
2559
|
+
A path is a file or a directory, and every path must exist. A file ending in
|
|
2560
|
+
${DIAGRAM_EXTS.join(" or ")} is read as one raw diagram, and any other file is read as markdown
|
|
2561
|
+
whose mermaid fences are checked. A directory is walked for ${[...DIAGRAM_FILE_EXTS].join(", ")} files,
|
|
2562
|
+
skipping dot-folders and ${[...SKIP_DIRS].join(", ")}. The walk does not follow a symbolic link,
|
|
2563
|
+
and names each one it skipped. A link named as a path is read, but --fix does not write
|
|
2564
|
+
through one whose target is outside the project root.
|
|
2565
|
+
Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/
|
|
2320
2566
|
|
|
2321
|
-
--preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
|
|
2322
|
-
(aliases low, medium, high).
|
|
2323
|
-
|
|
2324
|
-
|
|
2567
|
+
-p, --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
|
|
2568
|
+
(aliases low, medium, high). It overrides a preset set in the config, and a
|
|
2569
|
+
numeric threshold set in the config overrides both.
|
|
2570
|
+
--profile NAME sets how contrast is read: ${PROFILES.join(", ")}. It overrides a profile set in the config.
|
|
2571
|
+
auto reads mkdocs-material when any parent folder holds mkdocs.yml or mkdocs.yaml, else github.
|
|
2325
2572
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
2326
2573
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
2327
2574
|
--config FILE reads that config instead of mermaidjs-lint.config.json or .mjs at the project root.
|
|
2328
|
-
Plugins load on their own from .mermaidjs-lint/rules
|
|
2329
|
-
|
|
2575
|
+
Plugins load on their own from .mermaidjs-lint/rules, from dependencies named mermaidjs-lint-plugin,
|
|
2576
|
+
mermaidjs-lint-plugin-* or @scope/mermaidjs-lint-plugin(-*), and from the config's plugins list.
|
|
2577
|
+
--no-plugins runs the built-in rules only and loads no plugin. It reads a .json config as usual,
|
|
2578
|
+
and stops with a usage error rather than import a .mjs config, which is code.
|
|
2330
2579
|
--lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
|
|
2331
|
-
--validate-plugins
|
|
2580
|
+
--validate-plugins imports and checks every plugin, reports every failure, and runs no check or fix.
|
|
2332
2581
|
--list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
|
|
2333
|
-
|
|
2582
|
+
It imports the plugins to list them.
|
|
2583
|
+
--version prints the version, to quote in a bug report.
|
|
2584
|
+
Exits 0 when no findings remain, 1 when findings remain, a plugin fails to load or the run fails (a lenient run
|
|
2585
|
+
skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
|
|
2334
2586
|
var parse2 = (argv) => parseArgs({
|
|
2335
2587
|
args: argv,
|
|
2336
2588
|
options: {
|
|
@@ -2343,7 +2595,8 @@ var parse2 = (argv) => parseArgs({
|
|
|
2343
2595
|
"no-plugins": { type: "boolean", default: false },
|
|
2344
2596
|
"lenient-plugins": { type: "boolean", default: false },
|
|
2345
2597
|
"validate-plugins": { type: "boolean", default: false },
|
|
2346
|
-
"list-rules": { type: "boolean", default: false }
|
|
2598
|
+
"list-rules": { type: "boolean", default: false },
|
|
2599
|
+
version: { type: "boolean", default: false }
|
|
2347
2600
|
},
|
|
2348
2601
|
allowPositionals: true,
|
|
2349
2602
|
strict: true
|
|
@@ -2364,13 +2617,14 @@ var skippedLines = (failures, ignored) => [
|
|
|
2364
2617
|
];
|
|
2365
2618
|
var listRules = (registry) => {
|
|
2366
2619
|
const lines = [];
|
|
2620
|
+
const width = idWidth(registry.rules);
|
|
2367
2621
|
for (const [category, description] of registry.categories) {
|
|
2368
2622
|
const rules = registry.rules.filter((r) => r.category === category);
|
|
2369
2623
|
if (rules.length === 0)
|
|
2370
2624
|
continue;
|
|
2371
2625
|
lines.push(`${category}: ${description}`);
|
|
2372
2626
|
for (const r of rules)
|
|
2373
|
-
lines.push(` ${r
|
|
2627
|
+
lines.push(` ${ruleRow(r, width)}`);
|
|
2374
2628
|
}
|
|
2375
2629
|
if (registry.sources.length > 0) {
|
|
2376
2630
|
lines.push("", "plugins:");
|
|
@@ -2405,10 +2659,25 @@ var validate = (registry, failures, json) => {
|
|
|
2405
2659
|
const line = sources.length === 0 ? "mermaidjs-lint: no plugins found" : `mermaidjs-lint: plugins valid: ${namesOf(registry.sources)}`;
|
|
2406
2660
|
return { code, stdout: line, stderr: [] };
|
|
2407
2661
|
};
|
|
2662
|
+
var reportLine = (f, file) => {
|
|
2663
|
+
const where = f.endLine > f.line ? `${f.line}-${f.endLine}` : `${f.line}`;
|
|
2664
|
+
return `${file}:${where} ${f.rule} ${f.message}`;
|
|
2665
|
+
};
|
|
2408
2666
|
var display = (file, cwd) => {
|
|
2409
2667
|
const rel = relative(cwd, file);
|
|
2410
2668
|
return !rel || rel.startsWith("..") ? file : rel;
|
|
2411
2669
|
};
|
|
2670
|
+
var refuseOutsideLinks = async (files, root, cwd) => {
|
|
2671
|
+
const realRoot = await realpath(root);
|
|
2672
|
+
for (const file of files) {
|
|
2673
|
+
if (!(await lstat(file)).isSymbolicLink())
|
|
2674
|
+
continue;
|
|
2675
|
+
const target = await realpath(file);
|
|
2676
|
+
const rel = relative(realRoot, target);
|
|
2677
|
+
if (rel.startsWith("..") || isAbsolute2(rel))
|
|
2678
|
+
throw new FixOutsideRootError(display(file, cwd), target, root);
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2412
2681
|
var run = async (argv, cwd) => {
|
|
2413
2682
|
let parsed;
|
|
2414
2683
|
try {
|
|
@@ -2421,6 +2690,10 @@ var run = async (argv, cwd) => {
|
|
|
2421
2690
|
console.log(USAGE);
|
|
2422
2691
|
return 0;
|
|
2423
2692
|
}
|
|
2693
|
+
if (values.version) {
|
|
2694
|
+
console.log(package_default.version);
|
|
2695
|
+
return 0;
|
|
2696
|
+
}
|
|
2424
2697
|
const listing = values["list-rules"];
|
|
2425
2698
|
const validating = values["validate-plugins"];
|
|
2426
2699
|
if (values.preset !== undefined && canonicalPreset(values.preset) === null) {
|
|
@@ -2440,7 +2713,7 @@ var run = async (argv, cwd) => {
|
|
|
2440
2713
|
}
|
|
2441
2714
|
if (positionals.length === 0 && !listing && !validating)
|
|
2442
2715
|
throw new FlagError("files", "no files given");
|
|
2443
|
-
const { registry, skipped, failures } = await setUp({
|
|
2716
|
+
const { root, registry, skipped, failures } = await setUp({
|
|
2444
2717
|
cwd,
|
|
2445
2718
|
configPath: values.config,
|
|
2446
2719
|
noPlugins: values["no-plugins"],
|
|
@@ -2464,11 +2737,16 @@ var run = async (argv, cwd) => {
|
|
|
2464
2737
|
console.log(listRules(registry));
|
|
2465
2738
|
return 0;
|
|
2466
2739
|
}
|
|
2467
|
-
const { files, unreadable } = collectFiles(positionals.map((p) =>
|
|
2740
|
+
const { files, unreadable, symlinks } = collectFiles(positionals.map((p) => resolve5(cwd, p)));
|
|
2468
2741
|
if (unreadable.length > 0)
|
|
2469
2742
|
throw new FlagError("files", `cannot read ${unreadable.join(", ")}`);
|
|
2743
|
+
for (const link of symlinks) {
|
|
2744
|
+
console.error(`mermaidjs-lint: skipped symbolic link ${display(link, cwd)}; name it as a path to check it`);
|
|
2745
|
+
}
|
|
2470
2746
|
if (files.length === 0)
|
|
2471
2747
|
throw new FlagError("files", "no matching files");
|
|
2748
|
+
if (values.fix)
|
|
2749
|
+
await refuseOutsideLinks(files, root, cwd);
|
|
2472
2750
|
const flags = { preset: values.preset, profile: values.profile };
|
|
2473
2751
|
const perFile = await Promise.all(files.map(async (file) => {
|
|
2474
2752
|
let src = await readFile3(file, "utf8");
|
|
@@ -2479,9 +2757,9 @@ var run = async (argv, cwd) => {
|
|
|
2479
2757
|
src = fixed;
|
|
2480
2758
|
}
|
|
2481
2759
|
}
|
|
2482
|
-
const
|
|
2483
|
-
const found = await checkModel(buildDocModel(src,
|
|
2484
|
-
return found;
|
|
2760
|
+
const shown = display(file, cwd);
|
|
2761
|
+
const found = await checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
|
|
2762
|
+
return found.map((f) => ({ ...f, file: shown }));
|
|
2485
2763
|
}));
|
|
2486
2764
|
const all = perFile.flat();
|
|
2487
2765
|
if (values.json) {
|
|
@@ -2493,10 +2771,8 @@ var run = async (argv, cwd) => {
|
|
|
2493
2771
|
}));
|
|
2494
2772
|
console.log(JSON.stringify({ findings: all, files: files.length, plugins: registry.sources, skippedPlugins }, null, 2));
|
|
2495
2773
|
} else {
|
|
2496
|
-
for (const f of all)
|
|
2497
|
-
|
|
2498
|
-
console.log(`${f.file}:${where} ${f.rule} ${f.message}`);
|
|
2499
|
-
}
|
|
2774
|
+
for (const f of all)
|
|
2775
|
+
console.log(reportLine(f, f.file));
|
|
2500
2776
|
console.log(`${all.length} finding(s) in ${files.length} file(s)`);
|
|
2501
2777
|
}
|
|
2502
2778
|
return all.length > 0 ? 1 : 0;
|
|
@@ -2509,7 +2785,8 @@ var main = async (argv = process.argv.slice(2), cwd = process.cwd()) => {
|
|
|
2509
2785
|
if (!(err instanceof UsageError))
|
|
2510
2786
|
throw err;
|
|
2511
2787
|
console.error(describeError(err));
|
|
2512
|
-
console.error(
|
|
2788
|
+
console.error(SYNOPSIS);
|
|
2789
|
+
console.error("run mermaidjs-lint --help for more");
|
|
2513
2790
|
return 2;
|
|
2514
2791
|
}
|
|
2515
2792
|
};
|