@jpeakai/mermaidjs-lint 0.1.1 → 0.3.0

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