@jpeakai/mermaidjs-lint 0.3.0 → 0.4.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 +5 -4
- package/dist/bin.js +44 -13
- package/dist/index.js +44 -13
- package/dist/testing.js +42 -11
- package/dist/types/parse/stats.d.ts +1 -0
- package/package.json +1 -1
- package/src/errors.ts +1 -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.0",
|
|
13
13
|
type: "module",
|
|
14
14
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
15
15
|
license: "MIT",
|
|
@@ -281,6 +281,7 @@ async function extractStats(content) {
|
|
|
281
281
|
edges: 0,
|
|
282
282
|
subgraphs: 0,
|
|
283
283
|
max_subgraph_depth: 0,
|
|
284
|
+
max_tree_depth: 0,
|
|
284
285
|
node_ids: [],
|
|
285
286
|
subgraph_names: [],
|
|
286
287
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -330,6 +331,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
330
331
|
const subgraphNames = [];
|
|
331
332
|
let edges = 0;
|
|
332
333
|
let maxDepth = 0;
|
|
334
|
+
let treeDepth = 0;
|
|
333
335
|
switch (mapped) {
|
|
334
336
|
case "architecture": {
|
|
335
337
|
const services = ast.services ?? [];
|
|
@@ -376,7 +378,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
376
378
|
const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
|
|
377
379
|
nodeIds.push(...stats.labels);
|
|
378
380
|
edges = stats.edges;
|
|
379
|
-
|
|
381
|
+
treeDepth = stats.depth;
|
|
380
382
|
break;
|
|
381
383
|
}
|
|
382
384
|
case "treemap": {
|
|
@@ -384,7 +386,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
384
386
|
const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
|
|
385
387
|
nodeIds.push(...stats.labels);
|
|
386
388
|
edges = stats.edges;
|
|
387
|
-
|
|
389
|
+
treeDepth = stats.depth;
|
|
388
390
|
break;
|
|
389
391
|
}
|
|
390
392
|
case "wardley": {
|
|
@@ -404,6 +406,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
404
406
|
edges,
|
|
405
407
|
subgraphs: subgraphNames.length,
|
|
406
408
|
max_subgraph_depth: maxDepth,
|
|
409
|
+
max_tree_depth: treeDepth,
|
|
407
410
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
408
411
|
subgraph_names: subgraphNames,
|
|
409
412
|
parser_used: "langium",
|
|
@@ -454,12 +457,13 @@ async function extractCoreStats(content, keyword) {
|
|
|
454
457
|
if (!adapter) {
|
|
455
458
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
456
459
|
}
|
|
457
|
-
const { nodeIds, edges, subgraphNames, maxDepth } = adapter(diagram.db);
|
|
460
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
458
461
|
return {
|
|
459
462
|
nodes: nodeIds.length,
|
|
460
463
|
edges,
|
|
461
464
|
subgraphs: subgraphNames.length,
|
|
462
465
|
max_subgraph_depth: maxDepth,
|
|
466
|
+
max_tree_depth: treeDepth,
|
|
463
467
|
node_ids: [...nodeIds].sort(),
|
|
464
468
|
subgraph_names: subgraphNames,
|
|
465
469
|
parser_used: "mermaid-core",
|
|
@@ -470,6 +474,7 @@ async function extractCoreStats(content, keyword) {
|
|
|
470
474
|
var CORE_ADAPTERS = {
|
|
471
475
|
"flowchart-v2": adaptFlowchart,
|
|
472
476
|
flowchart: adaptFlowchart,
|
|
477
|
+
"flowchart-elk": adaptFlowchart,
|
|
473
478
|
graph: adaptFlowchart,
|
|
474
479
|
classDiagram: adaptClass,
|
|
475
480
|
"classDiagram-v2": adaptClass,
|
|
@@ -593,7 +598,13 @@ function adaptIshikawa(db) {
|
|
|
593
598
|
walk(c, depth + 1);
|
|
594
599
|
};
|
|
595
600
|
walk(root, 0);
|
|
596
|
-
return {
|
|
601
|
+
return {
|
|
602
|
+
nodeIds: labels,
|
|
603
|
+
edges: Math.max(0, labels.length - 1),
|
|
604
|
+
subgraphNames: [],
|
|
605
|
+
maxDepth: 0,
|
|
606
|
+
treeDepth: maxDepth
|
|
607
|
+
};
|
|
597
608
|
}
|
|
598
609
|
function adaptKanban(db) {
|
|
599
610
|
const data = callGetter(db, "getData");
|
|
@@ -621,7 +632,13 @@ function adaptMindmap(db) {
|
|
|
621
632
|
walk(c, depth + 1);
|
|
622
633
|
};
|
|
623
634
|
walk(root, 0);
|
|
624
|
-
return {
|
|
635
|
+
return {
|
|
636
|
+
nodeIds: labels,
|
|
637
|
+
edges: Math.max(0, labels.length - 1),
|
|
638
|
+
subgraphNames: [],
|
|
639
|
+
maxDepth: 0,
|
|
640
|
+
treeDepth: maxDepth
|
|
641
|
+
};
|
|
625
642
|
}
|
|
626
643
|
function adaptQuadrantChart(db) {
|
|
627
644
|
const data = callGetter(db, "getQuadrantData");
|
|
@@ -941,7 +958,7 @@ class FixOutsideRootError extends UsageError {
|
|
|
941
958
|
file;
|
|
942
959
|
target;
|
|
943
960
|
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
|
|
961
|
+
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");
|
|
945
962
|
this.file = file;
|
|
946
963
|
this.target = target;
|
|
947
964
|
}
|
|
@@ -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;
|
|
@@ -1917,26 +1934,40 @@ var sameDiagrams = async (before, after) => {
|
|
|
1917
1934
|
if (!now || was.keyword !== now.keyword)
|
|
1918
1935
|
return false;
|
|
1919
1936
|
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1920
|
-
const shape = (s) => [
|
|
1937
|
+
const shape = (s, source) => [
|
|
1938
|
+
parseFailed(source, s),
|
|
1921
1939
|
s.diagram_type,
|
|
1922
1940
|
s.nodes,
|
|
1923
1941
|
s.edges,
|
|
1924
1942
|
s.subgraphs,
|
|
1925
1943
|
s.max_subgraph_depth,
|
|
1944
|
+
s.max_tree_depth,
|
|
1926
1945
|
s.node_ids,
|
|
1927
1946
|
s.subgraph_names
|
|
1928
1947
|
];
|
|
1929
|
-
if (!sameJson(shape(a), shape(b)))
|
|
1948
|
+
if (!sameJson(shape(a, was.source), shape(b, now.source)))
|
|
1930
1949
|
return false;
|
|
1931
1950
|
}
|
|
1932
1951
|
return true;
|
|
1933
1952
|
};
|
|
1953
|
+
var breaksAParse = async (before, after) => {
|
|
1954
|
+
for (const [i, was] of before.diagrams.entries()) {
|
|
1955
|
+
const now = after.diagrams[i];
|
|
1956
|
+
if (!now)
|
|
1957
|
+
continue;
|
|
1958
|
+
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1959
|
+
if (!parseFailed(was.source, a) && parseFailed(now.source, b))
|
|
1960
|
+
return true;
|
|
1961
|
+
}
|
|
1962
|
+
return false;
|
|
1963
|
+
};
|
|
1934
1964
|
var verify = async (edit, before, after) => {
|
|
1935
1965
|
if (edit.expect.kind === "same-tree") {
|
|
1936
1966
|
if (!sameTree(before.tree, after.tree, {}))
|
|
1937
1967
|
return "refuse";
|
|
1938
|
-
if (before.kind === "markdown" && after.kind === "markdown")
|
|
1939
|
-
return "accept";
|
|
1968
|
+
if (before.kind === "markdown" && after.kind === "markdown") {
|
|
1969
|
+
return await breaksAParse(before, after) ? "refuse" : "accept";
|
|
1970
|
+
}
|
|
1940
1971
|
}
|
|
1941
1972
|
return await sameDiagrams(before, after) ? "accept" : "refuse";
|
|
1942
1973
|
};
|
package/dist/index.js
CHANGED
|
@@ -203,6 +203,7 @@ async function extractStats(content) {
|
|
|
203
203
|
edges: 0,
|
|
204
204
|
subgraphs: 0,
|
|
205
205
|
max_subgraph_depth: 0,
|
|
206
|
+
max_tree_depth: 0,
|
|
206
207
|
node_ids: [],
|
|
207
208
|
subgraph_names: [],
|
|
208
209
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -252,6 +253,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
252
253
|
const subgraphNames = [];
|
|
253
254
|
let edges = 0;
|
|
254
255
|
let maxDepth = 0;
|
|
256
|
+
let treeDepth = 0;
|
|
255
257
|
switch (mapped) {
|
|
256
258
|
case "architecture": {
|
|
257
259
|
const services = ast.services ?? [];
|
|
@@ -298,7 +300,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
298
300
|
const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
|
|
299
301
|
nodeIds.push(...stats.labels);
|
|
300
302
|
edges = stats.edges;
|
|
301
|
-
|
|
303
|
+
treeDepth = stats.depth;
|
|
302
304
|
break;
|
|
303
305
|
}
|
|
304
306
|
case "treemap": {
|
|
@@ -306,7 +308,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
306
308
|
const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
|
|
307
309
|
nodeIds.push(...stats.labels);
|
|
308
310
|
edges = stats.edges;
|
|
309
|
-
|
|
311
|
+
treeDepth = stats.depth;
|
|
310
312
|
break;
|
|
311
313
|
}
|
|
312
314
|
case "wardley": {
|
|
@@ -326,6 +328,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
326
328
|
edges,
|
|
327
329
|
subgraphs: subgraphNames.length,
|
|
328
330
|
max_subgraph_depth: maxDepth,
|
|
331
|
+
max_tree_depth: treeDepth,
|
|
329
332
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
330
333
|
subgraph_names: subgraphNames,
|
|
331
334
|
parser_used: "langium",
|
|
@@ -376,12 +379,13 @@ async function extractCoreStats(content, keyword) {
|
|
|
376
379
|
if (!adapter) {
|
|
377
380
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
378
381
|
}
|
|
379
|
-
const { nodeIds, edges, subgraphNames, maxDepth } = adapter(diagram.db);
|
|
382
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
380
383
|
return {
|
|
381
384
|
nodes: nodeIds.length,
|
|
382
385
|
edges,
|
|
383
386
|
subgraphs: subgraphNames.length,
|
|
384
387
|
max_subgraph_depth: maxDepth,
|
|
388
|
+
max_tree_depth: treeDepth,
|
|
385
389
|
node_ids: [...nodeIds].sort(),
|
|
386
390
|
subgraph_names: subgraphNames,
|
|
387
391
|
parser_used: "mermaid-core",
|
|
@@ -392,6 +396,7 @@ async function extractCoreStats(content, keyword) {
|
|
|
392
396
|
var CORE_ADAPTERS = {
|
|
393
397
|
"flowchart-v2": adaptFlowchart,
|
|
394
398
|
flowchart: adaptFlowchart,
|
|
399
|
+
"flowchart-elk": adaptFlowchart,
|
|
395
400
|
graph: adaptFlowchart,
|
|
396
401
|
classDiagram: adaptClass,
|
|
397
402
|
"classDiagram-v2": adaptClass,
|
|
@@ -515,7 +520,13 @@ function adaptIshikawa(db) {
|
|
|
515
520
|
walk(c, depth + 1);
|
|
516
521
|
};
|
|
517
522
|
walk(root, 0);
|
|
518
|
-
return {
|
|
523
|
+
return {
|
|
524
|
+
nodeIds: labels,
|
|
525
|
+
edges: Math.max(0, labels.length - 1),
|
|
526
|
+
subgraphNames: [],
|
|
527
|
+
maxDepth: 0,
|
|
528
|
+
treeDepth: maxDepth
|
|
529
|
+
};
|
|
519
530
|
}
|
|
520
531
|
function adaptKanban(db) {
|
|
521
532
|
const data = callGetter(db, "getData");
|
|
@@ -543,7 +554,13 @@ function adaptMindmap(db) {
|
|
|
543
554
|
walk(c, depth + 1);
|
|
544
555
|
};
|
|
545
556
|
walk(root, 0);
|
|
546
|
-
return {
|
|
557
|
+
return {
|
|
558
|
+
nodeIds: labels,
|
|
559
|
+
edges: Math.max(0, labels.length - 1),
|
|
560
|
+
subgraphNames: [],
|
|
561
|
+
maxDepth: 0,
|
|
562
|
+
treeDepth: maxDepth
|
|
563
|
+
};
|
|
547
564
|
}
|
|
548
565
|
function adaptQuadrantChart(db) {
|
|
549
566
|
const data = callGetter(db, "getQuadrantData");
|
|
@@ -863,7 +880,7 @@ class FixOutsideRootError extends UsageError {
|
|
|
863
880
|
file;
|
|
864
881
|
target;
|
|
865
882
|
constructor(file, target, root) {
|
|
866
|
-
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
|
|
883
|
+
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");
|
|
867
884
|
this.file = file;
|
|
868
885
|
this.target = target;
|
|
869
886
|
}
|
|
@@ -1278,7 +1295,7 @@ var calculateComplexity = (stats, config) => {
|
|
|
1278
1295
|
const n = stats.nodes;
|
|
1279
1296
|
const e = stats.edges;
|
|
1280
1297
|
const s = stats.subgraphs;
|
|
1281
|
-
const d = stats.max_subgraph_depth;
|
|
1298
|
+
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
1282
1299
|
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
1283
1300
|
const vcs = base * (1 + d * config.depth_weight);
|
|
1284
1301
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
|
@@ -1616,7 +1633,7 @@ var parseFenceUnterminated = {
|
|
|
1616
1633
|
id: RULE.FENCE_UNTERMINATED,
|
|
1617
1634
|
category: "parse",
|
|
1618
1635
|
summary: "a mermaid fence that is never closed",
|
|
1619
|
-
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.")),
|
|
1636
|
+
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.")),
|
|
1620
1637
|
fix: (ctx) => {
|
|
1621
1638
|
const edits = [];
|
|
1622
1639
|
const { src } = ctx.doc;
|
|
@@ -1754,7 +1771,7 @@ import { parseArgs } from "node:util";
|
|
|
1754
1771
|
// package.json
|
|
1755
1772
|
var package_default = {
|
|
1756
1773
|
name: "@jpeakai/mermaidjs-lint",
|
|
1757
|
-
version: "0.
|
|
1774
|
+
version: "0.4.0",
|
|
1758
1775
|
type: "module",
|
|
1759
1776
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
1760
1777
|
license: "MIT",
|
|
@@ -1916,26 +1933,40 @@ var sameDiagrams = async (before, after) => {
|
|
|
1916
1933
|
if (!now || was.keyword !== now.keyword)
|
|
1917
1934
|
return false;
|
|
1918
1935
|
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1919
|
-
const shape = (s) => [
|
|
1936
|
+
const shape = (s, source) => [
|
|
1937
|
+
parseFailed(source, s),
|
|
1920
1938
|
s.diagram_type,
|
|
1921
1939
|
s.nodes,
|
|
1922
1940
|
s.edges,
|
|
1923
1941
|
s.subgraphs,
|
|
1924
1942
|
s.max_subgraph_depth,
|
|
1943
|
+
s.max_tree_depth,
|
|
1925
1944
|
s.node_ids,
|
|
1926
1945
|
s.subgraph_names
|
|
1927
1946
|
];
|
|
1928
|
-
if (!sameJson(shape(a), shape(b)))
|
|
1947
|
+
if (!sameJson(shape(a, was.source), shape(b, now.source)))
|
|
1929
1948
|
return false;
|
|
1930
1949
|
}
|
|
1931
1950
|
return true;
|
|
1932
1951
|
};
|
|
1952
|
+
var breaksAParse = async (before, after) => {
|
|
1953
|
+
for (const [i, was] of before.diagrams.entries()) {
|
|
1954
|
+
const now = after.diagrams[i];
|
|
1955
|
+
if (!now)
|
|
1956
|
+
continue;
|
|
1957
|
+
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1958
|
+
if (!parseFailed(was.source, a) && parseFailed(now.source, b))
|
|
1959
|
+
return true;
|
|
1960
|
+
}
|
|
1961
|
+
return false;
|
|
1962
|
+
};
|
|
1933
1963
|
var verify = async (edit, before, after) => {
|
|
1934
1964
|
if (edit.expect.kind === "same-tree") {
|
|
1935
1965
|
if (!sameTree(before.tree, after.tree, {}))
|
|
1936
1966
|
return "refuse";
|
|
1937
|
-
if (before.kind === "markdown" && after.kind === "markdown")
|
|
1938
|
-
return "accept";
|
|
1967
|
+
if (before.kind === "markdown" && after.kind === "markdown") {
|
|
1968
|
+
return await breaksAParse(before, after) ? "refuse" : "accept";
|
|
1969
|
+
}
|
|
1939
1970
|
}
|
|
1940
1971
|
return await sameDiagrams(before, after) ? "accept" : "refuse";
|
|
1941
1972
|
};
|
package/dist/testing.js
CHANGED
|
@@ -203,6 +203,7 @@ async function extractStats(content) {
|
|
|
203
203
|
edges: 0,
|
|
204
204
|
subgraphs: 0,
|
|
205
205
|
max_subgraph_depth: 0,
|
|
206
|
+
max_tree_depth: 0,
|
|
206
207
|
node_ids: [],
|
|
207
208
|
subgraph_names: [],
|
|
208
209
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -252,6 +253,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
252
253
|
const subgraphNames = [];
|
|
253
254
|
let edges = 0;
|
|
254
255
|
let maxDepth = 0;
|
|
256
|
+
let treeDepth = 0;
|
|
255
257
|
switch (mapped) {
|
|
256
258
|
case "architecture": {
|
|
257
259
|
const services = ast.services ?? [];
|
|
@@ -298,7 +300,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
298
300
|
const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
|
|
299
301
|
nodeIds.push(...stats.labels);
|
|
300
302
|
edges = stats.edges;
|
|
301
|
-
|
|
303
|
+
treeDepth = stats.depth;
|
|
302
304
|
break;
|
|
303
305
|
}
|
|
304
306
|
case "treemap": {
|
|
@@ -306,7 +308,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
306
308
|
const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
|
|
307
309
|
nodeIds.push(...stats.labels);
|
|
308
310
|
edges = stats.edges;
|
|
309
|
-
|
|
311
|
+
treeDepth = stats.depth;
|
|
310
312
|
break;
|
|
311
313
|
}
|
|
312
314
|
case "wardley": {
|
|
@@ -326,6 +328,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
326
328
|
edges,
|
|
327
329
|
subgraphs: subgraphNames.length,
|
|
328
330
|
max_subgraph_depth: maxDepth,
|
|
331
|
+
max_tree_depth: treeDepth,
|
|
329
332
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
330
333
|
subgraph_names: subgraphNames,
|
|
331
334
|
parser_used: "langium",
|
|
@@ -376,12 +379,13 @@ async function extractCoreStats(content, keyword) {
|
|
|
376
379
|
if (!adapter) {
|
|
377
380
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
378
381
|
}
|
|
379
|
-
const { nodeIds, edges, subgraphNames, maxDepth } = adapter(diagram.db);
|
|
382
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
380
383
|
return {
|
|
381
384
|
nodes: nodeIds.length,
|
|
382
385
|
edges,
|
|
383
386
|
subgraphs: subgraphNames.length,
|
|
384
387
|
max_subgraph_depth: maxDepth,
|
|
388
|
+
max_tree_depth: treeDepth,
|
|
385
389
|
node_ids: [...nodeIds].sort(),
|
|
386
390
|
subgraph_names: subgraphNames,
|
|
387
391
|
parser_used: "mermaid-core",
|
|
@@ -392,6 +396,7 @@ async function extractCoreStats(content, keyword) {
|
|
|
392
396
|
var CORE_ADAPTERS = {
|
|
393
397
|
"flowchart-v2": adaptFlowchart,
|
|
394
398
|
flowchart: adaptFlowchart,
|
|
399
|
+
"flowchart-elk": adaptFlowchart,
|
|
395
400
|
graph: adaptFlowchart,
|
|
396
401
|
classDiagram: adaptClass,
|
|
397
402
|
"classDiagram-v2": adaptClass,
|
|
@@ -515,7 +520,13 @@ function adaptIshikawa(db) {
|
|
|
515
520
|
walk(c, depth + 1);
|
|
516
521
|
};
|
|
517
522
|
walk(root, 0);
|
|
518
|
-
return {
|
|
523
|
+
return {
|
|
524
|
+
nodeIds: labels,
|
|
525
|
+
edges: Math.max(0, labels.length - 1),
|
|
526
|
+
subgraphNames: [],
|
|
527
|
+
maxDepth: 0,
|
|
528
|
+
treeDepth: maxDepth
|
|
529
|
+
};
|
|
519
530
|
}
|
|
520
531
|
function adaptKanban(db) {
|
|
521
532
|
const data = callGetter(db, "getData");
|
|
@@ -543,7 +554,13 @@ function adaptMindmap(db) {
|
|
|
543
554
|
walk(c, depth + 1);
|
|
544
555
|
};
|
|
545
556
|
walk(root, 0);
|
|
546
|
-
return {
|
|
557
|
+
return {
|
|
558
|
+
nodeIds: labels,
|
|
559
|
+
edges: Math.max(0, labels.length - 1),
|
|
560
|
+
subgraphNames: [],
|
|
561
|
+
maxDepth: 0,
|
|
562
|
+
treeDepth: maxDepth
|
|
563
|
+
};
|
|
547
564
|
}
|
|
548
565
|
function adaptQuadrantChart(db) {
|
|
549
566
|
const data = callGetter(db, "getQuadrantData");
|
|
@@ -1168,7 +1185,7 @@ var calculateComplexity = (stats, config) => {
|
|
|
1168
1185
|
const n = stats.nodes;
|
|
1169
1186
|
const e = stats.edges;
|
|
1170
1187
|
const s = stats.subgraphs;
|
|
1171
|
-
const d = stats.max_subgraph_depth;
|
|
1188
|
+
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
1172
1189
|
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
1173
1190
|
const vcs = base * (1 + d * config.depth_weight);
|
|
1174
1191
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
|
@@ -1505,7 +1522,7 @@ var parseFenceUnterminated = {
|
|
|
1505
1522
|
id: RULE.FENCE_UNTERMINATED,
|
|
1506
1523
|
category: "parse",
|
|
1507
1524
|
summary: "a mermaid fence that is never closed",
|
|
1508
|
-
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.")),
|
|
1525
|
+
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.")),
|
|
1509
1526
|
fix: (ctx) => {
|
|
1510
1527
|
const edits = [];
|
|
1511
1528
|
const { src } = ctx.doc;
|
|
@@ -1669,26 +1686,40 @@ var sameDiagrams = async (before, after) => {
|
|
|
1669
1686
|
if (!now || was.keyword !== now.keyword)
|
|
1670
1687
|
return false;
|
|
1671
1688
|
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1672
|
-
const shape = (s) => [
|
|
1689
|
+
const shape = (s, source) => [
|
|
1690
|
+
parseFailed(source, s),
|
|
1673
1691
|
s.diagram_type,
|
|
1674
1692
|
s.nodes,
|
|
1675
1693
|
s.edges,
|
|
1676
1694
|
s.subgraphs,
|
|
1677
1695
|
s.max_subgraph_depth,
|
|
1696
|
+
s.max_tree_depth,
|
|
1678
1697
|
s.node_ids,
|
|
1679
1698
|
s.subgraph_names
|
|
1680
1699
|
];
|
|
1681
|
-
if (!sameJson(shape(a), shape(b)))
|
|
1700
|
+
if (!sameJson(shape(a, was.source), shape(b, now.source)))
|
|
1682
1701
|
return false;
|
|
1683
1702
|
}
|
|
1684
1703
|
return true;
|
|
1685
1704
|
};
|
|
1705
|
+
var breaksAParse = async (before, after) => {
|
|
1706
|
+
for (const [i, was] of before.diagrams.entries()) {
|
|
1707
|
+
const now = after.diagrams[i];
|
|
1708
|
+
if (!now)
|
|
1709
|
+
continue;
|
|
1710
|
+
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1711
|
+
if (!parseFailed(was.source, a) && parseFailed(now.source, b))
|
|
1712
|
+
return true;
|
|
1713
|
+
}
|
|
1714
|
+
return false;
|
|
1715
|
+
};
|
|
1686
1716
|
var verify = async (edit, before, after) => {
|
|
1687
1717
|
if (edit.expect.kind === "same-tree") {
|
|
1688
1718
|
if (!sameTree(before.tree, after.tree, {}))
|
|
1689
1719
|
return "refuse";
|
|
1690
|
-
if (before.kind === "markdown" && after.kind === "markdown")
|
|
1691
|
-
return "accept";
|
|
1720
|
+
if (before.kind === "markdown" && after.kind === "markdown") {
|
|
1721
|
+
return await breaksAParse(before, after) ? "refuse" : "accept";
|
|
1722
|
+
}
|
|
1692
1723
|
}
|
|
1693
1724
|
return await sameDiagrams(before, after) ? "accept" : "refuse";
|
|
1694
1725
|
};
|
package/package.json
CHANGED
package/src/errors.ts
CHANGED
|
@@ -138,7 +138,7 @@ export class FixOutsideRootError extends UsageError {
|
|
|
138
138
|
|
|
139
139
|
constructor(file: string, target: string, root: string) {
|
|
140
140
|
super(
|
|
141
|
-
`${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
|
|
141
|
+
`${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`,
|
|
142
142
|
"fix-outside-root",
|
|
143
143
|
);
|
|
144
144
|
this.file = file;
|
package/src/fix/verify.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Node, Root } from "mdast";
|
|
6
6
|
import type { DocModel } from "../model.ts";
|
|
7
|
+
import { parseFailed } from "../parse/metrics.ts";
|
|
7
8
|
import type { Edit } from "../rules/types.ts";
|
|
8
9
|
|
|
9
10
|
type Plain = { [k: string]: unknown };
|
|
@@ -43,28 +44,50 @@ const sameDiagrams = async (before: DocModel, after: DocModel): Promise<boolean>
|
|
|
43
44
|
const now = after.diagrams[i];
|
|
44
45
|
if (!now || was.keyword !== now.keyword) return false;
|
|
45
46
|
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
46
|
-
const shape = (s: typeof a): unknown => [
|
|
47
|
+
const shape = (s: typeof a, source: string): unknown => [
|
|
48
|
+
parseFailed(source, s),
|
|
47
49
|
s.diagram_type,
|
|
48
50
|
s.nodes,
|
|
49
51
|
s.edges,
|
|
50
52
|
s.subgraphs,
|
|
51
53
|
s.max_subgraph_depth,
|
|
54
|
+
s.max_tree_depth,
|
|
52
55
|
s.node_ids,
|
|
53
56
|
s.subgraph_names,
|
|
54
57
|
];
|
|
55
|
-
if (!sameJson(shape(a), shape(b))) return false;
|
|
58
|
+
if (!sameJson(shape(a, was.source), shape(b, now.source))) return false;
|
|
56
59
|
}
|
|
57
60
|
return true;
|
|
58
61
|
};
|
|
59
62
|
|
|
63
|
+
// Whether a diagram that parsed before the edit fails to parse after it. The
|
|
64
|
+
// diagrams are matched by position, which same-tree's identical tree already
|
|
65
|
+
// pins. Asked of same-tree edits as a belt to the tree comparison's braces: a
|
|
66
|
+
// diagram's text sits in the tree, so an unchanged tree cannot break it today,
|
|
67
|
+
// but no edit may ever trade a parseable diagram for an unparseable one.
|
|
68
|
+
const breaksAParse = async (before: DocModel, after: DocModel): Promise<boolean> => {
|
|
69
|
+
for (const [i, was] of before.diagrams.entries()) {
|
|
70
|
+
const now = after.diagrams[i];
|
|
71
|
+
if (!now) continue;
|
|
72
|
+
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
73
|
+
if (!parseFailed(was.source, a) && parseFailed(now.source, b)) return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
};
|
|
77
|
+
|
|
60
78
|
// A raw diagram file (.mmd, .mermaid) has no markdown around its diagram, so
|
|
61
79
|
// its tree is an empty root and comparing trees would prove nothing. There,
|
|
62
80
|
// same-tree also demands what same-diagrams does: the diagram is still there
|
|
63
|
-
// and parses to the same structure.
|
|
81
|
+
// and parses to the same structure. same-diagrams compares parse success too
|
|
82
|
+
// (parseFailed, the test parse-diagram-unparseable applies), so an edit can
|
|
83
|
+
// neither break a diagram that parsed nor claim to have kept the diagrams the
|
|
84
|
+
// same while making an unparseable one parse.
|
|
64
85
|
export const verify = async (edit: Edit, before: DocModel, after: DocModel): Promise<Verdict> => {
|
|
65
86
|
if (edit.expect.kind === "same-tree") {
|
|
66
87
|
if (!sameTree(before.tree, after.tree, {})) return "refuse";
|
|
67
|
-
if (before.kind === "markdown" && after.kind === "markdown")
|
|
88
|
+
if (before.kind === "markdown" && after.kind === "markdown") {
|
|
89
|
+
return (await breaksAParse(before, after)) ? "refuse" : "accept";
|
|
90
|
+
}
|
|
68
91
|
}
|
|
69
92
|
return (await sameDiagrams(before, after)) ? "accept" : "refuse";
|
|
70
93
|
};
|
package/src/parse/metrics.ts
CHANGED
|
@@ -152,11 +152,18 @@ const round4 = (x: number): number => Math.round(x * 10000) / 10000;
|
|
|
152
152
|
// (nodes + edges*w_e + subgraphs*w_s) * (1 + depth*w_d). Edge density is
|
|
153
153
|
// reported as 0 below two nodes rather than dividing by a fallback, which gave
|
|
154
154
|
// nonsense when a parser returned edges but no nodes.
|
|
155
|
+
//
|
|
156
|
+
// The depth is subgraph nesting or, for a tree-shaped type (mindmap, treemap,
|
|
157
|
+
// treeView, ishikawa), tree depth; a diagram has only one of the two. A deep
|
|
158
|
+
// tree is as hard to follow as deep nesting, so its depth still weighs on the
|
|
159
|
+
// score, and the score of those types is unchanged from when tree depth was
|
|
160
|
+
// counted as nesting. Only complexity-subgraph-nesting-too-deep ignores it,
|
|
161
|
+
// because its remedy (flatten subgraphs) cannot apply to a tree.
|
|
155
162
|
export const calculateComplexity = (stats: DiagramStats, config: ThresholdConfig): ComplexityMetrics => {
|
|
156
163
|
const n = stats.nodes;
|
|
157
164
|
const e = stats.edges;
|
|
158
165
|
const s = stats.subgraphs;
|
|
159
|
-
const d = stats.max_subgraph_depth;
|
|
166
|
+
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
160
167
|
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
161
168
|
const vcs = base * (1 + d * config.depth_weight);
|
|
162
169
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
package/src/parse/stats.ts
CHANGED
|
@@ -36,7 +36,14 @@ export interface DiagramStats {
|
|
|
36
36
|
nodes: number;
|
|
37
37
|
edges: number;
|
|
38
38
|
subgraphs: number;
|
|
39
|
+
// How deep real containers nest: flowchart subgraphs, composite states,
|
|
40
|
+
// architecture groups and the like.
|
|
39
41
|
max_subgraph_depth: number;
|
|
42
|
+
// How far a tree-shaped diagram (mindmap, treemap, treeView, ishikawa)
|
|
43
|
+
// reaches below its root. Those types draw no subgraphs, so their depth is
|
|
44
|
+
// kept apart from max_subgraph_depth and the nesting rule never reads it.
|
|
45
|
+
// It is 0 for every other type.
|
|
46
|
+
max_tree_depth: number;
|
|
40
47
|
node_ids: string[];
|
|
41
48
|
subgraph_names: string[];
|
|
42
49
|
// The parser that read the diagram, or, on a failure, the one whose reason
|
|
@@ -137,6 +144,7 @@ export async function extractStats(content: string): Promise<MermaidStats> {
|
|
|
137
144
|
edges: 0,
|
|
138
145
|
subgraphs: 0,
|
|
139
146
|
max_subgraph_depth: 0,
|
|
147
|
+
max_tree_depth: 0,
|
|
140
148
|
node_ids: [],
|
|
141
149
|
subgraph_names: [],
|
|
142
150
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -200,6 +208,7 @@ async function extractLangiumStats(content: string, keyword: string): Promise<Me
|
|
|
200
208
|
const subgraphNames: string[] = [];
|
|
201
209
|
let edges = 0;
|
|
202
210
|
let maxDepth = 0;
|
|
211
|
+
let treeDepth = 0;
|
|
203
212
|
|
|
204
213
|
switch (mapped) {
|
|
205
214
|
case "architecture": {
|
|
@@ -246,7 +255,7 @@ async function extractLangiumStats(content: string, keyword: string): Promise<Me
|
|
|
246
255
|
const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
|
|
247
256
|
nodeIds.push(...stats.labels);
|
|
248
257
|
edges = stats.edges;
|
|
249
|
-
|
|
258
|
+
treeDepth = stats.depth;
|
|
250
259
|
break;
|
|
251
260
|
}
|
|
252
261
|
case "treemap": {
|
|
@@ -255,7 +264,7 @@ async function extractLangiumStats(content: string, keyword: string): Promise<Me
|
|
|
255
264
|
const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
|
|
256
265
|
nodeIds.push(...stats.labels);
|
|
257
266
|
edges = stats.edges;
|
|
258
|
-
|
|
267
|
+
treeDepth = stats.depth;
|
|
259
268
|
break;
|
|
260
269
|
}
|
|
261
270
|
case "wardley": {
|
|
@@ -277,6 +286,7 @@ async function extractLangiumStats(content: string, keyword: string): Promise<Me
|
|
|
277
286
|
edges,
|
|
278
287
|
subgraphs: subgraphNames.length,
|
|
279
288
|
max_subgraph_depth: maxDepth,
|
|
289
|
+
max_tree_depth: treeDepth,
|
|
280
290
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
281
291
|
subgraph_names: subgraphNames,
|
|
282
292
|
parser_used: "langium",
|
|
@@ -340,6 +350,8 @@ interface Structure {
|
|
|
340
350
|
edges: number;
|
|
341
351
|
subgraphNames: string[];
|
|
342
352
|
maxDepth: number;
|
|
353
|
+
// Set only by the tree-shaped types: see max_tree_depth.
|
|
354
|
+
treeDepth?: number;
|
|
343
355
|
}
|
|
344
356
|
type CoreAdapter = (db: AnyDb) => Structure;
|
|
345
357
|
|
|
@@ -362,13 +374,14 @@ async function extractCoreStats(content: string, keyword: string): Promise<Merma
|
|
|
362
374
|
if (!adapter) {
|
|
363
375
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
364
376
|
}
|
|
365
|
-
const { nodeIds, edges, subgraphNames, maxDepth } = adapter(diagram.db);
|
|
377
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
366
378
|
|
|
367
379
|
return {
|
|
368
380
|
nodes: nodeIds.length,
|
|
369
381
|
edges,
|
|
370
382
|
subgraphs: subgraphNames.length,
|
|
371
383
|
max_subgraph_depth: maxDepth,
|
|
384
|
+
max_tree_depth: treeDepth,
|
|
372
385
|
node_ids: [...nodeIds].sort(),
|
|
373
386
|
subgraph_names: subgraphNames,
|
|
374
387
|
parser_used: "mermaid-core",
|
|
@@ -386,6 +399,9 @@ async function extractCoreStats(content: string, keyword: string): Promise<Merma
|
|
|
386
399
|
const CORE_ADAPTERS: Record<string, CoreAdapter> = {
|
|
387
400
|
"flowchart-v2": adaptFlowchart,
|
|
388
401
|
flowchart: adaptFlowchart,
|
|
402
|
+
// `flowchart-elk` is a flowchart drawn with the ELK layout. Mermaid parses it
|
|
403
|
+
// with the flowchart grammar into the same DB, and only the layout differs.
|
|
404
|
+
"flowchart-elk": adaptFlowchart,
|
|
389
405
|
graph: adaptFlowchart,
|
|
390
406
|
classDiagram: adaptClass,
|
|
391
407
|
"classDiagram-v2": adaptClass,
|
|
@@ -557,7 +573,13 @@ function adaptIshikawa(db: AnyDb): Structure {
|
|
|
557
573
|
for (const c of node.children ?? []) walk(c, depth + 1);
|
|
558
574
|
};
|
|
559
575
|
walk(root, 0);
|
|
560
|
-
return {
|
|
576
|
+
return {
|
|
577
|
+
nodeIds: labels,
|
|
578
|
+
edges: Math.max(0, labels.length - 1),
|
|
579
|
+
subgraphNames: [],
|
|
580
|
+
maxDepth: 0,
|
|
581
|
+
treeDepth: maxDepth,
|
|
582
|
+
};
|
|
561
583
|
}
|
|
562
584
|
|
|
563
585
|
interface KanbanNode {
|
|
@@ -596,7 +618,13 @@ function adaptMindmap(db: AnyDb): Structure {
|
|
|
596
618
|
for (const c of node.children ?? []) walk(c, depth + 1);
|
|
597
619
|
};
|
|
598
620
|
walk(root, 0);
|
|
599
|
-
return {
|
|
621
|
+
return {
|
|
622
|
+
nodeIds: labels,
|
|
623
|
+
edges: Math.max(0, labels.length - 1),
|
|
624
|
+
subgraphNames: [],
|
|
625
|
+
maxDepth: 0,
|
|
626
|
+
treeDepth: maxDepth,
|
|
627
|
+
};
|
|
600
628
|
}
|
|
601
629
|
|
|
602
630
|
type QuadrantText = { text?: string } | string | undefined;
|
|
@@ -3,6 +3,11 @@ import { RULE } from "./types.ts";
|
|
|
3
3
|
|
|
4
4
|
export const MAX_NESTING = 3;
|
|
5
5
|
|
|
6
|
+
// Reads max_subgraph_depth only. A mindmap, treemap, treeView or ishikawa
|
|
7
|
+
// diagram reports its depth as max_tree_depth instead, because it draws no
|
|
8
|
+
// subgraphs and flattening them is no remedy for a deep tree. Its depth still
|
|
9
|
+
// weighs on the visual complexity score (calculateComplexity in metrics.ts).
|
|
10
|
+
|
|
6
11
|
export const complexitySubgraphNestingTooDeep = complexityRule({
|
|
7
12
|
id: RULE.NESTING,
|
|
8
13
|
summary: "subgraphs nested three or more levels deep",
|
|
@@ -20,7 +20,7 @@ export const parseFenceUnterminated: Rule = {
|
|
|
20
20
|
d,
|
|
21
21
|
RULE.FENCE_UNTERMINATED,
|
|
22
22
|
`mermaid fence opened at line ${d.startLine} is never closed`,
|
|
23
|
-
"Add a closing fence after the last line of the diagram.",
|
|
23
|
+
"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.",
|
|
24
24
|
),
|
|
25
25
|
),
|
|
26
26
|
fix: (ctx) => {
|