@jpeakai/mermaidjs-lint 0.3.0 → 0.4.1
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 +5 -4
- package/dist/bin.js +234 -196
- package/dist/index.js +234 -196
- package/dist/testing.js +129 -79
- package/dist/types/check.d.ts +1 -0
- package/dist/types/parse/stats.d.ts +1 -0
- package/package.json +1 -1
- package/src/check.ts +20 -0
- package/src/cli.ts +4 -8
- package/src/errors.ts +1 -1
- package/src/fix/engine.ts +2 -1
- package/src/fix/verify.ts +27 -4
- package/src/parse/metrics.ts +8 -1
- package/src/parse/stats.ts +33 -5
- package/src/rules/complexity-subgraph-nesting-too-deep.ts +5 -0
- package/src/rules/parse-fence-unterminated.ts +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@ Deterministic checks and verified fixes for Mermaid diagrams in markdown.
|
|
|
5
5
|
|
|
6
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
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
The docs site describes the latest release on npm.
|
|
9
|
+
This README, read on GitHub, describes the `main` branch.
|
|
10
|
+
A change on `main` that is not yet released is listed under `Unreleased` in the [changelog](CHANGELOG.md).
|
|
10
11
|
|
|
11
12
|
## Usage
|
|
12
13
|
|
|
@@ -40,14 +41,14 @@ Three tenets are guarantees about every diagram the tool passes, and the fourth
|
|
|
40
41
|
| Extensible | Local rules and plugin packages |
|
|
41
42
|
|
|
42
43
|
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
|
+
`bunx @jpeakai/mermaidjs-lint --list-rules` prints every rule.
|
|
44
45
|
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
46
|
|
|
46
47
|
Every finding fails the run, and a rule you do not want is switched off in the config.
|
|
47
48
|
Node, edge and subgraph counts come from mermaid's own parsers.
|
|
48
49
|
Style directives are read line by line, so a style joined to other statements by a semicolon is not seen.
|
|
49
50
|
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
|
+
The check holds the markdown outside the diagrams unchanged, and each diagram's type, node ids, subgraph names and counts, and whether it parses.
|
|
51
52
|
It does not compare edge endpoints, edge labels or node text, so review the diff a fix writes.
|
|
52
53
|
|
|
53
54
|
## Requirements and compatibility
|
package/dist/bin.js
CHANGED
|
@@ -9,7 +9,7 @@ import { parseArgs } from "node:util";
|
|
|
9
9
|
// package.json
|
|
10
10
|
var package_default = {
|
|
11
11
|
name: "@jpeakai/mermaidjs-lint",
|
|
12
|
-
version: "0.
|
|
12
|
+
version: "0.4.1",
|
|
13
13
|
type: "module",
|
|
14
14
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
15
15
|
license: "MIT",
|
|
@@ -79,6 +79,184 @@ var package_default = {
|
|
|
79
79
|
}
|
|
80
80
|
};
|
|
81
81
|
|
|
82
|
+
// src/errors.ts
|
|
83
|
+
class MermaidLintError extends Error {
|
|
84
|
+
code;
|
|
85
|
+
constructor(code, message) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.code = code;
|
|
88
|
+
this.name = new.target.name;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class UsageError extends MermaidLintError {
|
|
93
|
+
constructor(message, code = "usage") {
|
|
94
|
+
super(code, message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class FlagError extends UsageError {
|
|
99
|
+
flag;
|
|
100
|
+
constructor(flag, message) {
|
|
101
|
+
super(message, "flag");
|
|
102
|
+
this.flag = flag;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class ConfigFileError extends UsageError {
|
|
107
|
+
file;
|
|
108
|
+
constructor(file, message) {
|
|
109
|
+
super(`${file}: ${message}`, "config-file");
|
|
110
|
+
this.file = file;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class ConfigNotFoundError extends UsageError {
|
|
115
|
+
path;
|
|
116
|
+
constructor(path) {
|
|
117
|
+
super(`--config ${path}: no such file`, "config-not-found");
|
|
118
|
+
this.path = path;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class ConfigCodeError extends UsageError {
|
|
123
|
+
file;
|
|
124
|
+
constructor(file) {
|
|
125
|
+
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");
|
|
126
|
+
this.file = file;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class AmbiguousConfigError extends UsageError {
|
|
131
|
+
files;
|
|
132
|
+
constructor(root, files) {
|
|
133
|
+
super(`more than one config file at ${root}: ${files.join(", ")}`, "config-ambiguous");
|
|
134
|
+
this.files = files;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
class ConfigShapeError extends UsageError {
|
|
139
|
+
file;
|
|
140
|
+
key;
|
|
141
|
+
constructor(file, message, key = null) {
|
|
142
|
+
super(`${file}: ${message}`, "config-shape");
|
|
143
|
+
this.file = file;
|
|
144
|
+
this.key = key;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class RuleSettingError extends UsageError {
|
|
149
|
+
file;
|
|
150
|
+
rule;
|
|
151
|
+
constructor(file, rule, message) {
|
|
152
|
+
super(`${file}: rule "${rule}" ${message}`, "rule-setting");
|
|
153
|
+
this.file = file;
|
|
154
|
+
this.rule = rule;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class UnknownRuleError extends UsageError {
|
|
159
|
+
file;
|
|
160
|
+
rule;
|
|
161
|
+
known;
|
|
162
|
+
constructor(file, rule, known) {
|
|
163
|
+
super(`${file}: unknown rule "${rule}"; a retired or misspelt id cannot be switched off. Known rules: ${known.join(", ")}`, "unknown-rule");
|
|
164
|
+
this.file = file;
|
|
165
|
+
this.rule = rule;
|
|
166
|
+
this.known = known;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
class RuleOptionError extends UsageError {
|
|
171
|
+
file;
|
|
172
|
+
rule;
|
|
173
|
+
option;
|
|
174
|
+
constructor(file, rule, option, message) {
|
|
175
|
+
super(`${file}: ${message}`, "rule-option");
|
|
176
|
+
this.file = file;
|
|
177
|
+
this.rule = rule;
|
|
178
|
+
this.option = option;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class FixOutsideRootError extends UsageError {
|
|
183
|
+
file;
|
|
184
|
+
target;
|
|
185
|
+
constructor(file, target, root) {
|
|
186
|
+
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 run on the target path itself`, "fix-outside-root");
|
|
187
|
+
this.file = file;
|
|
188
|
+
this.target = target;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class PackageJsonError extends MermaidLintError {
|
|
193
|
+
file;
|
|
194
|
+
constructor(file) {
|
|
195
|
+
super("package-json", `${file} is not a JSON object; fix or remove it`);
|
|
196
|
+
this.file = file;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
class FixNotConvergingError extends MermaidLintError {
|
|
201
|
+
file;
|
|
202
|
+
rules;
|
|
203
|
+
constructor(file, passes, rules) {
|
|
204
|
+
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`);
|
|
205
|
+
this.file = file;
|
|
206
|
+
this.rules = rules;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class PluginLoadError extends MermaidLintError {
|
|
211
|
+
source;
|
|
212
|
+
constructor(source, reason, code = "plugin") {
|
|
213
|
+
super(code, `plugin ${source}: ${reason}`);
|
|
214
|
+
this.source = source;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class PluginImportError extends PluginLoadError {
|
|
219
|
+
constructor(source, reason) {
|
|
220
|
+
super(source, `could not be imported: ${reason}`, "plugin-import");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
class PluginContractError extends PluginLoadError {
|
|
225
|
+
constructor(source, reason) {
|
|
226
|
+
super(source, reason, "plugin-contract");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
class PluginConflictError extends PluginLoadError {
|
|
231
|
+
other;
|
|
232
|
+
constructor(source, other, reason) {
|
|
233
|
+
super(source, reason, "plugin-conflict");
|
|
234
|
+
this.other = other;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
class PluginNotInstalledError extends PluginLoadError {
|
|
239
|
+
root;
|
|
240
|
+
constructor(source, root) {
|
|
241
|
+
super(source, `is declared or listed but not installed; looked from ${root} upward. Install it, or remove it`, "plugin-not-installed");
|
|
242
|
+
this.root = root;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
class PluginSourceError extends PluginLoadError {
|
|
247
|
+
constructor(source, reason) {
|
|
248
|
+
super(source, reason, "plugin-source");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
class PluginFixerError extends MermaidLintError {
|
|
253
|
+
rule;
|
|
254
|
+
constructor(rule, reason) {
|
|
255
|
+
super("plugin-fixer", `plugin rule ${rule} ${reason}`);
|
|
256
|
+
this.rule = rule;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
82
260
|
// src/model.ts
|
|
83
261
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
84
262
|
import { frontmatterFromMarkdown } from "mdast-util-frontmatter";
|
|
@@ -281,6 +459,7 @@ async function extractStats(content) {
|
|
|
281
459
|
edges: 0,
|
|
282
460
|
subgraphs: 0,
|
|
283
461
|
max_subgraph_depth: 0,
|
|
462
|
+
max_tree_depth: 0,
|
|
284
463
|
node_ids: [],
|
|
285
464
|
subgraph_names: [],
|
|
286
465
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -330,6 +509,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
330
509
|
const subgraphNames = [];
|
|
331
510
|
let edges = 0;
|
|
332
511
|
let maxDepth = 0;
|
|
512
|
+
let treeDepth = 0;
|
|
333
513
|
switch (mapped) {
|
|
334
514
|
case "architecture": {
|
|
335
515
|
const services = ast.services ?? [];
|
|
@@ -376,7 +556,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
376
556
|
const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
|
|
377
557
|
nodeIds.push(...stats.labels);
|
|
378
558
|
edges = stats.edges;
|
|
379
|
-
|
|
559
|
+
treeDepth = stats.depth;
|
|
380
560
|
break;
|
|
381
561
|
}
|
|
382
562
|
case "treemap": {
|
|
@@ -384,7 +564,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
384
564
|
const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
|
|
385
565
|
nodeIds.push(...stats.labels);
|
|
386
566
|
edges = stats.edges;
|
|
387
|
-
|
|
567
|
+
treeDepth = stats.depth;
|
|
388
568
|
break;
|
|
389
569
|
}
|
|
390
570
|
case "wardley": {
|
|
@@ -404,6 +584,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
404
584
|
edges,
|
|
405
585
|
subgraphs: subgraphNames.length,
|
|
406
586
|
max_subgraph_depth: maxDepth,
|
|
587
|
+
max_tree_depth: treeDepth,
|
|
407
588
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
408
589
|
subgraph_names: subgraphNames,
|
|
409
590
|
parser_used: "langium",
|
|
@@ -454,12 +635,13 @@ async function extractCoreStats(content, keyword) {
|
|
|
454
635
|
if (!adapter) {
|
|
455
636
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
456
637
|
}
|
|
457
|
-
const { nodeIds, edges, subgraphNames, maxDepth } = adapter(diagram.db);
|
|
638
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
458
639
|
return {
|
|
459
640
|
nodes: nodeIds.length,
|
|
460
641
|
edges,
|
|
461
642
|
subgraphs: subgraphNames.length,
|
|
462
643
|
max_subgraph_depth: maxDepth,
|
|
644
|
+
max_tree_depth: treeDepth,
|
|
463
645
|
node_ids: [...nodeIds].sort(),
|
|
464
646
|
subgraph_names: subgraphNames,
|
|
465
647
|
parser_used: "mermaid-core",
|
|
@@ -470,6 +652,7 @@ async function extractCoreStats(content, keyword) {
|
|
|
470
652
|
var CORE_ADAPTERS = {
|
|
471
653
|
"flowchart-v2": adaptFlowchart,
|
|
472
654
|
flowchart: adaptFlowchart,
|
|
655
|
+
"flowchart-elk": adaptFlowchart,
|
|
473
656
|
graph: adaptFlowchart,
|
|
474
657
|
classDiagram: adaptClass,
|
|
475
658
|
"classDiagram-v2": adaptClass,
|
|
@@ -593,7 +776,13 @@ function adaptIshikawa(db) {
|
|
|
593
776
|
walk(c, depth + 1);
|
|
594
777
|
};
|
|
595
778
|
walk(root, 0);
|
|
596
|
-
return {
|
|
779
|
+
return {
|
|
780
|
+
nodeIds: labels,
|
|
781
|
+
edges: Math.max(0, labels.length - 1),
|
|
782
|
+
subgraphNames: [],
|
|
783
|
+
maxDepth: 0,
|
|
784
|
+
treeDepth: maxDepth
|
|
785
|
+
};
|
|
597
786
|
}
|
|
598
787
|
function adaptKanban(db) {
|
|
599
788
|
const data = callGetter(db, "getData");
|
|
@@ -621,7 +810,13 @@ function adaptMindmap(db) {
|
|
|
621
810
|
walk(c, depth + 1);
|
|
622
811
|
};
|
|
623
812
|
walk(root, 0);
|
|
624
|
-
return {
|
|
813
|
+
return {
|
|
814
|
+
nodeIds: labels,
|
|
815
|
+
edges: Math.max(0, labels.length - 1),
|
|
816
|
+
subgraphNames: [],
|
|
817
|
+
maxDepth: 0,
|
|
818
|
+
treeDepth: maxDepth
|
|
819
|
+
};
|
|
625
820
|
}
|
|
626
821
|
function adaptQuadrantChart(db) {
|
|
627
822
|
const data = callGetter(db, "getQuadrantData");
|
|
@@ -837,184 +1032,6 @@ var buildDocModel = (src, kind = "markdown") => {
|
|
|
837
1032
|
return { src, kind, tree, diagrams };
|
|
838
1033
|
};
|
|
839
1034
|
|
|
840
|
-
// src/errors.ts
|
|
841
|
-
class MermaidLintError extends Error {
|
|
842
|
-
code;
|
|
843
|
-
constructor(code, message) {
|
|
844
|
-
super(message);
|
|
845
|
-
this.code = code;
|
|
846
|
-
this.name = new.target.name;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
class UsageError extends MermaidLintError {
|
|
851
|
-
constructor(message, code = "usage") {
|
|
852
|
-
super(code, message);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
class FlagError extends UsageError {
|
|
857
|
-
flag;
|
|
858
|
-
constructor(flag, message) {
|
|
859
|
-
super(message, "flag");
|
|
860
|
-
this.flag = flag;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
class ConfigFileError extends UsageError {
|
|
865
|
-
file;
|
|
866
|
-
constructor(file, message) {
|
|
867
|
-
super(`${file}: ${message}`, "config-file");
|
|
868
|
-
this.file = file;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
class ConfigNotFoundError extends UsageError {
|
|
873
|
-
path;
|
|
874
|
-
constructor(path) {
|
|
875
|
-
super(`--config ${path}: no such file`, "config-not-found");
|
|
876
|
-
this.path = path;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
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
|
-
|
|
888
|
-
class AmbiguousConfigError extends UsageError {
|
|
889
|
-
files;
|
|
890
|
-
constructor(root, files) {
|
|
891
|
-
super(`more than one config file at ${root}: ${files.join(", ")}`, "config-ambiguous");
|
|
892
|
-
this.files = files;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
class ConfigShapeError extends UsageError {
|
|
897
|
-
file;
|
|
898
|
-
key;
|
|
899
|
-
constructor(file, message, key = null) {
|
|
900
|
-
super(`${file}: ${message}`, "config-shape");
|
|
901
|
-
this.file = file;
|
|
902
|
-
this.key = key;
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
class RuleSettingError extends UsageError {
|
|
907
|
-
file;
|
|
908
|
-
rule;
|
|
909
|
-
constructor(file, rule, message) {
|
|
910
|
-
super(`${file}: rule "${rule}" ${message}`, "rule-setting");
|
|
911
|
-
this.file = file;
|
|
912
|
-
this.rule = rule;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
class UnknownRuleError extends UsageError {
|
|
917
|
-
file;
|
|
918
|
-
rule;
|
|
919
|
-
known;
|
|
920
|
-
constructor(file, rule, known) {
|
|
921
|
-
super(`${file}: unknown rule "${rule}"; a retired or misspelt id cannot be switched off. Known rules: ${known.join(", ")}`, "unknown-rule");
|
|
922
|
-
this.file = file;
|
|
923
|
-
this.rule = rule;
|
|
924
|
-
this.known = known;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
class RuleOptionError extends UsageError {
|
|
929
|
-
file;
|
|
930
|
-
rule;
|
|
931
|
-
option;
|
|
932
|
-
constructor(file, rule, option, message) {
|
|
933
|
-
super(`${file}: ${message}`, "rule-option");
|
|
934
|
-
this.file = file;
|
|
935
|
-
this.rule = rule;
|
|
936
|
-
this.option = option;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
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
|
-
|
|
968
|
-
class PluginLoadError extends MermaidLintError {
|
|
969
|
-
source;
|
|
970
|
-
constructor(source, reason, code = "plugin") {
|
|
971
|
-
super(code, `plugin ${source}: ${reason}`);
|
|
972
|
-
this.source = source;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
class PluginImportError extends PluginLoadError {
|
|
977
|
-
constructor(source, reason) {
|
|
978
|
-
super(source, `could not be imported: ${reason}`, "plugin-import");
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
class PluginContractError extends PluginLoadError {
|
|
983
|
-
constructor(source, reason) {
|
|
984
|
-
super(source, reason, "plugin-contract");
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
class PluginConflictError extends PluginLoadError {
|
|
989
|
-
other;
|
|
990
|
-
constructor(source, other, reason) {
|
|
991
|
-
super(source, reason, "plugin-conflict");
|
|
992
|
-
this.other = other;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
class PluginNotInstalledError extends PluginLoadError {
|
|
997
|
-
root;
|
|
998
|
-
constructor(source, root) {
|
|
999
|
-
super(source, `is declared or listed but not installed; looked from ${root} upward. Install it, or remove it`, "plugin-not-installed");
|
|
1000
|
-
this.root = root;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
class PluginSourceError extends PluginLoadError {
|
|
1005
|
-
constructor(source, reason) {
|
|
1006
|
-
super(source, reason, "plugin-source");
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
class PluginFixerError extends MermaidLintError {
|
|
1011
|
-
rule;
|
|
1012
|
-
constructor(rule, reason) {
|
|
1013
|
-
super("plugin-fixer", `plugin rule ${rule} ${reason}`);
|
|
1014
|
-
this.rule = rule;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
1035
|
// src/parse/contrast.ts
|
|
1019
1036
|
import { existsSync } from "node:fs";
|
|
1020
1037
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -1356,7 +1373,7 @@ var calculateComplexity = (stats, config) => {
|
|
|
1356
1373
|
const n = stats.nodes;
|
|
1357
1374
|
const e = stats.edges;
|
|
1358
1375
|
const s = stats.subgraphs;
|
|
1359
|
-
const d = stats.max_subgraph_depth;
|
|
1376
|
+
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
1360
1377
|
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
1361
1378
|
const vcs = base * (1 + d * config.depth_weight);
|
|
1362
1379
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
|
@@ -1694,7 +1711,7 @@ var parseFenceUnterminated = {
|
|
|
1694
1711
|
id: RULE.FENCE_UNTERMINATED,
|
|
1695
1712
|
category: "parse",
|
|
1696
1713
|
summary: "a mermaid fence that is never closed",
|
|
1697
|
-
check: (ctx) => ctx.doc.diagrams.filter((d) => !d.terminated).map((d) => finding(ctx, d, RULE.FENCE_UNTERMINATED, `mermaid fence opened at line ${d.startLine} is never closed`, "Add a closing fence after the last line of the diagram.")),
|
|
1714
|
+
check: (ctx) => ctx.doc.diagrams.filter((d) => !d.terminated).map((d) => finding(ctx, d, RULE.FENCE_UNTERMINATED, `mermaid fence opened at line ${d.startLine} is never closed`, "Add a closing fence directly after the last line of the diagram, with the opening line's prefix: `> ` in a quote, the item's indent in a list.")),
|
|
1698
1715
|
fix: (ctx) => {
|
|
1699
1716
|
const edits = [];
|
|
1700
1717
|
const { src } = ctx.doc;
|
|
@@ -1810,6 +1827,15 @@ var buildRegistry = (input) => {
|
|
|
1810
1827
|
var BUILTIN = buildRegistry({ rules: RULES, fixOrder: FIX_ORDER, categories: BUILTIN_CATEGORIES });
|
|
1811
1828
|
|
|
1812
1829
|
// src/check.ts
|
|
1830
|
+
var checkRunFlags = (flags, prefix = "") => {
|
|
1831
|
+
const { preset, profile } = flags;
|
|
1832
|
+
if (preset !== undefined && canonicalPreset(preset) === null) {
|
|
1833
|
+
throw new FlagError(`${prefix}preset`, `${prefix}preset must be one of ${PRESET_NAMES.join(", ")}, not "${preset}"`);
|
|
1834
|
+
}
|
|
1835
|
+
if (profile !== undefined && !PROFILES.includes(profile)) {
|
|
1836
|
+
throw new FlagError(`${prefix}profile`, `${prefix}profile must be one of ${PROFILES.join(", ")}, not "${profile}"`);
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1813
1839
|
var optionsFor = (rule, configured, flags) => {
|
|
1814
1840
|
const options = { ...configured };
|
|
1815
1841
|
for (const name of ["preset", "profile"]) {
|
|
@@ -1820,6 +1846,7 @@ var optionsFor = (rule, configured, flags) => {
|
|
|
1820
1846
|
return options;
|
|
1821
1847
|
};
|
|
1822
1848
|
var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
|
|
1849
|
+
checkRunFlags(flags);
|
|
1823
1850
|
const perRule = registry.rules.map((rule) => rule.check({ doc, file, options: optionsFor(rule, registry.options.get(rule.id), flags), helpers: HELPERS }));
|
|
1824
1851
|
const findings = (await Promise.all(perRule)).flat();
|
|
1825
1852
|
return findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
|
|
@@ -1917,26 +1944,40 @@ var sameDiagrams = async (before, after) => {
|
|
|
1917
1944
|
if (!now || was.keyword !== now.keyword)
|
|
1918
1945
|
return false;
|
|
1919
1946
|
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1920
|
-
const shape = (s) => [
|
|
1947
|
+
const shape = (s, source) => [
|
|
1948
|
+
parseFailed(source, s),
|
|
1921
1949
|
s.diagram_type,
|
|
1922
1950
|
s.nodes,
|
|
1923
1951
|
s.edges,
|
|
1924
1952
|
s.subgraphs,
|
|
1925
1953
|
s.max_subgraph_depth,
|
|
1954
|
+
s.max_tree_depth,
|
|
1926
1955
|
s.node_ids,
|
|
1927
1956
|
s.subgraph_names
|
|
1928
1957
|
];
|
|
1929
|
-
if (!sameJson(shape(a), shape(b)))
|
|
1958
|
+
if (!sameJson(shape(a, was.source), shape(b, now.source)))
|
|
1930
1959
|
return false;
|
|
1931
1960
|
}
|
|
1932
1961
|
return true;
|
|
1933
1962
|
};
|
|
1963
|
+
var breaksAParse = async (before, after) => {
|
|
1964
|
+
for (const [i, was] of before.diagrams.entries()) {
|
|
1965
|
+
const now = after.diagrams[i];
|
|
1966
|
+
if (!now)
|
|
1967
|
+
continue;
|
|
1968
|
+
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1969
|
+
if (!parseFailed(was.source, a) && parseFailed(now.source, b))
|
|
1970
|
+
return true;
|
|
1971
|
+
}
|
|
1972
|
+
return false;
|
|
1973
|
+
};
|
|
1934
1974
|
var verify = async (edit, before, after) => {
|
|
1935
1975
|
if (edit.expect.kind === "same-tree") {
|
|
1936
1976
|
if (!sameTree(before.tree, after.tree, {}))
|
|
1937
1977
|
return "refuse";
|
|
1938
|
-
if (before.kind === "markdown" && after.kind === "markdown")
|
|
1939
|
-
return "accept";
|
|
1978
|
+
if (before.kind === "markdown" && after.kind === "markdown") {
|
|
1979
|
+
return await breaksAParse(before, after) ? "refuse" : "accept";
|
|
1980
|
+
}
|
|
1940
1981
|
}
|
|
1941
1982
|
return await sameDiagrams(before, after) ? "accept" : "refuse";
|
|
1942
1983
|
};
|
|
@@ -1956,6 +1997,7 @@ var overlaps = (edits) => {
|
|
|
1956
1997
|
};
|
|
1957
1998
|
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";
|
|
1958
1999
|
var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
2000
|
+
checkRunFlags(flags);
|
|
1959
2001
|
const kind = documentKind(file);
|
|
1960
2002
|
const applied = [];
|
|
1961
2003
|
const refused = [];
|
|
@@ -2569,6 +2611,7 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
|
|
|
2569
2611
|
numeric threshold set in the config overrides both.
|
|
2570
2612
|
--profile NAME sets how contrast is read: ${PROFILES.join(", ")}. It overrides a profile set in the config.
|
|
2571
2613
|
auto reads mkdocs-material when any parent folder holds mkdocs.yml or mkdocs.yaml, else github.
|
|
2614
|
+
--json prints one JSON object instead of text lines.
|
|
2572
2615
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
2573
2616
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
2574
2617
|
--config FILE reads that config instead of mermaidjs-lint.config.json or .mjs at the project root.
|
|
@@ -2696,12 +2739,7 @@ var run = async (argv, cwd) => {
|
|
|
2696
2739
|
}
|
|
2697
2740
|
const listing = values["list-rules"];
|
|
2698
2741
|
const validating = values["validate-plugins"];
|
|
2699
|
-
|
|
2700
|
-
throw new FlagError("--preset", `--preset must be one of ${PRESET_NAMES.join(", ")}, not "${values.preset}"`);
|
|
2701
|
-
}
|
|
2702
|
-
if (values.profile !== undefined && !PROFILES.includes(values.profile)) {
|
|
2703
|
-
throw new FlagError("--profile", `--profile must be one of ${PROFILES.join(", ")}, not "${values.profile}"`);
|
|
2704
|
-
}
|
|
2742
|
+
checkRunFlags({ preset: values.preset, profile: values.profile }, "--");
|
|
2705
2743
|
if (values["no-plugins"] && values["lenient-plugins"]) {
|
|
2706
2744
|
throw new FlagError("--lenient-plugins", "--no-plugins and --lenient-plugins contradict each other");
|
|
2707
2745
|
}
|