@jpeakai/mermaidjs-lint 0.2.0 → 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 (54) hide show
  1. package/README.md +7 -5
  2. package/dist/bin.js +174 -55
  3. package/dist/index.js +177 -55
  4. package/dist/testing.js +59 -23
  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/package.json +5 -4
  43. package/src/cli.ts +47 -15
  44. package/src/errors.ts +46 -0
  45. package/src/files.ts +21 -2
  46. package/src/fix/engine.ts +22 -10
  47. package/src/fix/verify.ts +6 -1
  48. package/src/index.ts +5 -3
  49. package/src/parse/stats.ts +39 -11
  50. package/src/plugins/contract.ts +4 -2
  51. package/src/plugins/discover.ts +38 -15
  52. package/src/plugins/index.ts +5 -3
  53. package/src/plugins/load.ts +32 -1
  54. package/src/project.ts +3 -1
package/dist/index.js CHANGED
@@ -547,12 +547,10 @@ function adaptMindmap(db) {
547
547
  }
548
548
  function adaptQuadrantChart(db) {
549
549
  const data = callGetter(db, "getQuadrantData");
550
- const quadrants = data?.quadrants ?? [];
551
- const labels = quadrants.map((q) => {
552
- const t = q.text;
553
- return typeof t === "string" ? t : t?.text ?? "";
554
- });
555
- return { nodeIds: labels, edges: 0, subgraphNames: [], maxDepth: 0 };
550
+ const textOf = (t) => typeof t === "string" ? t : t?.text ?? "";
551
+ const quadrants = (data?.quadrants ?? []).map((q) => textOf(q.text)).filter((t) => t.trim() !== "");
552
+ const points = (data?.points ?? []).map((p, i) => textOf(p.text) || `point_${i}`);
553
+ return { nodeIds: [...quadrants, ...points], edges: 0, subgraphNames: [], maxDepth: 0 };
556
554
  }
557
555
  function adaptRequirement(db) {
558
556
  const requirements = db.requirements instanceof Map ? db.requirements : new Map;
@@ -578,18 +576,30 @@ function adaptSankey(db) {
578
576
  function adaptState(db) {
579
577
  const nodes = db.nodes ?? [];
580
578
  const edges = db.edges ?? [];
579
+ const groups = nodes.filter((n) => n.isGroup === true);
580
+ const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
581
+ let maxDepth = 0;
582
+ for (const g of groups) {
583
+ let depth = 1;
584
+ const seen = new Set;
585
+ for (let cur = g.parentId;cur && !seen.has(cur); cur = parentOf.get(cur)) {
586
+ seen.add(cur);
587
+ depth++;
588
+ }
589
+ maxDepth = Math.max(maxDepth, depth);
590
+ }
581
591
  return {
582
592
  nodeIds: nodes.map((n) => n.id ?? ""),
583
593
  edges: edges.length,
584
- subgraphNames: [],
585
- maxDepth: 0
594
+ subgraphNames: groups.map((g) => g.id ?? ""),
595
+ maxDepth
586
596
  };
587
597
  }
588
598
  function adaptTimeline(db) {
589
599
  const tasks = callGetter(db, "getTasks") ?? [];
590
600
  const sections = callGetter(db, "getSections") ?? [];
591
601
  return {
592
- nodeIds: tasks.map((t) => t.task ?? ""),
602
+ nodeIds: tasks.flatMap((t) => [t.task ?? "", ...(t.events ?? []).map((e) => `${t.task ?? ""}: ${e}`)]),
593
603
  edges: 0,
594
604
  subgraphNames: sections.map((s) => s.sectionName ?? s.section ?? ""),
595
605
  maxDepth: sections.length > 0 ? 1 : 0
@@ -619,7 +629,10 @@ function adaptVenn(db) {
619
629
  function adaptXychart(db) {
620
630
  const data = callGetter(db, "getXYChartData");
621
631
  const categories = data?.xAxis?.categories ?? [];
622
- return { nodeIds: categories, edges: 0, subgraphNames: [], maxDepth: 0 };
632
+ if (categories.length > 0)
633
+ return { nodeIds: categories, edges: 0, subgraphNames: [], maxDepth: 0 };
634
+ const longest = Math.max(0, ...(data?.plots ?? []).map((p) => p.data?.length ?? 0));
635
+ return { nodeIds: Array.from({ length: longest }, (_, i) => `x_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
623
636
  }
624
637
  function subgraphNestingDepth(subGraphs) {
625
638
  if (subGraphs.length === 0)
@@ -846,6 +859,34 @@ class RuleOptionError extends UsageError {
846
859
  }
847
860
  }
848
861
 
862
+ class FixOutsideRootError extends UsageError {
863
+ file;
864
+ target;
865
+ constructor(file, target, root) {
866
+ super(`${file} is a symbolic link to ${target}, outside the project root ${root}; --fix does not write through it. Fix the target in its own project, or name it directly`, "fix-outside-root");
867
+ this.file = file;
868
+ this.target = target;
869
+ }
870
+ }
871
+
872
+ class PackageJsonError extends MermaidLintError {
873
+ file;
874
+ constructor(file) {
875
+ super("package-json", `${file} is not a JSON object; fix or remove it`);
876
+ this.file = file;
877
+ }
878
+ }
879
+
880
+ class FixNotConvergingError extends MermaidLintError {
881
+ file;
882
+ rules;
883
+ constructor(file, passes, rules) {
884
+ 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`);
885
+ this.file = file;
886
+ this.rules = rules;
887
+ }
888
+ }
889
+
849
890
  class PluginLoadError extends MermaidLintError {
850
891
  source;
851
892
  constructor(source, reason, code = "plugin") {
@@ -1707,13 +1748,13 @@ var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
1707
1748
  };
1708
1749
  var checkMarkdown = (src, file, flags = {}, registry = BUILTIN) => checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
1709
1750
  // src/cli.ts
1710
- import { readFile as readFile3, writeFile } from "node:fs/promises";
1711
- import { relative } from "node:path";
1751
+ import { lstat, readFile as readFile3, realpath, writeFile } from "node:fs/promises";
1752
+ import { isAbsolute as isAbsolute2, relative, resolve as resolve5 } from "node:path";
1712
1753
  import { parseArgs } from "node:util";
1713
1754
  // package.json
1714
1755
  var package_default = {
1715
1756
  name: "@jpeakai/mermaidjs-lint",
1716
- version: "0.2.0",
1757
+ version: "0.3.0",
1717
1758
  type: "module",
1718
1759
  description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
1719
1760
  license: "MIT",
@@ -1742,15 +1783,16 @@ var package_default = {
1742
1783
  bin: {
1743
1784
  "mermaidjs-lint": "dist/bin.js"
1744
1785
  },
1786
+ types: "./dist/types/index.d.ts",
1745
1787
  exports: {
1746
1788
  ".": {
1789
+ types: "./dist/types/index.d.ts",
1747
1790
  bun: "./src/index.ts",
1748
- types: "./src/index.ts",
1749
1791
  default: "./dist/index.js"
1750
1792
  },
1751
1793
  "./testing": {
1794
+ types: "./dist/types/testing.d.ts",
1752
1795
  bun: "./src/testing.ts",
1753
- types: "./src/testing.ts",
1754
1796
  default: "./dist/testing.js"
1755
1797
  }
1756
1798
  },
@@ -1764,6 +1806,7 @@ var package_default = {
1764
1806
  ],
1765
1807
  dependencies: {
1766
1808
  "@mermaid-js/parser": "^1.1.0",
1809
+ "@types/mdast": "^4",
1767
1810
  "colorjs.io": "^0.6.1",
1768
1811
  "happy-dom": "^20.9.0",
1769
1812
  "mdast-util-from-markdown": "^2.0.3",
@@ -1777,7 +1820,6 @@ var package_default = {
1777
1820
  "@biomejs/biome": "^2",
1778
1821
  "@jpeakai/prose-gates": "^0.2.0",
1779
1822
  "@types/bun": "latest",
1780
- "@types/mdast": "^4",
1781
1823
  typescript: "^5"
1782
1824
  }
1783
1825
  };
@@ -1788,9 +1830,17 @@ import { extname, join as join2, resolve as resolve2 } from "node:path";
1788
1830
  var DIAGRAM_FILE_EXTS = new Set([...DIAGRAM_EXTS, ".md", ".markdown"]);
1789
1831
  var SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tmp", "__pycache__"]);
1790
1832
  var skipDir = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
1833
+ var linksToDir = (path) => {
1834
+ try {
1835
+ return statSync(path).isDirectory();
1836
+ } catch {
1837
+ return false;
1838
+ }
1839
+ };
1791
1840
  var collectFiles = (paths) => {
1792
1841
  const files = [];
1793
1842
  const unreadable = [];
1843
+ const symlinks = [];
1794
1844
  const seen = new Set;
1795
1845
  const walk = (dir) => {
1796
1846
  let entries;
@@ -1802,7 +1852,11 @@ var collectFiles = (paths) => {
1802
1852
  }
1803
1853
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1804
1854
  const abs = join2(dir, entry.name);
1805
- if (entry.isDirectory()) {
1855
+ if (entry.isSymbolicLink()) {
1856
+ if (!skipDir(entry.name) && (DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase()) || linksToDir(abs))) {
1857
+ symlinks.push(abs);
1858
+ }
1859
+ } else if (entry.isDirectory()) {
1806
1860
  if (!skipDir(entry.name))
1807
1861
  walk(abs);
1808
1862
  } else if (entry.isFile() && DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase())) {
@@ -1827,7 +1881,7 @@ var collectFiles = (paths) => {
1827
1881
  unreadable.push(p);
1828
1882
  }
1829
1883
  }
1830
- return { files, unreadable };
1884
+ return { files, unreadable, symlinks };
1831
1885
  };
1832
1886
 
1833
1887
  // src/fix/verify.ts
@@ -1878,13 +1932,17 @@ var sameDiagrams = async (before, after) => {
1878
1932
  };
1879
1933
  var verify = async (edit, before, after) => {
1880
1934
  if (edit.expect.kind === "same-tree") {
1881
- return sameTree(before.tree, after.tree, {}) ? "accept" : "refuse";
1935
+ if (!sameTree(before.tree, after.tree, {}))
1936
+ return "refuse";
1937
+ if (before.kind === "markdown" && after.kind === "markdown")
1938
+ return "accept";
1882
1939
  }
1883
1940
  return await sameDiagrams(before, after) ? "accept" : "refuse";
1884
1941
  };
1885
1942
 
1886
1943
  // src/fix/engine.ts
1887
1944
  var MAX_PASSES = 1000;
1945
+ var RECENT_PASSES = 50;
1888
1946
  var splice = (src, edits) => {
1889
1947
  let out = src;
1890
1948
  for (const e of [...edits].sort((a, b) => b.start - a.start))
@@ -1902,6 +1960,7 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
1902
1960
  const refused = [];
1903
1961
  const refusedKeys = new Set;
1904
1962
  let current = src;
1963
+ const keptBy = [];
1905
1964
  for (let pass = 0;pass < MAX_PASSES; pass++) {
1906
1965
  const doc = buildDocModel(current, kind);
1907
1966
  const keyOf = (e) => `${e.rule}\x00${current.slice(e.start, e.end)}\x00${e.text}`;
@@ -1934,23 +1993,26 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
1934
1993
  continue;
1935
1994
  if (edits.length > 1 && !overlaps(edits) && await attempt(edits)) {
1936
1995
  progressed = true;
1937
- break;
1938
- }
1939
- for (const edit of edits) {
1940
- if (await attempt([edit])) {
1941
- progressed = true;
1942
- break;
1996
+ } else {
1997
+ for (const edit of edits) {
1998
+ if (await attempt([edit])) {
1999
+ progressed = true;
2000
+ break;
2001
+ }
2002
+ refusedKeys.add(keyOf(edit));
2003
+ refused.push(edit);
1943
2004
  }
1944
- refusedKeys.add(keyOf(edit));
1945
- refused.push(edit);
1946
2005
  }
1947
- if (progressed)
2006
+ if (progressed) {
2007
+ keptBy.push(rule.id);
1948
2008
  break;
2009
+ }
1949
2010
  }
1950
2011
  if (!progressed)
1951
2012
  return { output: current, applied, refused };
1952
2013
  }
1953
- throw new Error(`fix did not converge after ${MAX_PASSES} passes; a fixer keeps proposing edits`);
2014
+ const lingering = [...new Set(keptBy.slice(-RECENT_PASSES))].sort();
2015
+ throw new FixNotConvergingError(file, MAX_PASSES, lingering);
1954
2016
  };
1955
2017
  var fixSource = async (src, file, flags = {}, registry = BUILTIN) => (await fixSourceReport(src, file, flags, registry)).output;
1956
2018
 
@@ -2053,7 +2115,7 @@ var readJson = async (path) => {
2053
2115
  if (isRecord2(value))
2054
2116
  return value;
2055
2117
  } catch {}
2056
- throw new Error(`${path} is not a JSON object`);
2118
+ throw new PackageJsonError(path);
2057
2119
  };
2058
2120
  var LOCAL_EXTENSIONS = new Set([".js", ".mjs", ".ts", ".mts", ".cts"]);
2059
2121
  var localSources = async (root) => {
@@ -2097,7 +2159,14 @@ var entryOf = (pkg) => {
2097
2159
  var resolvePackage = async (root, name) => {
2098
2160
  for (let dir = resolve3(root);; dir = dirname2(dir)) {
2099
2161
  const folder = join4(dir, "node_modules", name);
2100
- 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
+ }
2101
2170
  if (pkg)
2102
2171
  return join4(folder, entryOf(pkg));
2103
2172
  if (dirname2(dir) === dir)
@@ -2105,8 +2174,15 @@ var resolvePackage = async (root, name) => {
2105
2174
  }
2106
2175
  throw new PluginNotInstalledError(name, root);
2107
2176
  };
2108
- var declaredPlugins = async (root) => {
2109
- 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
+ }
2110
2186
  if (!pkg)
2111
2187
  return [];
2112
2188
  const names = ["dependencies", "devDependencies", "optionalDependencies"].flatMap((field) => {
@@ -2115,19 +2191,19 @@ var declaredPlugins = async (root) => {
2115
2191
  });
2116
2192
  return [...new Set(names)].filter((name) => PACKAGE_PATTERN.test(name)).sort();
2117
2193
  };
2118
- var discoverSources = async (root, config) => {
2194
+ var discoverSources = async (root, config, pluginsOff = false) => {
2119
2195
  const sources = await localSources(root);
2120
2196
  const failures = [];
2121
2197
  const add = async (kind, spec, resolveIt) => {
2122
2198
  try {
2123
2199
  sources.push({ kind, spec, path: await resolveIt() });
2124
2200
  } catch (err) {
2125
- if (!(err instanceof PluginNotInstalledError))
2201
+ if (!(err instanceof PluginLoadError))
2126
2202
  throw err;
2127
- failures.push(err);
2203
+ failures.push({ kind, error: err });
2128
2204
  }
2129
2205
  };
2130
- for (const name of await declaredPlugins(root))
2206
+ for (const name of await declaredPlugins(root, pluginsOff))
2131
2207
  await add("package", name, () => resolvePackage(root, name));
2132
2208
  for (const spec of config.plugins || []) {
2133
2209
  const path = spec.startsWith(".") || isAbsolute(spec);
@@ -2144,8 +2220,8 @@ import { pathToFileURL as pathToFileURL2 } from "node:url";
2144
2220
  // src/plugins/contract.ts
2145
2221
  var API_VERSION = 1;
2146
2222
  var CATEGORY_WORD = /^[a-z]+$/;
2147
- var NAMESPACE = /^[a-z][a-z0-9-]*$/;
2148
- 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]+)*$/;
2149
2225
 
2150
2226
  // src/plugins/runtime.ts
2151
2227
  var canImportTypeScript = (runtime = process) => typeof runtime.versions.bun === "string" || Boolean(runtime.features.typescript);
@@ -2172,6 +2248,7 @@ var importModule = async (source, runtime) => {
2172
2248
  }
2173
2249
  };
2174
2250
  var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]"]);
2251
+ var RESERVED_OPTIONS = ["preset", "profile"];
2175
2252
  var checkRule = (spec, key, raw, own) => {
2176
2253
  const where = `rule "${key}"`;
2177
2254
  if (!isRecord3(raw))
@@ -2201,6 +2278,11 @@ var checkRule = (spec, key, raw, own) => {
2201
2278
  if (!valid) {
2202
2279
  throw new PluginContractError(spec, `${where} options must map names to number, string, boolean or string[]`);
2203
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
+ }
2204
2286
  }
2205
2287
  return raw;
2206
2288
  };
@@ -2252,7 +2334,18 @@ var loadLocal = async (sources, runtime) => {
2252
2334
  const categories = {};
2253
2335
  const owner = {};
2254
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
+ }
2255
2346
  for (const source of sources) {
2347
+ if ((byKey.get(keyOf(source))?.length ?? 0) > 1)
2348
+ continue;
2256
2349
  try {
2257
2350
  const mod = await importModule(source, runtime);
2258
2351
  if (mod.default === undefined) {
@@ -2266,7 +2359,7 @@ var loadLocal = async (sources, runtime) => {
2266
2359
  categories[word] = description;
2267
2360
  owner[word] = source.spec;
2268
2361
  }
2269
- loaded.push({ key: basename(source.path, extname4(source.path)), source, raw: mod.default });
2362
+ loaded.push({ key: keyOf(source), source, raw: mod.default });
2270
2363
  } catch (err) {
2271
2364
  fail(err);
2272
2365
  }
@@ -2330,7 +2423,7 @@ var toRule = (namespace, key, rule) => {
2330
2423
 
2331
2424
  // src/plugins/index.ts
2332
2425
  var loadPlugins = async (root, config, mode) => {
2333
- const { sources: found, failures: unresolved } = await discoverSources(root, config);
2426
+ const { sources: found, failures: unresolved } = await discoverSources(root, config, mode === "off");
2334
2427
  const set = {
2335
2428
  rules: [],
2336
2429
  categories: new Map(BUILTIN_CATEGORIES),
@@ -2338,16 +2431,17 @@ var loadPlugins = async (root, config, mode) => {
2338
2431
  skipped: [],
2339
2432
  failures: []
2340
2433
  };
2341
- if (mode === "off")
2342
- 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
+ }
2343
2437
  const settle = (error, kind) => {
2344
2438
  if (mode === "strict")
2345
2439
  throw error;
2346
2440
  if (!set.failures.some((f) => f.source === error.source))
2347
2441
  set.failures.push({ source: error.source, kind, error });
2348
2442
  };
2349
- for (const error of unresolved)
2350
- settle(error, "package");
2443
+ for (const { error, kind } of unresolved)
2444
+ settle(error, kind);
2351
2445
  const loaded = [];
2352
2446
  const locals = found.filter((s) => s.kind === "local");
2353
2447
  if (locals.length > 0) {
@@ -2444,7 +2538,7 @@ var setUp = async ({
2444
2538
  config,
2445
2539
  ignoreUnknownPluginIds: mode !== "strict"
2446
2540
  });
2447
- return { registry, skipped: plugins.skipped, failures: plugins.failures };
2541
+ return { root, registry, skipped: plugins.skipped, failures: plugins.failures };
2448
2542
  };
2449
2543
 
2450
2544
  // src/cli.ts
@@ -2453,17 +2547,21 @@ var SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME]
2453
2547
  mermaidjs-lint --list-rules
2454
2548
  mermaidjs-lint --validate-plugins [--json] [--config FILE]
2455
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));
2456
2552
  var USAGE = `${SYNOPSIS}
2457
2553
 
2458
2554
  Deterministic Mermaid diagram gates (* = autofixable):
2459
- ${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(`
2460
2556
  `)).join(`
2461
2557
  `)}
2462
2558
 
2463
2559
  A path is a file or a directory, and every path must exist. A file ending in
2464
2560
  ${DIAGRAM_EXTS.join(" or ")} is read as one raw diagram, and any other file is read as markdown
2465
2561
  whose mermaid fences are checked. A directory is walked for ${[...DIAGRAM_FILE_EXTS].join(", ")} files,
2466
- skipping dot-folders and ${[...SKIP_DIRS].join(", ")}.
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.
2467
2565
  Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/
2468
2566
 
2469
2567
  -p, --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
@@ -2474,14 +2572,16 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
2474
2572
  --fix applies every fix it can prove safe, then reports what remains.
2475
2573
  A fixer that is unsure leaves the text alone and the finding stays.
2476
2574
  --config FILE reads that config instead of mermaidjs-lint.config.json or .mjs at the project root.
2477
- Plugins load on their own from .mermaidjs-lint/rules and from dependencies named mermaidjs-lint-plugin-*.
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.
2478
2577
  --no-plugins runs the built-in rules only and loads no plugin. It reads a .json config as usual,
2479
2578
  and stops with a usage error rather than import a .mjs config, which is code.
2480
2579
  --lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
2481
- --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.
2482
2581
  --list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
2582
+ It imports the plugins to list them.
2483
2583
  --version prints the version, to quote in a bug report.
2484
- Exits 0 when no findings remain, 1 when findings remain or a plugin fails to load (a lenient run
2584
+ Exits 0 when no findings remain, 1 when findings remain, a plugin fails to load or the run fails (a lenient run
2485
2585
  skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
2486
2586
  var parse2 = (argv) => parseArgs({
2487
2587
  args: argv,
@@ -2517,13 +2617,14 @@ var skippedLines = (failures, ignored) => [
2517
2617
  ];
2518
2618
  var listRules = (registry) => {
2519
2619
  const lines = [];
2620
+ const width = idWidth(registry.rules);
2520
2621
  for (const [category, description] of registry.categories) {
2521
2622
  const rules = registry.rules.filter((r) => r.category === category);
2522
2623
  if (rules.length === 0)
2523
2624
  continue;
2524
2625
  lines.push(`${category}: ${description}`);
2525
2626
  for (const r of rules)
2526
- lines.push(` ${r.id}${r.fix ? "*" : " "} ${r.summary}`);
2627
+ lines.push(` ${ruleRow(r, width)}`);
2527
2628
  }
2528
2629
  if (registry.sources.length > 0) {
2529
2630
  lines.push("", "plugins:");
@@ -2566,6 +2667,17 @@ var display = (file, cwd) => {
2566
2667
  const rel = relative(cwd, file);
2567
2668
  return !rel || rel.startsWith("..") ? file : rel;
2568
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
+ };
2569
2681
  var run = async (argv, cwd) => {
2570
2682
  let parsed;
2571
2683
  try {
@@ -2601,7 +2713,7 @@ var run = async (argv, cwd) => {
2601
2713
  }
2602
2714
  if (positionals.length === 0 && !listing && !validating)
2603
2715
  throw new FlagError("files", "no files given");
2604
- const { registry, skipped, failures } = await setUp({
2716
+ const { root, registry, skipped, failures } = await setUp({
2605
2717
  cwd,
2606
2718
  configPath: values.config,
2607
2719
  noPlugins: values["no-plugins"],
@@ -2625,11 +2737,16 @@ var run = async (argv, cwd) => {
2625
2737
  console.log(listRules(registry));
2626
2738
  return 0;
2627
2739
  }
2628
- const { files, unreadable } = collectFiles(positionals.map((p) => p.startsWith("/") ? p : `${cwd}/${p}`));
2740
+ const { files, unreadable, symlinks } = collectFiles(positionals.map((p) => resolve5(cwd, p)));
2629
2741
  if (unreadable.length > 0)
2630
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
+ }
2631
2746
  if (files.length === 0)
2632
2747
  throw new FlagError("files", "no matching files");
2748
+ if (values.fix)
2749
+ await refuseOutsideLinks(files, root, cwd);
2633
2750
  const flags = { preset: values.preset, profile: values.profile };
2634
2751
  const perFile = await Promise.all(files.map(async (file) => {
2635
2752
  let src = await readFile3(file, "utf8");
@@ -2640,7 +2757,9 @@ var run = async (argv, cwd) => {
2640
2757
  src = fixed;
2641
2758
  }
2642
2759
  }
2643
- return checkModel(buildDocModel(src, documentKind(file)), display(file, cwd), flags, registry);
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 }));
2644
2763
  }));
2645
2764
  const all = perFile.flat();
2646
2765
  if (values.json) {
@@ -2701,9 +2820,12 @@ export {
2701
2820
  PluginFixerError,
2702
2821
  PluginContractError,
2703
2822
  PluginConflictError,
2823
+ PackageJsonError,
2704
2824
  PRESETS,
2705
2825
  MermaidLintError,
2706
2826
  FlagError,
2827
+ FixOutsideRootError,
2828
+ FixNotConvergingError,
2707
2829
  FIX_ORDER,
2708
2830
  DIAGRAM_EXTS,
2709
2831
  ConfigShapeError,