@jpeakai/mermaidjs-lint 0.7.3 → 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 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.7.3",
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
- return { nodeIds: [...classes.keys()], edges: relations.length, subgraphNames: [], maxDepth: 0 };
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 messages = Array.isArray(rawMessages) ? rawMessages : [];
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: messages.length,
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.nodes ?? [];
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
- return { nodeIds: [...classes.keys()], edges: relations.length, subgraphNames: [], maxDepth: 0 };
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 messages = Array.isArray(rawMessages) ? rawMessages : [];
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: messages.length,
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.nodes ?? [];
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.7.3",
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
- return { nodeIds: [...classes.keys()], edges: relations.length, subgraphNames: [], maxDepth: 0 };
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 messages = Array.isArray(rawMessages) ? rawMessages : [];
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: messages.length,
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.nodes ?? [];
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpeakai/mermaidjs-lint",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
6
6
  "license": "MIT",
package/src/fix/verify.ts CHANGED
@@ -57,6 +57,7 @@ const sameDiagrams = async (before: DocModel, after: DocModel): Promise<boolean>
57
57
  s.subgraphs,
58
58
  s.max_subgraph_depth,
59
59
  s.max_tree_depth,
60
+ s.rows,
60
61
  s.node_ids,
61
62
  s.subgraph_names,
62
63
  ];
@@ -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 { getMermaidCore } from "./stats.ts";
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> => {
@@ -73,10 +73,13 @@ const PRESET_ALIASES: Record<string, string> = {
73
73
  default: "high-density",
74
74
  };
75
75
 
76
- const VCS_WEIGHTS = { edge: 0.5, subgraph: 3.0, depth: 0.1 } as const;
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 {
@@ -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<string, unknown>;
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
- return { nodeIds: [...classes.keys()], edges: relations.length, subgraphNames: [], maxDepth: 0 };
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 messages = Array.isArray(rawMessages) ? rawMessages : [];
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: messages.length,
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
- id1?: string;
721
- id2?: string;
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 = (db.nodes as StateNode[] | undefined) ?? [];
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;
@@ -35,6 +35,7 @@ export const COMPLEXITY_OPTIONS: OptionSpec = {
35
35
  nodeTarget: "number",
36
36
  vcsTarget: "number",
37
37
  edgeWeight: "number",
38
+ rowWeight: "number",
38
39
  subgraphWeight: "number",
39
40
  depthWeight: "number",
40
41
  };