@jpeakai/mermaidjs-lint 0.7.4 → 0.8.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/dist/bin.js +78 -12
- package/dist/index.js +78 -12
- package/dist/testing.js +77 -11
- package/dist/types/parse/metrics.d.ts +2 -0
- package/dist/types/parse/stats.d.ts +29 -0
- package/package.json +1 -1
- package/src/fix/verify.ts +1 -0
- package/src/parse/graph.ts +118 -1
- package/src/parse/metrics.ts +8 -3
- package/src/parse/stats.ts +132 -14
- package/src/rules/shared.ts +1 -0
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.8.0",
|
|
13
13
|
type: "module",
|
|
14
14
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
15
15
|
license: "MIT",
|
|
@@ -381,6 +381,7 @@ var extractStyleDirectives = (source) => {
|
|
|
381
381
|
|
|
382
382
|
// src/parse/stats.ts
|
|
383
383
|
import { parse as langiumParse } from "@mermaid-js/parser";
|
|
384
|
+
import Color2 from "colorjs.io";
|
|
384
385
|
import { Window } from "happy-dom";
|
|
385
386
|
var installDomShim = () => {
|
|
386
387
|
const g = globalThis;
|
|
@@ -395,6 +396,23 @@ var installDomShim = () => {
|
|
|
395
396
|
g.DocumentFragment = win.DocumentFragment;
|
|
396
397
|
g.NodeFilter = win.NodeFilter;
|
|
397
398
|
g.getComputedStyle = win.getComputedStyle.bind(win);
|
|
399
|
+
const css = win.CSS;
|
|
400
|
+
if (css) {
|
|
401
|
+
const supports = css.supports.bind(css);
|
|
402
|
+
const fixed = {
|
|
403
|
+
...css,
|
|
404
|
+
supports: (property, value) => property === "color" && value !== undefined ? isCssColour(value) : supports(property, value)
|
|
405
|
+
};
|
|
406
|
+
Object.defineProperty(win, "CSS", { value: fixed, configurable: true });
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var isCssColour = (value) => {
|
|
410
|
+
try {
|
|
411
|
+
new Color2(value);
|
|
412
|
+
return true;
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
398
416
|
};
|
|
399
417
|
var mermaidCore = null;
|
|
400
418
|
var getMermaid = async () => {
|
|
@@ -470,6 +488,7 @@ async function extractStats(content) {
|
|
|
470
488
|
subgraphs: 0,
|
|
471
489
|
max_subgraph_depth: 0,
|
|
472
490
|
max_tree_depth: 0,
|
|
491
|
+
rows: 0,
|
|
473
492
|
node_ids: [],
|
|
474
493
|
subgraph_names: [],
|
|
475
494
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -522,6 +541,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
522
541
|
subgraphs: subgraphNames.length,
|
|
523
542
|
max_subgraph_depth: maxDepth,
|
|
524
543
|
max_tree_depth: treeDepth,
|
|
544
|
+
rows: 0,
|
|
525
545
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
526
546
|
subgraph_names: subgraphNames,
|
|
527
547
|
parser_used: "langium",
|
|
@@ -651,13 +671,14 @@ async function extractCoreStats(content, keyword) {
|
|
|
651
671
|
if (!adapter) {
|
|
652
672
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
653
673
|
}
|
|
654
|
-
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
674
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0, rows = 0 } = adapter(diagram.db);
|
|
655
675
|
return {
|
|
656
676
|
nodes: nodeIds.length,
|
|
657
677
|
edges,
|
|
658
678
|
subgraphs: subgraphNames.length,
|
|
659
679
|
max_subgraph_depth: maxDepth,
|
|
660
680
|
max_tree_depth: treeDepth,
|
|
681
|
+
rows,
|
|
661
682
|
node_ids: [...nodeIds].sort(),
|
|
662
683
|
subgraph_names: subgraphNames,
|
|
663
684
|
parser_used: "mermaid-core",
|
|
@@ -706,21 +727,50 @@ function adaptFlowchart(db) {
|
|
|
706
727
|
maxDepth: subgraphNestingDepth(subGraphs)
|
|
707
728
|
};
|
|
708
729
|
}
|
|
730
|
+
var lengthOf = (x) => Array.isArray(x) ? x.length : 0;
|
|
709
731
|
function adaptClass(db) {
|
|
710
732
|
const classes = db.classes instanceof Map ? db.classes : new Map;
|
|
711
733
|
const relations = Array.isArray(db.relations) ? db.relations : [];
|
|
712
|
-
|
|
734
|
+
const namespaces = db.namespaces instanceof Map ? db.namespaces : new Map;
|
|
735
|
+
const groups = [...namespaces].map(([id, ns]) => ({ id, in: ns.parent, label: ns.label || id }));
|
|
736
|
+
let rows = 0;
|
|
737
|
+
for (const c of classes.values())
|
|
738
|
+
rows += lengthOf(c.members) + lengthOf(c.methods);
|
|
739
|
+
return {
|
|
740
|
+
nodeIds: [...classes.keys()],
|
|
741
|
+
edges: relations.length,
|
|
742
|
+
subgraphNames: groups.map((g) => g.label),
|
|
743
|
+
maxDepth: architectureDepth(groups),
|
|
744
|
+
rows
|
|
745
|
+
};
|
|
713
746
|
}
|
|
747
|
+
var SEQ_NOTE = 2;
|
|
748
|
+
var SEQUENCE_BLOCK_OPEN = new Set([10, 12, 15, 19, 27, 30, 32]);
|
|
749
|
+
var SEQUENCE_BLOCK_CLOSE = new Set([11, 14, 16, 21, 29, 31]);
|
|
750
|
+
var isSequenceMessage = (m) => m.type !== SEQ_NOTE && typeof m.from === "string" && typeof m.to === "string";
|
|
714
751
|
function adaptSequence(db) {
|
|
715
752
|
const state = db.state?.records;
|
|
716
753
|
const actors = state?.actors instanceof Map ? state.actors : new Map;
|
|
717
754
|
const rawMessages = state?.messages;
|
|
718
|
-
const
|
|
755
|
+
const records = Array.isArray(rawMessages) ? rawMessages : [];
|
|
756
|
+
const boxes = Array.isArray(state?.boxes) ? state.boxes : [];
|
|
757
|
+
const subgraphNames = boxes.map((b, i) => b.name || `box_${i}`);
|
|
758
|
+
let depth = 0;
|
|
759
|
+
let maxBlockDepth = 0;
|
|
760
|
+
for (const r of records) {
|
|
761
|
+
if (r.type !== undefined && SEQUENCE_BLOCK_OPEN.has(r.type)) {
|
|
762
|
+
depth++;
|
|
763
|
+
maxBlockDepth = Math.max(maxBlockDepth, depth);
|
|
764
|
+
subgraphNames.push(typeof r.message === "string" && r.message ? r.message : `block_${subgraphNames.length}`);
|
|
765
|
+
} else if (r.type !== undefined && SEQUENCE_BLOCK_CLOSE.has(r.type)) {
|
|
766
|
+
depth = Math.max(0, depth - 1);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
719
769
|
return {
|
|
720
770
|
nodeIds: [...actors.keys()],
|
|
721
|
-
edges:
|
|
722
|
-
subgraphNames
|
|
723
|
-
maxDepth: 0
|
|
771
|
+
edges: records.filter(isSequenceMessage).length,
|
|
772
|
+
subgraphNames,
|
|
773
|
+
maxDepth: Math.max(boxes.length > 0 ? 1 : 0, maxBlockDepth)
|
|
724
774
|
};
|
|
725
775
|
}
|
|
726
776
|
function adaptBlock(db) {
|
|
@@ -761,11 +811,15 @@ function adaptC4(db) {
|
|
|
761
811
|
function adaptEr(db) {
|
|
762
812
|
const entities = db.entities instanceof Map ? db.entities : new Map;
|
|
763
813
|
const relationships = Array.isArray(db.relationships) ? db.relationships : [];
|
|
814
|
+
let rows = 0;
|
|
815
|
+
for (const e of entities.values())
|
|
816
|
+
rows += lengthOf(e.attributes);
|
|
764
817
|
return {
|
|
765
818
|
nodeIds: [...entities.keys()],
|
|
766
819
|
edges: relationships.length,
|
|
767
820
|
subgraphNames: [],
|
|
768
|
-
maxDepth: 0
|
|
821
|
+
maxDepth: 0,
|
|
822
|
+
rows
|
|
769
823
|
};
|
|
770
824
|
}
|
|
771
825
|
function adaptGantt(db) {
|
|
@@ -862,9 +916,17 @@ function adaptSankey(db) {
|
|
|
862
916
|
maxDepth: 0
|
|
863
917
|
};
|
|
864
918
|
}
|
|
919
|
+
var STATE_NOT_A_STATE = new Set(["stateStart", "stateEnd", "note", "noteGroup"]);
|
|
920
|
+
var isCountedState = (n) => !STATE_NOT_A_STATE.has(n.shape ?? "");
|
|
921
|
+
var countedStateElements = (db) => {
|
|
922
|
+
const all = db.nodes ?? [];
|
|
923
|
+
const nodes = all.filter(isCountedState);
|
|
924
|
+
const kept = new Set(nodes.map((n) => n.id ?? ""));
|
|
925
|
+
const edges = (db.edges ?? []).filter((e) => kept.has(e.start ?? "") && kept.has(e.end ?? ""));
|
|
926
|
+
return { nodes, edges };
|
|
927
|
+
};
|
|
865
928
|
function adaptState(db) {
|
|
866
|
-
const nodes = db
|
|
867
|
-
const edges = db.edges ?? [];
|
|
929
|
+
const { nodes, edges } = countedStateElements(db);
|
|
868
930
|
const groups = nodes.filter((n) => n.isGroup === true);
|
|
869
931
|
const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
|
|
870
932
|
let maxDepth = 0;
|
|
@@ -1489,7 +1551,7 @@ var PRESET_ALIASES = {
|
|
|
1489
1551
|
permissive: "high-density",
|
|
1490
1552
|
default: "high-density"
|
|
1491
1553
|
};
|
|
1492
|
-
var VCS_WEIGHTS = { edge: 0.5, subgraph: 3, depth: 0.1 };
|
|
1554
|
+
var VCS_WEIGHTS = { edge: 0.5, row: 0.25, subgraph: 3, depth: 0.1 };
|
|
1493
1555
|
var THRESHOLD_OPTIONS = {
|
|
1494
1556
|
nodeIdeal: "node_ideal",
|
|
1495
1557
|
nodeAcceptable: "node_acceptable",
|
|
@@ -1502,6 +1564,7 @@ var THRESHOLD_OPTIONS = {
|
|
|
1502
1564
|
nodeTarget: "node_target",
|
|
1503
1565
|
vcsTarget: "vcs_target",
|
|
1504
1566
|
edgeWeight: "edge_weight",
|
|
1567
|
+
rowWeight: "row_weight",
|
|
1505
1568
|
subgraphWeight: "subgraph_weight",
|
|
1506
1569
|
depthWeight: "depth_weight"
|
|
1507
1570
|
};
|
|
@@ -1520,6 +1583,7 @@ var configFromPreset = (name) => {
|
|
|
1520
1583
|
return {
|
|
1521
1584
|
...preset,
|
|
1522
1585
|
edge_weight: VCS_WEIGHTS.edge,
|
|
1586
|
+
row_weight: VCS_WEIGHTS.row,
|
|
1523
1587
|
subgraph_weight: VCS_WEIGHTS.subgraph,
|
|
1524
1588
|
depth_weight: VCS_WEIGHTS.depth,
|
|
1525
1589
|
preset_name: canonical
|
|
@@ -1547,7 +1611,7 @@ var calculateComplexity = (stats, config) => {
|
|
|
1547
1611
|
const e = stats.edges;
|
|
1548
1612
|
const s = stats.subgraphs;
|
|
1549
1613
|
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
1550
|
-
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
1614
|
+
const base = n + e * config.edge_weight + stats.rows * config.row_weight + s * config.subgraph_weight;
|
|
1551
1615
|
const vcs = base * (1 + d * config.depth_weight);
|
|
1552
1616
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
|
1553
1617
|
return {
|
|
@@ -1583,6 +1647,7 @@ var COMPLEXITY_OPTIONS = {
|
|
|
1583
1647
|
nodeTarget: "number",
|
|
1584
1648
|
vcsTarget: "number",
|
|
1585
1649
|
edgeWeight: "number",
|
|
1650
|
+
rowWeight: "number",
|
|
1586
1651
|
subgraphWeight: "number",
|
|
1587
1652
|
depthWeight: "number"
|
|
1588
1653
|
};
|
|
@@ -2273,6 +2338,7 @@ var sameDiagrams = async (before, after) => {
|
|
|
2273
2338
|
s.subgraphs,
|
|
2274
2339
|
s.max_subgraph_depth,
|
|
2275
2340
|
s.max_tree_depth,
|
|
2341
|
+
s.rows,
|
|
2276
2342
|
s.node_ids,
|
|
2277
2343
|
s.subgraph_names
|
|
2278
2344
|
];
|
package/dist/index.js
CHANGED
|
@@ -303,6 +303,7 @@ var extractStyleDirectives = (source) => {
|
|
|
303
303
|
|
|
304
304
|
// src/parse/stats.ts
|
|
305
305
|
import { parse as langiumParse } from "@mermaid-js/parser";
|
|
306
|
+
import Color2 from "colorjs.io";
|
|
306
307
|
import { Window } from "happy-dom";
|
|
307
308
|
var installDomShim = () => {
|
|
308
309
|
const g = globalThis;
|
|
@@ -317,6 +318,23 @@ var installDomShim = () => {
|
|
|
317
318
|
g.DocumentFragment = win.DocumentFragment;
|
|
318
319
|
g.NodeFilter = win.NodeFilter;
|
|
319
320
|
g.getComputedStyle = win.getComputedStyle.bind(win);
|
|
321
|
+
const css = win.CSS;
|
|
322
|
+
if (css) {
|
|
323
|
+
const supports = css.supports.bind(css);
|
|
324
|
+
const fixed = {
|
|
325
|
+
...css,
|
|
326
|
+
supports: (property, value) => property === "color" && value !== undefined ? isCssColour(value) : supports(property, value)
|
|
327
|
+
};
|
|
328
|
+
Object.defineProperty(win, "CSS", { value: fixed, configurable: true });
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var isCssColour = (value) => {
|
|
332
|
+
try {
|
|
333
|
+
new Color2(value);
|
|
334
|
+
return true;
|
|
335
|
+
} catch {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
320
338
|
};
|
|
321
339
|
var mermaidCore = null;
|
|
322
340
|
var getMermaid = async () => {
|
|
@@ -392,6 +410,7 @@ async function extractStats(content) {
|
|
|
392
410
|
subgraphs: 0,
|
|
393
411
|
max_subgraph_depth: 0,
|
|
394
412
|
max_tree_depth: 0,
|
|
413
|
+
rows: 0,
|
|
395
414
|
node_ids: [],
|
|
396
415
|
subgraph_names: [],
|
|
397
416
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -444,6 +463,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
444
463
|
subgraphs: subgraphNames.length,
|
|
445
464
|
max_subgraph_depth: maxDepth,
|
|
446
465
|
max_tree_depth: treeDepth,
|
|
466
|
+
rows: 0,
|
|
447
467
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
448
468
|
subgraph_names: subgraphNames,
|
|
449
469
|
parser_used: "langium",
|
|
@@ -573,13 +593,14 @@ async function extractCoreStats(content, keyword) {
|
|
|
573
593
|
if (!adapter) {
|
|
574
594
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
575
595
|
}
|
|
576
|
-
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
596
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0, rows = 0 } = adapter(diagram.db);
|
|
577
597
|
return {
|
|
578
598
|
nodes: nodeIds.length,
|
|
579
599
|
edges,
|
|
580
600
|
subgraphs: subgraphNames.length,
|
|
581
601
|
max_subgraph_depth: maxDepth,
|
|
582
602
|
max_tree_depth: treeDepth,
|
|
603
|
+
rows,
|
|
583
604
|
node_ids: [...nodeIds].sort(),
|
|
584
605
|
subgraph_names: subgraphNames,
|
|
585
606
|
parser_used: "mermaid-core",
|
|
@@ -628,21 +649,50 @@ function adaptFlowchart(db) {
|
|
|
628
649
|
maxDepth: subgraphNestingDepth(subGraphs)
|
|
629
650
|
};
|
|
630
651
|
}
|
|
652
|
+
var lengthOf = (x) => Array.isArray(x) ? x.length : 0;
|
|
631
653
|
function adaptClass(db) {
|
|
632
654
|
const classes = db.classes instanceof Map ? db.classes : new Map;
|
|
633
655
|
const relations = Array.isArray(db.relations) ? db.relations : [];
|
|
634
|
-
|
|
656
|
+
const namespaces = db.namespaces instanceof Map ? db.namespaces : new Map;
|
|
657
|
+
const groups = [...namespaces].map(([id, ns]) => ({ id, in: ns.parent, label: ns.label || id }));
|
|
658
|
+
let rows = 0;
|
|
659
|
+
for (const c of classes.values())
|
|
660
|
+
rows += lengthOf(c.members) + lengthOf(c.methods);
|
|
661
|
+
return {
|
|
662
|
+
nodeIds: [...classes.keys()],
|
|
663
|
+
edges: relations.length,
|
|
664
|
+
subgraphNames: groups.map((g) => g.label),
|
|
665
|
+
maxDepth: architectureDepth(groups),
|
|
666
|
+
rows
|
|
667
|
+
};
|
|
635
668
|
}
|
|
669
|
+
var SEQ_NOTE = 2;
|
|
670
|
+
var SEQUENCE_BLOCK_OPEN = new Set([10, 12, 15, 19, 27, 30, 32]);
|
|
671
|
+
var SEQUENCE_BLOCK_CLOSE = new Set([11, 14, 16, 21, 29, 31]);
|
|
672
|
+
var isSequenceMessage = (m) => m.type !== SEQ_NOTE && typeof m.from === "string" && typeof m.to === "string";
|
|
636
673
|
function adaptSequence(db) {
|
|
637
674
|
const state = db.state?.records;
|
|
638
675
|
const actors = state?.actors instanceof Map ? state.actors : new Map;
|
|
639
676
|
const rawMessages = state?.messages;
|
|
640
|
-
const
|
|
677
|
+
const records = Array.isArray(rawMessages) ? rawMessages : [];
|
|
678
|
+
const boxes = Array.isArray(state?.boxes) ? state.boxes : [];
|
|
679
|
+
const subgraphNames = boxes.map((b, i) => b.name || `box_${i}`);
|
|
680
|
+
let depth = 0;
|
|
681
|
+
let maxBlockDepth = 0;
|
|
682
|
+
for (const r of records) {
|
|
683
|
+
if (r.type !== undefined && SEQUENCE_BLOCK_OPEN.has(r.type)) {
|
|
684
|
+
depth++;
|
|
685
|
+
maxBlockDepth = Math.max(maxBlockDepth, depth);
|
|
686
|
+
subgraphNames.push(typeof r.message === "string" && r.message ? r.message : `block_${subgraphNames.length}`);
|
|
687
|
+
} else if (r.type !== undefined && SEQUENCE_BLOCK_CLOSE.has(r.type)) {
|
|
688
|
+
depth = Math.max(0, depth - 1);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
641
691
|
return {
|
|
642
692
|
nodeIds: [...actors.keys()],
|
|
643
|
-
edges:
|
|
644
|
-
subgraphNames
|
|
645
|
-
maxDepth: 0
|
|
693
|
+
edges: records.filter(isSequenceMessage).length,
|
|
694
|
+
subgraphNames,
|
|
695
|
+
maxDepth: Math.max(boxes.length > 0 ? 1 : 0, maxBlockDepth)
|
|
646
696
|
};
|
|
647
697
|
}
|
|
648
698
|
function adaptBlock(db) {
|
|
@@ -683,11 +733,15 @@ function adaptC4(db) {
|
|
|
683
733
|
function adaptEr(db) {
|
|
684
734
|
const entities = db.entities instanceof Map ? db.entities : new Map;
|
|
685
735
|
const relationships = Array.isArray(db.relationships) ? db.relationships : [];
|
|
736
|
+
let rows = 0;
|
|
737
|
+
for (const e of entities.values())
|
|
738
|
+
rows += lengthOf(e.attributes);
|
|
686
739
|
return {
|
|
687
740
|
nodeIds: [...entities.keys()],
|
|
688
741
|
edges: relationships.length,
|
|
689
742
|
subgraphNames: [],
|
|
690
|
-
maxDepth: 0
|
|
743
|
+
maxDepth: 0,
|
|
744
|
+
rows
|
|
691
745
|
};
|
|
692
746
|
}
|
|
693
747
|
function adaptGantt(db) {
|
|
@@ -784,9 +838,17 @@ function adaptSankey(db) {
|
|
|
784
838
|
maxDepth: 0
|
|
785
839
|
};
|
|
786
840
|
}
|
|
841
|
+
var STATE_NOT_A_STATE = new Set(["stateStart", "stateEnd", "note", "noteGroup"]);
|
|
842
|
+
var isCountedState = (n) => !STATE_NOT_A_STATE.has(n.shape ?? "");
|
|
843
|
+
var countedStateElements = (db) => {
|
|
844
|
+
const all = db.nodes ?? [];
|
|
845
|
+
const nodes = all.filter(isCountedState);
|
|
846
|
+
const kept = new Set(nodes.map((n) => n.id ?? ""));
|
|
847
|
+
const edges = (db.edges ?? []).filter((e) => kept.has(e.start ?? "") && kept.has(e.end ?? ""));
|
|
848
|
+
return { nodes, edges };
|
|
849
|
+
};
|
|
787
850
|
function adaptState(db) {
|
|
788
|
-
const nodes = db
|
|
789
|
-
const edges = db.edges ?? [];
|
|
851
|
+
const { nodes, edges } = countedStateElements(db);
|
|
790
852
|
const groups = nodes.filter((n) => n.isGroup === true);
|
|
791
853
|
const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
|
|
792
854
|
let maxDepth = 0;
|
|
@@ -1411,7 +1473,7 @@ var PRESET_ALIASES = {
|
|
|
1411
1473
|
permissive: "high-density",
|
|
1412
1474
|
default: "high-density"
|
|
1413
1475
|
};
|
|
1414
|
-
var VCS_WEIGHTS = { edge: 0.5, subgraph: 3, depth: 0.1 };
|
|
1476
|
+
var VCS_WEIGHTS = { edge: 0.5, row: 0.25, subgraph: 3, depth: 0.1 };
|
|
1415
1477
|
var THRESHOLD_OPTIONS = {
|
|
1416
1478
|
nodeIdeal: "node_ideal",
|
|
1417
1479
|
nodeAcceptable: "node_acceptable",
|
|
@@ -1424,6 +1486,7 @@ var THRESHOLD_OPTIONS = {
|
|
|
1424
1486
|
nodeTarget: "node_target",
|
|
1425
1487
|
vcsTarget: "vcs_target",
|
|
1426
1488
|
edgeWeight: "edge_weight",
|
|
1489
|
+
rowWeight: "row_weight",
|
|
1427
1490
|
subgraphWeight: "subgraph_weight",
|
|
1428
1491
|
depthWeight: "depth_weight"
|
|
1429
1492
|
};
|
|
@@ -1442,6 +1505,7 @@ var configFromPreset = (name) => {
|
|
|
1442
1505
|
return {
|
|
1443
1506
|
...preset,
|
|
1444
1507
|
edge_weight: VCS_WEIGHTS.edge,
|
|
1508
|
+
row_weight: VCS_WEIGHTS.row,
|
|
1445
1509
|
subgraph_weight: VCS_WEIGHTS.subgraph,
|
|
1446
1510
|
depth_weight: VCS_WEIGHTS.depth,
|
|
1447
1511
|
preset_name: canonical
|
|
@@ -1469,7 +1533,7 @@ var calculateComplexity = (stats, config) => {
|
|
|
1469
1533
|
const e = stats.edges;
|
|
1470
1534
|
const s = stats.subgraphs;
|
|
1471
1535
|
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
1472
|
-
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
1536
|
+
const base = n + e * config.edge_weight + stats.rows * config.row_weight + s * config.subgraph_weight;
|
|
1473
1537
|
const vcs = base * (1 + d * config.depth_weight);
|
|
1474
1538
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
|
1475
1539
|
return {
|
|
@@ -1505,6 +1569,7 @@ var COMPLEXITY_OPTIONS = {
|
|
|
1505
1569
|
nodeTarget: "number",
|
|
1506
1570
|
vcsTarget: "number",
|
|
1507
1571
|
edgeWeight: "number",
|
|
1572
|
+
rowWeight: "number",
|
|
1508
1573
|
subgraphWeight: "number",
|
|
1509
1574
|
depthWeight: "number"
|
|
1510
1575
|
};
|
|
@@ -2095,7 +2160,7 @@ import { parseArgs } from "node:util";
|
|
|
2095
2160
|
// package.json
|
|
2096
2161
|
var package_default = {
|
|
2097
2162
|
name: "@jpeakai/mermaidjs-lint",
|
|
2098
|
-
version: "0.
|
|
2163
|
+
version: "0.8.0",
|
|
2099
2164
|
type: "module",
|
|
2100
2165
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
2101
2166
|
license: "MIT",
|
|
@@ -2272,6 +2337,7 @@ var sameDiagrams = async (before, after) => {
|
|
|
2272
2337
|
s.subgraphs,
|
|
2273
2338
|
s.max_subgraph_depth,
|
|
2274
2339
|
s.max_tree_depth,
|
|
2340
|
+
s.rows,
|
|
2275
2341
|
s.node_ids,
|
|
2276
2342
|
s.subgraph_names
|
|
2277
2343
|
];
|
package/dist/testing.js
CHANGED
|
@@ -201,6 +201,7 @@ var extractStyleDirectives = (source) => {
|
|
|
201
201
|
|
|
202
202
|
// src/parse/stats.ts
|
|
203
203
|
import { parse as langiumParse } from "@mermaid-js/parser";
|
|
204
|
+
import Color2 from "colorjs.io";
|
|
204
205
|
import { Window } from "happy-dom";
|
|
205
206
|
var installDomShim = () => {
|
|
206
207
|
const g = globalThis;
|
|
@@ -215,6 +216,23 @@ var installDomShim = () => {
|
|
|
215
216
|
g.DocumentFragment = win.DocumentFragment;
|
|
216
217
|
g.NodeFilter = win.NodeFilter;
|
|
217
218
|
g.getComputedStyle = win.getComputedStyle.bind(win);
|
|
219
|
+
const css = win.CSS;
|
|
220
|
+
if (css) {
|
|
221
|
+
const supports = css.supports.bind(css);
|
|
222
|
+
const fixed = {
|
|
223
|
+
...css,
|
|
224
|
+
supports: (property, value) => property === "color" && value !== undefined ? isCssColour(value) : supports(property, value)
|
|
225
|
+
};
|
|
226
|
+
Object.defineProperty(win, "CSS", { value: fixed, configurable: true });
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
var isCssColour = (value) => {
|
|
230
|
+
try {
|
|
231
|
+
new Color2(value);
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
218
236
|
};
|
|
219
237
|
var mermaidCore = null;
|
|
220
238
|
var getMermaid = async () => {
|
|
@@ -290,6 +308,7 @@ async function extractStats(content) {
|
|
|
290
308
|
subgraphs: 0,
|
|
291
309
|
max_subgraph_depth: 0,
|
|
292
310
|
max_tree_depth: 0,
|
|
311
|
+
rows: 0,
|
|
293
312
|
node_ids: [],
|
|
294
313
|
subgraph_names: [],
|
|
295
314
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -342,6 +361,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
342
361
|
subgraphs: subgraphNames.length,
|
|
343
362
|
max_subgraph_depth: maxDepth,
|
|
344
363
|
max_tree_depth: treeDepth,
|
|
364
|
+
rows: 0,
|
|
345
365
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
346
366
|
subgraph_names: subgraphNames,
|
|
347
367
|
parser_used: "langium",
|
|
@@ -471,13 +491,14 @@ async function extractCoreStats(content, keyword) {
|
|
|
471
491
|
if (!adapter) {
|
|
472
492
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
473
493
|
}
|
|
474
|
-
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
494
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0, rows = 0 } = adapter(diagram.db);
|
|
475
495
|
return {
|
|
476
496
|
nodes: nodeIds.length,
|
|
477
497
|
edges,
|
|
478
498
|
subgraphs: subgraphNames.length,
|
|
479
499
|
max_subgraph_depth: maxDepth,
|
|
480
500
|
max_tree_depth: treeDepth,
|
|
501
|
+
rows,
|
|
481
502
|
node_ids: [...nodeIds].sort(),
|
|
482
503
|
subgraph_names: subgraphNames,
|
|
483
504
|
parser_used: "mermaid-core",
|
|
@@ -526,21 +547,50 @@ function adaptFlowchart(db) {
|
|
|
526
547
|
maxDepth: subgraphNestingDepth(subGraphs)
|
|
527
548
|
};
|
|
528
549
|
}
|
|
550
|
+
var lengthOf = (x) => Array.isArray(x) ? x.length : 0;
|
|
529
551
|
function adaptClass(db) {
|
|
530
552
|
const classes = db.classes instanceof Map ? db.classes : new Map;
|
|
531
553
|
const relations = Array.isArray(db.relations) ? db.relations : [];
|
|
532
|
-
|
|
554
|
+
const namespaces = db.namespaces instanceof Map ? db.namespaces : new Map;
|
|
555
|
+
const groups = [...namespaces].map(([id, ns]) => ({ id, in: ns.parent, label: ns.label || id }));
|
|
556
|
+
let rows = 0;
|
|
557
|
+
for (const c of classes.values())
|
|
558
|
+
rows += lengthOf(c.members) + lengthOf(c.methods);
|
|
559
|
+
return {
|
|
560
|
+
nodeIds: [...classes.keys()],
|
|
561
|
+
edges: relations.length,
|
|
562
|
+
subgraphNames: groups.map((g) => g.label),
|
|
563
|
+
maxDepth: architectureDepth(groups),
|
|
564
|
+
rows
|
|
565
|
+
};
|
|
533
566
|
}
|
|
567
|
+
var SEQ_NOTE = 2;
|
|
568
|
+
var SEQUENCE_BLOCK_OPEN = new Set([10, 12, 15, 19, 27, 30, 32]);
|
|
569
|
+
var SEQUENCE_BLOCK_CLOSE = new Set([11, 14, 16, 21, 29, 31]);
|
|
570
|
+
var isSequenceMessage = (m) => m.type !== SEQ_NOTE && typeof m.from === "string" && typeof m.to === "string";
|
|
534
571
|
function adaptSequence(db) {
|
|
535
572
|
const state = db.state?.records;
|
|
536
573
|
const actors = state?.actors instanceof Map ? state.actors : new Map;
|
|
537
574
|
const rawMessages = state?.messages;
|
|
538
|
-
const
|
|
575
|
+
const records = Array.isArray(rawMessages) ? rawMessages : [];
|
|
576
|
+
const boxes = Array.isArray(state?.boxes) ? state.boxes : [];
|
|
577
|
+
const subgraphNames = boxes.map((b, i) => b.name || `box_${i}`);
|
|
578
|
+
let depth = 0;
|
|
579
|
+
let maxBlockDepth = 0;
|
|
580
|
+
for (const r of records) {
|
|
581
|
+
if (r.type !== undefined && SEQUENCE_BLOCK_OPEN.has(r.type)) {
|
|
582
|
+
depth++;
|
|
583
|
+
maxBlockDepth = Math.max(maxBlockDepth, depth);
|
|
584
|
+
subgraphNames.push(typeof r.message === "string" && r.message ? r.message : `block_${subgraphNames.length}`);
|
|
585
|
+
} else if (r.type !== undefined && SEQUENCE_BLOCK_CLOSE.has(r.type)) {
|
|
586
|
+
depth = Math.max(0, depth - 1);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
539
589
|
return {
|
|
540
590
|
nodeIds: [...actors.keys()],
|
|
541
|
-
edges:
|
|
542
|
-
subgraphNames
|
|
543
|
-
maxDepth: 0
|
|
591
|
+
edges: records.filter(isSequenceMessage).length,
|
|
592
|
+
subgraphNames,
|
|
593
|
+
maxDepth: Math.max(boxes.length > 0 ? 1 : 0, maxBlockDepth)
|
|
544
594
|
};
|
|
545
595
|
}
|
|
546
596
|
function adaptBlock(db) {
|
|
@@ -581,11 +631,15 @@ function adaptC4(db) {
|
|
|
581
631
|
function adaptEr(db) {
|
|
582
632
|
const entities = db.entities instanceof Map ? db.entities : new Map;
|
|
583
633
|
const relationships = Array.isArray(db.relationships) ? db.relationships : [];
|
|
634
|
+
let rows = 0;
|
|
635
|
+
for (const e of entities.values())
|
|
636
|
+
rows += lengthOf(e.attributes);
|
|
584
637
|
return {
|
|
585
638
|
nodeIds: [...entities.keys()],
|
|
586
639
|
edges: relationships.length,
|
|
587
640
|
subgraphNames: [],
|
|
588
|
-
maxDepth: 0
|
|
641
|
+
maxDepth: 0,
|
|
642
|
+
rows
|
|
589
643
|
};
|
|
590
644
|
}
|
|
591
645
|
function adaptGantt(db) {
|
|
@@ -682,9 +736,17 @@ function adaptSankey(db) {
|
|
|
682
736
|
maxDepth: 0
|
|
683
737
|
};
|
|
684
738
|
}
|
|
739
|
+
var STATE_NOT_A_STATE = new Set(["stateStart", "stateEnd", "note", "noteGroup"]);
|
|
740
|
+
var isCountedState = (n) => !STATE_NOT_A_STATE.has(n.shape ?? "");
|
|
741
|
+
var countedStateElements = (db) => {
|
|
742
|
+
const all = db.nodes ?? [];
|
|
743
|
+
const nodes = all.filter(isCountedState);
|
|
744
|
+
const kept = new Set(nodes.map((n) => n.id ?? ""));
|
|
745
|
+
const edges = (db.edges ?? []).filter((e) => kept.has(e.start ?? "") && kept.has(e.end ?? ""));
|
|
746
|
+
return { nodes, edges };
|
|
747
|
+
};
|
|
685
748
|
function adaptState(db) {
|
|
686
|
-
const nodes = db
|
|
687
|
-
const edges = db.edges ?? [];
|
|
749
|
+
const { nodes, edges } = countedStateElements(db);
|
|
688
750
|
const groups = nodes.filter((n) => n.isGroup === true);
|
|
689
751
|
const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
|
|
690
752
|
let maxDepth = 0;
|
|
@@ -1309,7 +1371,7 @@ var PRESET_ALIASES = {
|
|
|
1309
1371
|
permissive: "high-density",
|
|
1310
1372
|
default: "high-density"
|
|
1311
1373
|
};
|
|
1312
|
-
var VCS_WEIGHTS = { edge: 0.5, subgraph: 3, depth: 0.1 };
|
|
1374
|
+
var VCS_WEIGHTS = { edge: 0.5, row: 0.25, subgraph: 3, depth: 0.1 };
|
|
1313
1375
|
var THRESHOLD_OPTIONS = {
|
|
1314
1376
|
nodeIdeal: "node_ideal",
|
|
1315
1377
|
nodeAcceptable: "node_acceptable",
|
|
@@ -1322,6 +1384,7 @@ var THRESHOLD_OPTIONS = {
|
|
|
1322
1384
|
nodeTarget: "node_target",
|
|
1323
1385
|
vcsTarget: "vcs_target",
|
|
1324
1386
|
edgeWeight: "edge_weight",
|
|
1387
|
+
rowWeight: "row_weight",
|
|
1325
1388
|
subgraphWeight: "subgraph_weight",
|
|
1326
1389
|
depthWeight: "depth_weight"
|
|
1327
1390
|
};
|
|
@@ -1340,6 +1403,7 @@ var configFromPreset = (name) => {
|
|
|
1340
1403
|
return {
|
|
1341
1404
|
...preset,
|
|
1342
1405
|
edge_weight: VCS_WEIGHTS.edge,
|
|
1406
|
+
row_weight: VCS_WEIGHTS.row,
|
|
1343
1407
|
subgraph_weight: VCS_WEIGHTS.subgraph,
|
|
1344
1408
|
depth_weight: VCS_WEIGHTS.depth,
|
|
1345
1409
|
preset_name: canonical
|
|
@@ -1367,7 +1431,7 @@ var calculateComplexity = (stats, config) => {
|
|
|
1367
1431
|
const e = stats.edges;
|
|
1368
1432
|
const s = stats.subgraphs;
|
|
1369
1433
|
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
1370
|
-
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
1434
|
+
const base = n + e * config.edge_weight + stats.rows * config.row_weight + s * config.subgraph_weight;
|
|
1371
1435
|
const vcs = base * (1 + d * config.depth_weight);
|
|
1372
1436
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
|
1373
1437
|
return {
|
|
@@ -1403,6 +1467,7 @@ var COMPLEXITY_OPTIONS = {
|
|
|
1403
1467
|
nodeTarget: "number",
|
|
1404
1468
|
vcsTarget: "number",
|
|
1405
1469
|
edgeWeight: "number",
|
|
1470
|
+
rowWeight: "number",
|
|
1406
1471
|
subgraphWeight: "number",
|
|
1407
1472
|
depthWeight: "number"
|
|
1408
1473
|
};
|
|
@@ -2025,6 +2090,7 @@ var sameDiagrams = async (before, after) => {
|
|
|
2025
2090
|
s.subgraphs,
|
|
2026
2091
|
s.max_subgraph_depth,
|
|
2027
2092
|
s.max_tree_depth,
|
|
2093
|
+
s.rows,
|
|
2028
2094
|
s.node_ids,
|
|
2029
2095
|
s.subgraph_names
|
|
2030
2096
|
];
|
|
@@ -15,6 +15,7 @@ export declare const PRESETS: Record<string, Preset>;
|
|
|
15
15
|
export declare const DEFAULT_PRESET = "high-density";
|
|
16
16
|
export interface ThresholdConfig extends Preset {
|
|
17
17
|
edge_weight: number;
|
|
18
|
+
row_weight: number;
|
|
18
19
|
subgraph_weight: number;
|
|
19
20
|
depth_weight: number;
|
|
20
21
|
preset_name: string;
|
|
@@ -31,6 +32,7 @@ export declare const THRESHOLD_OPTIONS: {
|
|
|
31
32
|
readonly nodeTarget: "node_target";
|
|
32
33
|
readonly vcsTarget: "vcs_target";
|
|
33
34
|
readonly edgeWeight: "edge_weight";
|
|
35
|
+
readonly rowWeight: "row_weight";
|
|
34
36
|
readonly subgraphWeight: "subgraph_weight";
|
|
35
37
|
readonly depthWeight: "depth_weight";
|
|
36
38
|
};
|
|
@@ -17,6 +17,7 @@ export interface DiagramStats {
|
|
|
17
17
|
subgraphs: number;
|
|
18
18
|
max_subgraph_depth: number;
|
|
19
19
|
max_tree_depth: number;
|
|
20
|
+
rows: number;
|
|
20
21
|
node_ids: string[];
|
|
21
22
|
subgraph_names: string[];
|
|
22
23
|
parser_used: "langium" | "mermaid-core";
|
|
@@ -35,4 +36,32 @@ export declare function indentTreeStats(items: Array<{
|
|
|
35
36
|
edges: number;
|
|
36
37
|
depth: number;
|
|
37
38
|
};
|
|
39
|
+
type AnyDb = Record<string, unknown>;
|
|
40
|
+
interface SequenceRecord {
|
|
41
|
+
type?: number;
|
|
42
|
+
from?: unknown;
|
|
43
|
+
to?: unknown;
|
|
44
|
+
message?: unknown;
|
|
45
|
+
}
|
|
46
|
+
export declare const SEQUENCE_BLOCK_OPEN: ReadonlySet<number>;
|
|
47
|
+
export declare const SEQUENCE_BLOCK_CLOSE: ReadonlySet<number>;
|
|
48
|
+
export declare const isSequenceMessage: (m: SequenceRecord) => boolean;
|
|
49
|
+
interface StateNode {
|
|
50
|
+
id?: string;
|
|
51
|
+
type?: string;
|
|
52
|
+
shape?: string;
|
|
53
|
+
isGroup?: boolean;
|
|
54
|
+
parentId?: string;
|
|
55
|
+
}
|
|
56
|
+
interface StateEdge {
|
|
57
|
+
start?: string;
|
|
58
|
+
end?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare const isCountedState: (n: {
|
|
61
|
+
shape?: string;
|
|
62
|
+
}) => boolean;
|
|
63
|
+
export declare const countedStateElements: <N extends StateNode, E extends StateEdge>(db: AnyDb) => {
|
|
64
|
+
nodes: N[];
|
|
65
|
+
edges: E[];
|
|
66
|
+
};
|
|
38
67
|
export {};
|
package/package.json
CHANGED
package/src/fix/verify.ts
CHANGED
package/src/parse/graph.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
// the docs site can draw what the parser returned beside the score it earns.
|
|
5
5
|
// Only the types the site draws are read, and any other type throws.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
countedStateElements,
|
|
9
|
+
getMermaidCore,
|
|
10
|
+
isSequenceMessage,
|
|
11
|
+
SEQUENCE_BLOCK_CLOSE,
|
|
12
|
+
SEQUENCE_BLOCK_OPEN,
|
|
13
|
+
} from "./stats.ts";
|
|
8
14
|
|
|
9
15
|
export interface GraphNode {
|
|
10
16
|
id: string;
|
|
@@ -80,11 +86,122 @@ const mindmapGraph = (type: string, db: AnyDb): DiagramGraph => {
|
|
|
80
86
|
return { diagram_type: type, nodes, edges, subgraphs: [] };
|
|
81
87
|
};
|
|
82
88
|
|
|
89
|
+
interface SequenceActor {
|
|
90
|
+
description?: string;
|
|
91
|
+
box?: { name?: string };
|
|
92
|
+
}
|
|
93
|
+
interface SequenceRecord {
|
|
94
|
+
type?: number;
|
|
95
|
+
from?: unknown;
|
|
96
|
+
to?: unknown;
|
|
97
|
+
message?: unknown;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// A participant is a node, and a message an edge between two. A box holds its
|
|
101
|
+
// participants. A block (loop, alt, opt, par and the rest) frames messages,
|
|
102
|
+
// not participants, so it holds no node; it is drawn as an empty boundary,
|
|
103
|
+
// inside the block around it.
|
|
104
|
+
const sequenceGraph = (type: string, db: AnyDb): DiagramGraph => {
|
|
105
|
+
const records = (db.state as { records?: Record<string, unknown> } | undefined)?.records ?? {};
|
|
106
|
+
const actors = records.actors instanceof Map ? (records.actors as Map<string, SequenceActor>) : new Map();
|
|
107
|
+
const boxes = (Array.isArray(records.boxes) ? records.boxes : []) as Array<{ name?: string }>;
|
|
108
|
+
const messages = (Array.isArray(records.messages) ? records.messages : []) as SequenceRecord[];
|
|
109
|
+
const boxId = (i: number): string => `box_${i}`;
|
|
110
|
+
const subgraphs: GraphNode[] = boxes.map((b, i) => ({ id: boxId(i), label: b.name || boxId(i), parent: null }));
|
|
111
|
+
const nodes: GraphNode[] = [...actors].map(([id, a]) => {
|
|
112
|
+
const i = a.box ? boxes.indexOf(a.box) : -1;
|
|
113
|
+
return { id, label: a.description ?? id, parent: i >= 0 ? boxId(i) : null };
|
|
114
|
+
});
|
|
115
|
+
const edges = messages.filter(isSequenceMessage).map((r) => ({ source: r.from as string, target: r.to as string }));
|
|
116
|
+
return { diagram_type: type, nodes, edges, subgraphs: [...subgraphs, ...sequenceBlocks(messages, subgraphs.length)] };
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Each block in statement order, inside the block open around it. Ids number
|
|
120
|
+
// on from the boxes, so a block and a box never share one.
|
|
121
|
+
const sequenceBlocks = (records: SequenceRecord[], first: number): GraphNode[] => {
|
|
122
|
+
const blocks: GraphNode[] = [];
|
|
123
|
+
const open: string[] = [];
|
|
124
|
+
for (const r of records) {
|
|
125
|
+
if (r.type === undefined) continue;
|
|
126
|
+
if (SEQUENCE_BLOCK_OPEN.has(r.type)) {
|
|
127
|
+
const id = `block_${first + blocks.length}`;
|
|
128
|
+
const label = typeof r.message === "string" && r.message ? r.message : id;
|
|
129
|
+
blocks.push({ id, label, parent: open[open.length - 1] ?? null });
|
|
130
|
+
open.push(id);
|
|
131
|
+
} else if (SEQUENCE_BLOCK_CLOSE.has(r.type)) {
|
|
132
|
+
open.pop();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return blocks;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
interface ErEntity {
|
|
139
|
+
id?: string;
|
|
140
|
+
alias?: string;
|
|
141
|
+
}
|
|
142
|
+
interface ErRelationship {
|
|
143
|
+
entityA?: string;
|
|
144
|
+
entityB?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// An entity is a node and a relationship an edge. The db keys entities by
|
|
148
|
+
// name, and a relationship names its two ends by the entity's own id.
|
|
149
|
+
const erGraph = (type: string, db: AnyDb): DiagramGraph => {
|
|
150
|
+
const entities = db.entities instanceof Map ? (db.entities as Map<string, ErEntity>) : new Map();
|
|
151
|
+
const rels = (Array.isArray(db.relationships) ? db.relationships : []) as ErRelationship[];
|
|
152
|
+
const nameOf = new Map([...entities].map(([name, e]) => [e.id ?? name, name]));
|
|
153
|
+
return {
|
|
154
|
+
diagram_type: type,
|
|
155
|
+
nodes: [...entities].map(([name, e]) => ({ id: name, label: e.alias || name, parent: null })),
|
|
156
|
+
edges: rels.map((r) => ({
|
|
157
|
+
source: nameOf.get(r.entityA ?? "") ?? r.entityA ?? "",
|
|
158
|
+
target: nameOf.get(r.entityB ?? "") ?? r.entityB ?? "",
|
|
159
|
+
})),
|
|
160
|
+
subgraphs: [],
|
|
161
|
+
};
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
interface StateNode {
|
|
165
|
+
id?: string;
|
|
166
|
+
label?: string;
|
|
167
|
+
isGroup?: boolean;
|
|
168
|
+
parentId?: string;
|
|
169
|
+
}
|
|
170
|
+
interface StateEdge {
|
|
171
|
+
start?: string;
|
|
172
|
+
end?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The states and transitions stats.ts counts, without start and end markers or
|
|
176
|
+
// notes. A composite state is both a node and the subgraph that holds its
|
|
177
|
+
// children, as the stats count it; the node is named apart from its box, since
|
|
178
|
+
// cytoscape cannot draw one element as both.
|
|
179
|
+
const stateGraph = (type: string, db: AnyDb): DiagramGraph => {
|
|
180
|
+
const { nodes, edges } = countedStateElements<StateNode, StateEdge>(db);
|
|
181
|
+
const groupId = (id: string): string => `${id}__group`;
|
|
182
|
+
const groups = new Set(nodes.filter((n) => n.isGroup).map((n) => n.id ?? ""));
|
|
183
|
+
const parentOf = (n: StateNode): string | null => (n.parentId && groups.has(n.parentId) ? groupId(n.parentId) : null);
|
|
184
|
+
return {
|
|
185
|
+
diagram_type: type,
|
|
186
|
+
nodes: nodes.map((n) => ({
|
|
187
|
+
id: n.id ?? "",
|
|
188
|
+
label: n.label ?? n.id ?? "",
|
|
189
|
+
parent: n.isGroup ? groupId(n.id ?? "") : parentOf(n),
|
|
190
|
+
})),
|
|
191
|
+
edges: edges.map((e) => ({ source: e.start ?? "", target: e.end ?? "" })),
|
|
192
|
+
subgraphs: nodes.filter((n) => n.isGroup).map((n) => ({ id: groupId(n.id ?? ""), label: "", parent: parentOf(n) })),
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
83
196
|
const READERS: Record<string, (type: string, db: AnyDb) => DiagramGraph> = {
|
|
84
197
|
"flowchart-v2": flowchartGraph,
|
|
85
198
|
flowchart: flowchartGraph,
|
|
86
199
|
graph: flowchartGraph,
|
|
87
200
|
mindmap: mindmapGraph,
|
|
201
|
+
sequence: sequenceGraph,
|
|
202
|
+
er: erGraph,
|
|
203
|
+
stateDiagram: stateGraph,
|
|
204
|
+
"stateDiagram-v2": stateGraph,
|
|
88
205
|
};
|
|
89
206
|
|
|
90
207
|
export const extractGraph = async (content: string): Promise<DiagramGraph> => {
|
package/src/parse/metrics.ts
CHANGED
|
@@ -73,10 +73,13 @@ const PRESET_ALIASES: Record<string, string> = {
|
|
|
73
73
|
default: "high-density",
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
// A row (an entity's attribute, a class's member) weighs half an edge: it is
|
|
77
|
+
// text to read, but inside a box the reader has already found.
|
|
78
|
+
const VCS_WEIGHTS = { edge: 0.5, row: 0.25, subgraph: 3.0, depth: 0.1 } as const;
|
|
77
79
|
|
|
78
80
|
export interface ThresholdConfig extends Preset {
|
|
79
81
|
edge_weight: number;
|
|
82
|
+
row_weight: number;
|
|
80
83
|
subgraph_weight: number;
|
|
81
84
|
depth_weight: number;
|
|
82
85
|
preset_name: string;
|
|
@@ -96,6 +99,7 @@ export const THRESHOLD_OPTIONS = {
|
|
|
96
99
|
nodeTarget: "node_target",
|
|
97
100
|
vcsTarget: "vcs_target",
|
|
98
101
|
edgeWeight: "edge_weight",
|
|
102
|
+
rowWeight: "row_weight",
|
|
99
103
|
subgraphWeight: "subgraph_weight",
|
|
100
104
|
depthWeight: "depth_weight",
|
|
101
105
|
} as const;
|
|
@@ -117,6 +121,7 @@ export const configFromPreset = (name: string): ThresholdConfig => {
|
|
|
117
121
|
return {
|
|
118
122
|
...preset,
|
|
119
123
|
edge_weight: VCS_WEIGHTS.edge,
|
|
124
|
+
row_weight: VCS_WEIGHTS.row,
|
|
120
125
|
subgraph_weight: VCS_WEIGHTS.subgraph,
|
|
121
126
|
depth_weight: VCS_WEIGHTS.depth,
|
|
122
127
|
preset_name: canonical,
|
|
@@ -149,7 +154,7 @@ export interface ComplexityMetrics {
|
|
|
149
154
|
const round2 = (x: number): number => Math.round(x * 100) / 100;
|
|
150
155
|
const round4 = (x: number): number => Math.round(x * 10000) / 10000;
|
|
151
156
|
|
|
152
|
-
// (nodes + edges*w_e + subgraphs*w_s) * (1 + depth*w_d). Edge density is
|
|
157
|
+
// (nodes + edges*w_e + rows*w_r + subgraphs*w_s) * (1 + depth*w_d). Edge density is
|
|
153
158
|
// reported as 0 below two nodes rather than dividing by a fallback, which gave
|
|
154
159
|
// nonsense when a parser returned edges but no nodes.
|
|
155
160
|
//
|
|
@@ -164,7 +169,7 @@ export const calculateComplexity = (stats: DiagramStats, config: ThresholdConfig
|
|
|
164
169
|
const e = stats.edges;
|
|
165
170
|
const s = stats.subgraphs;
|
|
166
171
|
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
167
|
-
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
172
|
+
const base = n + e * config.edge_weight + stats.rows * config.row_weight + s * config.subgraph_weight;
|
|
168
173
|
const vcs = base * (1 + d * config.depth_weight);
|
|
169
174
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
|
170
175
|
return {
|
package/src/parse/stats.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// diagram is measured the way mermaid itself reads it.
|
|
6
6
|
|
|
7
7
|
import { parse as langiumParse } from "@mermaid-js/parser";
|
|
8
|
+
import Color from "colorjs.io";
|
|
8
9
|
import { Window } from "happy-dom";
|
|
9
10
|
|
|
10
11
|
// mermaid core registers DOMPurify hooks while its grammars run, and DOMPurify
|
|
@@ -22,6 +23,31 @@ const installDomShim = (): void => {
|
|
|
22
23
|
g.DocumentFragment = win.DocumentFragment;
|
|
23
24
|
g.NodeFilter = win.NodeFilter;
|
|
24
25
|
g.getComputedStyle = win.getComputedStyle.bind(win);
|
|
26
|
+
// happy-dom's CSS.supports("color", v) is true for any word, where a browser
|
|
27
|
+
// checks that v is a colour. Mermaid asks it to split `box Shop` into a fill
|
|
28
|
+
// and a title, so the shim's answer gave every one-word box title away as
|
|
29
|
+
// its fill. A colour is whatever colorjs.io can parse, as in color.ts.
|
|
30
|
+
// happy-dom builds a fresh CSS object on each read, so the window's CSS is
|
|
31
|
+
// replaced by one fixed object that carries the corrected check.
|
|
32
|
+
const css = (win as unknown as { CSS?: { supports: (p: string, v?: string) => boolean } }).CSS;
|
|
33
|
+
if (css) {
|
|
34
|
+
const supports = css.supports.bind(css);
|
|
35
|
+
const fixed = {
|
|
36
|
+
...css,
|
|
37
|
+
supports: (property: string, value?: string): boolean =>
|
|
38
|
+
property === "color" && value !== undefined ? isCssColour(value) : supports(property, value),
|
|
39
|
+
};
|
|
40
|
+
Object.defineProperty(win, "CSS", { value: fixed, configurable: true });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const isCssColour = (value: string): boolean => {
|
|
45
|
+
try {
|
|
46
|
+
new Color(value);
|
|
47
|
+
return true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
25
51
|
};
|
|
26
52
|
|
|
27
53
|
let mermaidCore: typeof import("mermaid").default | null = null;
|
|
@@ -58,6 +84,11 @@ export interface DiagramStats {
|
|
|
58
84
|
// kept apart from max_subgraph_depth and the nesting rule never reads it.
|
|
59
85
|
// It is 0 for every other type.
|
|
60
86
|
max_tree_depth: number;
|
|
87
|
+
// The rows drawn inside a node: an erDiagram entity's attributes, and a
|
|
88
|
+
// classDiagram class's attributes and methods. A row is text to read, but it
|
|
89
|
+
// sits in a box the reader has already found, so it is not a node. It is 0
|
|
90
|
+
// for every other type.
|
|
91
|
+
rows: number;
|
|
61
92
|
node_ids: string[];
|
|
62
93
|
subgraph_names: string[];
|
|
63
94
|
// The parser that read the diagram, or, on a failure, the one whose reason
|
|
@@ -167,6 +198,7 @@ export async function extractStats(content: string): Promise<MermaidStats> {
|
|
|
167
198
|
subgraphs: 0,
|
|
168
199
|
max_subgraph_depth: 0,
|
|
169
200
|
max_tree_depth: 0,
|
|
201
|
+
rows: 0,
|
|
170
202
|
node_ids: [],
|
|
171
203
|
subgraph_names: [],
|
|
172
204
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -233,6 +265,7 @@ async function extractLangiumStats(content: string, keyword: string): Promise<Me
|
|
|
233
265
|
subgraphs: subgraphNames.length,
|
|
234
266
|
max_subgraph_depth: maxDepth,
|
|
235
267
|
max_tree_depth: treeDepth,
|
|
268
|
+
rows: 0,
|
|
236
269
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
237
270
|
subgraph_names: subgraphNames,
|
|
238
271
|
parser_used: "langium",
|
|
@@ -395,6 +428,8 @@ interface Structure {
|
|
|
395
428
|
maxDepth: number;
|
|
396
429
|
// Set only by the tree-shaped types: see max_tree_depth.
|
|
397
430
|
treeDepth?: number;
|
|
431
|
+
// Set only by the types that draw rows inside a node: see rows.
|
|
432
|
+
rows?: number;
|
|
398
433
|
}
|
|
399
434
|
type CoreAdapter = (db: AnyDb) => Structure;
|
|
400
435
|
|
|
@@ -417,7 +452,7 @@ async function extractCoreStats(content: string, keyword: string): Promise<Merma
|
|
|
417
452
|
if (!adapter) {
|
|
418
453
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
419
454
|
}
|
|
420
|
-
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
455
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0, rows = 0 } = adapter(diagram.db);
|
|
421
456
|
|
|
422
457
|
return {
|
|
423
458
|
nodes: nodeIds.length,
|
|
@@ -425,6 +460,7 @@ async function extractCoreStats(content: string, keyword: string): Promise<Merma
|
|
|
425
460
|
subgraphs: subgraphNames.length,
|
|
426
461
|
max_subgraph_depth: maxDepth,
|
|
427
462
|
max_tree_depth: treeDepth,
|
|
463
|
+
rows,
|
|
428
464
|
node_ids: [...nodeIds].sort(),
|
|
429
465
|
subgraph_names: subgraphNames,
|
|
430
466
|
parser_used: "mermaid-core",
|
|
@@ -491,30 +527,88 @@ function adaptFlowchart(db: AnyDb): Structure {
|
|
|
491
527
|
};
|
|
492
528
|
}
|
|
493
529
|
|
|
530
|
+
const lengthOf = (x: unknown): number => (Array.isArray(x) ? x.length : 0);
|
|
531
|
+
|
|
494
532
|
function adaptClass(db: AnyDb): Structure {
|
|
495
533
|
// classDb exposes data on direct properties: db.classes (Map), db.relations (Array).
|
|
496
|
-
const classes = (db.classes instanceof Map ? db.classes : new Map()) as Map<
|
|
534
|
+
const classes = (db.classes instanceof Map ? db.classes : new Map()) as Map<
|
|
535
|
+
string,
|
|
536
|
+
{ members?: unknown; methods?: unknown }
|
|
537
|
+
>;
|
|
497
538
|
const relations = (Array.isArray(db.relations) ? db.relations : []) as Array<unknown>;
|
|
498
|
-
|
|
539
|
+
// Each namespace draws a box, and one may sit inside another through parent.
|
|
540
|
+
const namespaces = (db.namespaces instanceof Map ? db.namespaces : new Map()) as Map<
|
|
541
|
+
string,
|
|
542
|
+
{ label?: string; parent?: string }
|
|
543
|
+
>;
|
|
544
|
+
const groups = [...namespaces].map(([id, ns]) => ({ id, in: ns.parent, label: ns.label || id }));
|
|
545
|
+
let rows = 0;
|
|
546
|
+
for (const c of classes.values()) rows += lengthOf(c.members) + lengthOf(c.methods);
|
|
547
|
+
return {
|
|
548
|
+
nodeIds: [...classes.keys()],
|
|
549
|
+
edges: relations.length,
|
|
550
|
+
subgraphNames: groups.map((g) => g.label),
|
|
551
|
+
maxDepth: architectureDepth(groups),
|
|
552
|
+
rows,
|
|
553
|
+
};
|
|
499
554
|
}
|
|
500
555
|
|
|
556
|
+
interface SequenceRecord {
|
|
557
|
+
type?: number;
|
|
558
|
+
from?: unknown;
|
|
559
|
+
to?: unknown;
|
|
560
|
+
message?: unknown;
|
|
561
|
+
}
|
|
501
562
|
interface SequenceState {
|
|
502
563
|
records?: {
|
|
503
564
|
actors?: Map<string, unknown>;
|
|
504
565
|
messages?: Array<unknown>;
|
|
566
|
+
boxes?: Array<{ name?: string }>;
|
|
505
567
|
};
|
|
506
568
|
}
|
|
569
|
+
// Mermaid's sequence LINETYPE codes. Its messages list holds every statement in
|
|
570
|
+
// order: a message, but also a note, an activation and each line that opens,
|
|
571
|
+
// divides or closes a block. A block opens with one code, may be divided
|
|
572
|
+
// (else, and, option), and closes with another. A par_over block closes with
|
|
573
|
+
// PAR_END, like par.
|
|
574
|
+
const SEQ_NOTE = 2;
|
|
575
|
+
// loop, alt, opt, par, critical, break, par_over. A `rect` (22, 23) is left
|
|
576
|
+
// out: it tints the background behind its messages, with no title or border,
|
|
577
|
+
// so it is a highlight rather than a boundary to track.
|
|
578
|
+
export const SEQUENCE_BLOCK_OPEN: ReadonlySet<number> = new Set([10, 12, 15, 19, 27, 30, 32]);
|
|
579
|
+
export const SEQUENCE_BLOCK_CLOSE: ReadonlySet<number> = new Set([11, 14, 16, 21, 29, 31]);
|
|
580
|
+
|
|
581
|
+
// A message runs from one participant to another (or itself). A note also
|
|
582
|
+
// names both, so it is told apart by its code, and an activation names only one.
|
|
583
|
+
export const isSequenceMessage = (m: SequenceRecord): boolean =>
|
|
584
|
+
m.type !== SEQ_NOTE && typeof m.from === "string" && typeof m.to === "string";
|
|
585
|
+
|
|
507
586
|
function adaptSequence(db: AnyDb): Structure {
|
|
508
|
-
// sequenceDb stores state under db.state.records.{actors,messages}.
|
|
587
|
+
// sequenceDb stores state under db.state.records.{actors,messages,boxes}.
|
|
509
588
|
const state = (db.state as SequenceState | undefined)?.records;
|
|
510
589
|
const actors = state?.actors instanceof Map ? state.actors : new Map<string, unknown>();
|
|
511
590
|
const rawMessages = state?.messages;
|
|
512
|
-
const
|
|
591
|
+
const records = (Array.isArray(rawMessages) ? rawMessages : []) as SequenceRecord[];
|
|
592
|
+
const boxes = Array.isArray(state?.boxes) ? state.boxes : [];
|
|
593
|
+
// A box groups participants and a block frames messages: both are drawn as
|
|
594
|
+
// a labelled boundary. Boxes cannot nest, and blocks nest inside each other.
|
|
595
|
+
const subgraphNames = boxes.map((b, i) => b.name || `box_${i}`);
|
|
596
|
+
let depth = 0;
|
|
597
|
+
let maxBlockDepth = 0;
|
|
598
|
+
for (const r of records) {
|
|
599
|
+
if (r.type !== undefined && SEQUENCE_BLOCK_OPEN.has(r.type)) {
|
|
600
|
+
depth++;
|
|
601
|
+
maxBlockDepth = Math.max(maxBlockDepth, depth);
|
|
602
|
+
subgraphNames.push(typeof r.message === "string" && r.message ? r.message : `block_${subgraphNames.length}`);
|
|
603
|
+
} else if (r.type !== undefined && SEQUENCE_BLOCK_CLOSE.has(r.type)) {
|
|
604
|
+
depth = Math.max(0, depth - 1);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
513
607
|
return {
|
|
514
608
|
nodeIds: [...actors.keys()],
|
|
515
|
-
edges:
|
|
516
|
-
subgraphNames
|
|
517
|
-
maxDepth: 0,
|
|
609
|
+
edges: records.filter(isSequenceMessage).length,
|
|
610
|
+
subgraphNames,
|
|
611
|
+
maxDepth: Math.max(boxes.length > 0 ? 1 : 0, maxBlockDepth),
|
|
518
612
|
};
|
|
519
613
|
}
|
|
520
614
|
|
|
@@ -572,13 +666,16 @@ function adaptC4(db: AnyDb): Structure {
|
|
|
572
666
|
function adaptEr(db: AnyDb): Structure {
|
|
573
667
|
// ER db exposes data on direct properties. entities is a Map keyed on
|
|
574
668
|
// entity name (CAR, NAMED-DRIVER); relationships is an Array.
|
|
575
|
-
const entities = db.entities instanceof Map ? db.entities : new Map<string, unknown
|
|
669
|
+
const entities = (db.entities instanceof Map ? db.entities : new Map()) as Map<string, { attributes?: unknown }>;
|
|
576
670
|
const relationships = (Array.isArray(db.relationships) ? db.relationships : []) as Array<unknown>;
|
|
671
|
+
let rows = 0;
|
|
672
|
+
for (const e of entities.values()) rows += lengthOf(e.attributes);
|
|
577
673
|
return {
|
|
578
674
|
nodeIds: [...entities.keys()],
|
|
579
675
|
edges: relationships.length,
|
|
580
676
|
subgraphNames: [],
|
|
581
677
|
maxDepth: 0,
|
|
678
|
+
rows,
|
|
582
679
|
};
|
|
583
680
|
}
|
|
584
681
|
|
|
@@ -713,21 +810,42 @@ function adaptSankey(db: AnyDb): Structure {
|
|
|
713
810
|
interface StateNode {
|
|
714
811
|
id?: string;
|
|
715
812
|
type?: string;
|
|
813
|
+
shape?: string;
|
|
716
814
|
isGroup?: boolean;
|
|
717
815
|
parentId?: string;
|
|
718
816
|
}
|
|
719
817
|
interface StateEdge {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
}
|
|
818
|
+
start?: string;
|
|
819
|
+
end?: string;
|
|
820
|
+
}
|
|
821
|
+
// The shapes stateDb gives what is not a state: the start and end markers
|
|
822
|
+
// ([*], one pair per composite state), and a note with the group mermaid wraps
|
|
823
|
+
// it in. A marker only says where a path begins or ends, and a note is an
|
|
824
|
+
// annotation, as notes are in every other type.
|
|
825
|
+
const STATE_NOT_A_STATE = new Set(["stateStart", "stateEnd", "note", "noteGroup"]);
|
|
826
|
+
|
|
827
|
+
export const isCountedState = (n: { shape?: string }): boolean => !STATE_NOT_A_STATE.has(n.shape ?? "");
|
|
828
|
+
|
|
829
|
+
// The states and transitions a stateDiagram draws, without markers or notes.
|
|
830
|
+
// A transition into or out of a marker goes with it, as does a note's
|
|
831
|
+
// connector. graph.ts draws the same set, so the two never disagree.
|
|
832
|
+
export const countedStateElements = <N extends StateNode, E extends StateEdge>(
|
|
833
|
+
db: AnyDb,
|
|
834
|
+
): { nodes: N[]; edges: E[] } => {
|
|
835
|
+
const all = (db.nodes as N[] | undefined) ?? [];
|
|
836
|
+
const nodes = all.filter(isCountedState);
|
|
837
|
+
const kept = new Set(nodes.map((n) => n.id ?? ""));
|
|
838
|
+
const edges = ((db.edges as E[] | undefined) ?? []).filter((e) => kept.has(e.start ?? "") && kept.has(e.end ?? ""));
|
|
839
|
+
return { nodes, edges };
|
|
840
|
+
};
|
|
841
|
+
|
|
723
842
|
// A composite state (`state A { ... }`) is a group that holds other states,
|
|
724
843
|
// so it counts as a subgraph too, and its nesting depth is read from each
|
|
725
844
|
// state's parentId chain. It stays a node as well, because it is drawn as a
|
|
726
845
|
// labelled state.
|
|
727
846
|
function adaptState(db: AnyDb): Structure {
|
|
728
847
|
// stateDiagram db exposes data on direct properties.
|
|
729
|
-
const nodes
|
|
730
|
-
const edges = (db.edges as StateEdge[] | undefined) ?? [];
|
|
848
|
+
const { nodes, edges } = countedStateElements<StateNode, StateEdge>(db);
|
|
731
849
|
const groups = nodes.filter((n) => n.isGroup === true);
|
|
732
850
|
const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
|
|
733
851
|
let maxDepth = 0;
|