@jpeakai/mermaidjs-lint 0.1.0 → 0.2.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 (40) hide show
  1. package/README.md +82 -53
  2. package/dist/bin.js +202 -44
  3. package/dist/index.js +206 -45
  4. package/dist/testing.js +86 -23
  5. package/examples/architecture_beta_iconify_logos.mmd +27 -0
  6. package/examples/flowchart_fontawesome_icons.mmd +31 -0
  7. package/examples/layout_cosebilkent_mindmap.mmd +19 -0
  8. package/examples/layout_dagre_classic.mmd +22 -0
  9. package/examples/layout_dagre_handdrawn.mmd +23 -0
  10. package/examples/layout_dagre_neo.mmd +26 -0
  11. package/examples/layout_elk_default.mmd +22 -0
  12. package/examples/layout_elk_tuned.mmd +25 -0
  13. package/examples/layout_tidytree_mindmap.mmd +19 -0
  14. package/package.json +2 -2
  15. package/src/check.ts +2 -3
  16. package/src/cli.ts +45 -23
  17. package/src/config.ts +3 -2
  18. package/src/errors.ts +18 -3
  19. package/src/files.ts +3 -1
  20. package/src/fix/engine.ts +4 -4
  21. package/src/fix/verify.ts +2 -2
  22. package/src/index.ts +1 -1
  23. package/src/model.ts +10 -0
  24. package/src/parse/color.ts +2 -2
  25. package/src/parse/contrast.ts +59 -5
  26. package/src/parse/metrics.ts +16 -8
  27. package/src/parse/stats.ts +45 -19
  28. package/src/plugins/contract.ts +1 -1
  29. package/src/plugins/discover.ts +5 -5
  30. package/src/plugins/load.ts +5 -3
  31. package/src/project.ts +11 -4
  32. package/src/rules/complexity-node-count-hard-limit.ts +1 -1
  33. package/src/rules/complexity-visual-score-critical.ts +2 -2
  34. package/src/rules/contrast-text-below-aa.ts +6 -2
  35. package/src/rules/index.ts +2 -1
  36. package/src/rules/parse-diagram-unparseable.ts +29 -14
  37. package/src/rules/registry.ts +7 -4
  38. package/src/rules/shared.ts +1 -1
  39. package/src/rules/types.ts +2 -2
  40. package/src/testing.ts +4 -3
package/dist/testing.js CHANGED
@@ -183,17 +183,20 @@ function detectKeyword(content) {
183
183
  }
184
184
  async function extractStats(content) {
185
185
  const keyword = detectKeyword(content) ?? "unknown";
186
- if (keyword in LANGIUM_TYPES) {
186
+ const langium = keyword in LANGIUM_TYPES;
187
+ let langiumError = null;
188
+ if (langium) {
187
189
  try {
188
190
  return await extractLangiumStats(content, keyword);
189
- } catch {}
191
+ } catch (err) {
192
+ langiumError = parserReason(err);
193
+ }
190
194
  }
191
- let error = null;
195
+ let coreError;
192
196
  try {
193
197
  return await extractCoreStats(content, keyword);
194
198
  } catch (err) {
195
- error = (err instanceof Error ? err.message : String(err)).split(`
196
- `)[0] ?? null;
199
+ coreError = parserReason(err);
197
200
  }
198
201
  return {
199
202
  nodes: 0,
@@ -202,11 +205,20 @@ async function extractStats(content) {
202
205
  max_subgraph_depth: 0,
203
206
  node_ids: [],
204
207
  subgraph_names: [],
205
- parser_used: "mermaid-core",
208
+ parser_used: langium ? "langium" : "mermaid-core",
206
209
  diagram_type: keyword,
207
- error
210
+ error: langiumError ?? coreError
208
211
  };
209
212
  }
213
+ var POINTER = /^-*\^$/;
214
+ var parserReason = (err) => {
215
+ const message = err instanceof Error ? err.message : String(err);
216
+ const lines = message.split(`
217
+ `);
218
+ if (message.startsWith("No diagram type detected"))
219
+ return (lines[0] ?? "").trim();
220
+ return lines.filter((line, i) => !POINTER.test(line.trim()) && !POINTER.test((lines[i + 1] ?? "").trim())).join(" ").replace(/\s+/g, " ").trim();
221
+ };
210
222
  async function langiumParseAny(type, content) {
211
223
  switch (type) {
212
224
  case "architecture":
@@ -692,6 +704,11 @@ var collectMermaid = (node, out) => {
692
704
  for (const child of node.children ?? [])
693
705
  collectMermaid(child, out);
694
706
  };
707
+ var DIAGRAM_EXTS = [".mmd", ".mermaid"];
708
+ var documentKind = (file) => {
709
+ const lower = file.toLowerCase();
710
+ return DIAGRAM_EXTS.some((ext) => lower.endsWith(ext)) ? "diagram" : "markdown";
711
+ };
695
712
  var buildDocModel = (src, kind = "markdown") => {
696
713
  const lineStarts = lineStartsOf(src);
697
714
  if (kind === "diagram") {
@@ -798,6 +815,14 @@ var MKDOCS_MATERIAL = {
798
815
  };
799
816
  var GITHUB = { light: { bg: "#ffffff" }, dark: { bg: "#0d1117" } };
800
817
  var THEMES = ["light", "dark"];
818
+ var MERMAID_EDGE_LABEL = {
819
+ light: { box: "rgba(232,232,232,0.8)", backing: "rgba(232,232,232,0.5)" },
820
+ dark: { box: "#585858", backing: "rgba(88,88,88,0.5)" }
821
+ };
822
+ var edgeLabelBackground = (theme) => {
823
+ const { box, backing } = MERMAID_EDGE_LABEL[theme];
824
+ return compositeOver(box, compositeOver(box, compositeOver(backing, GITHUB[theme].bg)));
825
+ };
801
826
  var MERMAID_ER = {
802
827
  light: { odd: "#ffffff", even: "#f1f1ff", label: "#333333" },
803
828
  dark: { odd: "#2c2d2d", even: "#060606", label: "#cccccc" }
@@ -818,10 +843,27 @@ var scorePair = (kind, d, fg, bg) => {
818
843
  };
819
844
  };
820
845
  var message = (err) => err instanceof Error ? err.message : String(err);
846
+ var scoreEdgeLabel = (d, pairs, skipped) => {
847
+ const { color } = d.properties;
848
+ if (!color)
849
+ return;
850
+ try {
851
+ pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t))));
852
+ } catch (err) {
853
+ skipped.push({
854
+ selector: d.selector,
855
+ line: d.line,
856
+ reason: `edge label text unparseable: ${message(err)}`,
857
+ blocking: true
858
+ });
859
+ }
860
+ };
821
861
  var scoreGithub = (directives) => {
822
862
  const pairs = [];
823
863
  const skipped = [];
824
864
  for (const d of directives) {
865
+ if (d.kind === "linkStyle")
866
+ scoreEdgeLabel(d, pairs, skipped);
825
867
  if (d.kind !== "classDef" && d.kind !== "style")
826
868
  continue;
827
869
  const { fill, color, stroke } = d.properties;
@@ -886,6 +928,13 @@ var scoreMkdocs = (directives) => {
886
928
  const pairs = [];
887
929
  const skipped = [];
888
930
  for (const d of directives) {
931
+ if (d.kind === "linkStyle" && d.properties.color) {
932
+ skipped.push({
933
+ selector: d.selector,
934
+ line: d.line,
935
+ reason: "color: ignored, because the host theme forces edge label text"
936
+ });
937
+ }
889
938
  if (d.kind !== "classDef" && d.kind !== "style")
890
939
  continue;
891
940
  const { fill, stroke, color } = d.properties;
@@ -1108,15 +1157,17 @@ var calculateComplexity = (stats, config) => {
1108
1157
  };
1109
1158
  };
1110
1159
  var parseFailed = (content, stats) => {
1111
- const lines = content.split(`
1112
- `).filter((l) => {
1113
- const t = l.trim();
1114
- return t && !t.startsWith("%%") && t !== "---";
1115
- });
1116
- if (lines.length <= 1)
1160
+ if (stats.error !== null)
1161
+ return true;
1162
+ if (stats.parser_used === "langium" || stats.nodes > 0)
1117
1163
  return false;
1118
- return stats.nodes === 0;
1164
+ return sourceLines(content) > 1;
1119
1165
  };
1166
+ var sourceLines = (content) => content.split(`
1167
+ `).filter((l) => {
1168
+ const t = l.trim();
1169
+ return t && !t.startsWith("%%") && t !== "---";
1170
+ }).length;
1120
1171
 
1121
1172
  // src/rules/shared.ts
1122
1173
  var COMPLEXITY_OPTIONS = {
@@ -1241,7 +1292,7 @@ var complexityNodeCountCognitiveLimit = complexityRule({
1241
1292
  // src/rules/complexity-node-count-hard-limit.ts
1242
1293
  var complexityNodeCountHardLimit = complexityRule({
1243
1294
  id: RULE.NODES_HARD,
1244
- summary: "more nodes than any diagram can hold, above the hard limit",
1295
+ summary: "more nodes than the preset's hard limit",
1245
1296
  hit: ({ stats }, c) => stats.nodes > c.node_hard_limit ? {
1246
1297
  message: `${stats.nodes} nodes > ${c.node_hard_limit} hard limit`,
1247
1298
  remediation: "Split immediately. This diagram is beyond any comprehensible size. " + `${boundaryHint(stats.subgraph_names).replace(/^along /, "Split along ")}. ${splitTarget(c)}`
@@ -1272,9 +1323,9 @@ var complexityVisualScoreAcceptableExceeded = complexityRule({
1272
1323
  // src/rules/complexity-visual-score-critical.ts
1273
1324
  var complexityVisualScoreCritical = complexityRule({
1274
1325
  id: RULE.VCS_CRITICAL,
1275
- summary: "a visual complexity score above the critical threshold",
1326
+ summary: "a visual complexity score above the preset's complex threshold",
1276
1327
  hit: ({ stats, metrics }, c) => metrics.visual_complexity_score > c.vcs_complex ? {
1277
- message: `VCS ${metrics.visual_complexity_score.toFixed(1)} > ${c.vcs_complex} critical threshold`,
1328
+ message: `VCS ${metrics.visual_complexity_score.toFixed(1)} > ${c.vcs_complex} complex threshold`,
1278
1329
  remediation: `Reduce diagram complexity. Split ${boundaryHint(stats.subgraph_names)}, remove redundant edges, ` + `or collapse leaf nodes into labelled groups. ${splitTarget(c)}`
1279
1330
  } : null
1280
1331
  });
@@ -1360,7 +1411,7 @@ var contrastTextBelowAa = {
1360
1411
  endLine: line,
1361
1412
  rule: RULE.TEXT,
1362
1413
  message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1363
- 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."
1414
+ 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."
1364
1415
  });
1365
1416
  }
1366
1417
  }
@@ -1397,18 +1448,30 @@ var contrastTextBelowAa = {
1397
1448
  };
1398
1449
 
1399
1450
  // src/rules/parse-diagram-unparseable.ts
1451
+ var EXTERNAL_TYPES = new Set(["zenuml"]);
1452
+ var messageOf = (stats) => {
1453
+ const type = stats.diagram_type;
1454
+ if (EXTERNAL_TYPES.has(type)) {
1455
+ return `${type} is drawn by an external mermaid add-on that mermaidjs-lint does not include, so it cannot be parsed or checked`;
1456
+ }
1457
+ if (stats.error !== null) {
1458
+ const lines = /\bline \d/.test(stats.error) ? " (its line numbers count within the diagram, not the file)" : "";
1459
+ return `${type} did not parse (parser: ${stats.parser_used})${lines}: ${stats.error}`;
1460
+ }
1461
+ return `${type} parsed to 0 nodes from multi-line source with no error (parser: ${stats.parser_used}), a silent parse failure`;
1462
+ };
1400
1463
  var parseDiagramUnparseable = {
1401
1464
  id: RULE.UNPARSEABLE,
1402
1465
  category: "parse",
1403
- summary: "a multi-line diagram that mermaid's parser reads as zero nodes",
1466
+ summary: "a diagram mermaid's parser rejects, or reads as zero nodes from several lines",
1404
1467
  check: async (ctx) => {
1405
1468
  const found = [];
1406
1469
  for (const diagram of ctx.doc.diagrams) {
1407
1470
  const stats = await diagram.stats();
1408
1471
  if (!parseFailed(diagram.source, stats))
1409
1472
  continue;
1410
- const why = stats.error ? `: ${stats.error}` : "";
1411
- 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."));
1473
+ 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.";
1474
+ found.push(finding(ctx, diagram, RULE.UNPARSEABLE, messageOf(stats), remediation));
1412
1475
  }
1413
1476
  return found;
1414
1477
  }
@@ -1550,7 +1613,7 @@ var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
1550
1613
  const findings = (await Promise.all(perRule)).flat();
1551
1614
  return findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
1552
1615
  };
1553
- var checkMarkdown = (src, file, flags = {}, registry = BUILTIN) => checkModel(buildDocModel(src, file.toLowerCase().endsWith(".mmd") ? "diagram" : "markdown"), file, flags, registry);
1616
+ var checkMarkdown = (src, file, flags = {}, registry = BUILTIN) => checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
1554
1617
 
1555
1618
  // src/fix/verify.ts
1556
1619
  var OMIT = new Set(["position", "data"]);
@@ -1619,7 +1682,7 @@ var overlaps = (edits) => {
1619
1682
  };
1620
1683
  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";
1621
1684
  var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
1622
- const kind = file.toLowerCase().endsWith(".mmd") ? "diagram" : "markdown";
1685
+ const kind = documentKind(file);
1623
1686
  const applied = [];
1624
1687
  const refused = [];
1625
1688
  const refusedKeys = new Set;
@@ -0,0 +1,27 @@
1
+ architecture-beta
2
+ group ingress(logos:aws)[Ingress]
3
+ service dns(logos:aws-route53)[Route 53] in ingress
4
+ service cdn(logos:aws-cloudfront)[CloudFront] in ingress
5
+
6
+ group web(logos:aws-elb)[Web Tier]
7
+ service alb(logos:aws-elb)[Load Balancer] in web
8
+ service server(logos:aws-ec2)[Web Servers] in web
9
+
10
+ group app(logos:aws-ecs)[App Tier]
11
+ service worker(logos:aws-lambda)[Worker] in app
12
+ service api(logos:aws-ecs)[App Service] in app
13
+
14
+ group data(logos:aws-rds)[Data Tier]
15
+ service cache(logos:aws-elasticache)[Redis Cache] in data
16
+ service db(logos:aws-rds)[Primary DB] in data
17
+ service replica(logos:aws-rds)[Read Replica] in data
18
+
19
+ dns:R -[request]-> L:cdn
20
+ cdn:R -[route]-> L:alb
21
+ alb:R -[forward]-> L:server
22
+ server:R -[dispatch]-> L:worker
23
+ worker:R -[process]-> L:api
24
+ api:R -[read]-> L:cache
25
+ cache:R -[query]-> L:db
26
+ db:R -[replicate]-> L:replica
27
+ %% expect: none
@@ -0,0 +1,31 @@
1
+ flowchart LR
2
+ subgraph Ingress
3
+ dns["fa:fa-globe Route 53"]
4
+ cdn["fa:fa-cloud CloudFront"]
5
+ end
6
+ subgraph web["Web Tier"]
7
+ alb["fa:fa-random Load Balancer"]
8
+ web1["fa:fa-desktop Web 1"]
9
+ web2["fa:fa-desktop Web 2"]
10
+ end
11
+ subgraph app["App Tier"]
12
+ api["fa:fa-cog App Service"]
13
+ worker["fa:fa-bolt Worker"]
14
+ end
15
+ subgraph data["Data Tier"]
16
+ cache["fa:fa-bolt Redis Cache"]
17
+ db[("fa:fa-database Primary DB")]
18
+ replica[("fa:fa-copy Read Replica")]
19
+ end
20
+
21
+ dns --> cdn
22
+ cdn --> alb
23
+ alb --> web1
24
+ alb --> web2
25
+ web1 --> api
26
+ web2 --> api
27
+ api --> cache
28
+ api --> db
29
+ worker --> db
30
+ db -- replication --> replica
31
+ %% expect: none
@@ -0,0 +1,19 @@
1
+ ---
2
+ config:
3
+ layout: cose-bilkent
4
+ look: classic
5
+ theme: default
6
+ ---
7
+ mindmap
8
+ root((Platform))
9
+ Ingress
10
+ CDN
11
+ Load Balancer
12
+ Compute
13
+ API
14
+ Worker
15
+ Data
16
+ Cache
17
+ Primary DB
18
+ Object Store
19
+ %% expect: none
@@ -0,0 +1,22 @@
1
+ ---
2
+ config:
3
+ layout: dagre
4
+ look: classic
5
+ theme: default
6
+ ---
7
+ flowchart LR
8
+ user([User]) --> edge[CDN / Edge]
9
+ edge --> lb[Load Balancer]
10
+ lb --> api1[API Server 1]
11
+ lb --> api2[API Server 2]
12
+ api1 --> cache[(Cache)]
13
+ api2 --> cache
14
+ api1 --> db[(Primary DB)]
15
+ api2 --> db
16
+ api1 --> queue[[Job Queue]]
17
+ api2 --> queue
18
+ queue --> worker[Worker]
19
+ worker --> db
20
+ worker --> store[(Object Store)]
21
+ db -. replicate .-> replica[(Read Replica)]
22
+ %% expect: none
@@ -0,0 +1,23 @@
1
+ ---
2
+ config:
3
+ layout: dagre
4
+ look: handDrawn
5
+ handDrawnSeed: 1
6
+ theme: default
7
+ ---
8
+ flowchart LR
9
+ user([User]) --> edge[CDN / Edge]
10
+ edge --> lb[Load Balancer]
11
+ lb --> api1[API Server 1]
12
+ lb --> api2[API Server 2]
13
+ api1 --> cache[(Cache)]
14
+ api2 --> cache
15
+ api1 --> db[(Primary DB)]
16
+ api2 --> db
17
+ api1 --> queue[[Job Queue]]
18
+ api2 --> queue
19
+ queue --> worker[Worker]
20
+ worker --> db
21
+ worker --> store[(Object Store)]
22
+ db -. replicate .-> replica[(Read Replica)]
23
+ %% expect: none
@@ -0,0 +1,26 @@
1
+ ---
2
+ config:
3
+ layout: dagre
4
+ look: neo
5
+ theme: default
6
+ useGradient: true
7
+ gradientStart: "#ede9fe"
8
+ gradientStop: "#a78bfa"
9
+ dropShadow: true
10
+ ---
11
+ flowchart LR
12
+ user([User]) --> edge[CDN / Edge]
13
+ edge --> lb[Load Balancer]
14
+ lb --> api1[API Server 1]
15
+ lb --> api2[API Server 2]
16
+ api1 --> cache[(Cache)]
17
+ api2 --> cache
18
+ api1 --> db[(Primary DB)]
19
+ api2 --> db
20
+ api1 --> queue[[Job Queue]]
21
+ api2 --> queue
22
+ queue --> worker[Worker]
23
+ worker --> db
24
+ worker --> store[(Object Store)]
25
+ db -. replicate .-> replica[(Read Replica)]
26
+ %% expect: none
@@ -0,0 +1,22 @@
1
+ ---
2
+ config:
3
+ layout: elk
4
+ look: classic
5
+ theme: default
6
+ ---
7
+ flowchart LR
8
+ user([User]) --> edge[CDN / Edge]
9
+ edge --> lb[Load Balancer]
10
+ lb --> api1[API Server 1]
11
+ lb --> api2[API Server 2]
12
+ api1 --> cache[(Cache)]
13
+ api2 --> cache
14
+ api1 --> db[(Primary DB)]
15
+ api2 --> db
16
+ api1 --> queue[[Job Queue]]
17
+ api2 --> queue
18
+ queue --> worker[Worker]
19
+ worker --> db
20
+ worker --> store[(Object Store)]
21
+ db -. replicate .-> replica[(Read Replica)]
22
+ %% expect: none
@@ -0,0 +1,25 @@
1
+ ---
2
+ config:
3
+ layout: elk
4
+ look: classic
5
+ theme: default
6
+ elk:
7
+ mergeEdges: true
8
+ nodePlacementStrategy: LINEAR_SEGMENTS
9
+ ---
10
+ flowchart LR
11
+ user([User]) --> edge[CDN / Edge]
12
+ edge --> lb[Load Balancer]
13
+ lb --> api1[API Server 1]
14
+ lb --> api2[API Server 2]
15
+ api1 --> cache[(Cache)]
16
+ api2 --> cache
17
+ api1 --> db[(Primary DB)]
18
+ api2 --> db
19
+ api1 --> queue[[Job Queue]]
20
+ api2 --> queue
21
+ queue --> worker[Worker]
22
+ worker --> db
23
+ worker --> store[(Object Store)]
24
+ db -. replicate .-> replica[(Read Replica)]
25
+ %% expect: none
@@ -0,0 +1,19 @@
1
+ ---
2
+ config:
3
+ layout: tidy-tree
4
+ look: classic
5
+ theme: default
6
+ ---
7
+ mindmap
8
+ root((System))
9
+ Ingress
10
+ CDN
11
+ Load Balancer
12
+ Compute
13
+ API
14
+ Worker
15
+ Data
16
+ Cache
17
+ Primary DB
18
+ Object Store
19
+ %% expect: none
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jpeakai/mermaidjs-lint",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
6
6
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  "type": "git",
17
17
  "url": "git+https://github.com/jpeakai/jpai-mermaidjs-lint.git"
18
18
  },
19
- "homepage": "https://github.com/jpeakai/jpai-mermaidjs-lint#readme",
19
+ "homepage": "https://jpeakai.github.io/jpai-mermaidjs-lint/",
20
20
  "bugs": "https://github.com/jpeakai/jpai-mermaidjs-lint/issues",
21
21
  "packageManager": "bun@1.3.12",
22
22
  "publishConfig": {
package/src/check.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // them. A rule may be synchronous or return a promise, so a rule that waits on
5
5
  // a tool or a service overlaps with the others.
6
6
 
7
- import { buildDocModel, type DocModel } from "./model.ts";
7
+ import { buildDocModel, type DocModel, documentKind } from "./model.ts";
8
8
  import { BUILTIN, HELPERS, type Registry } from "./rules/registry.ts";
9
9
  import type { Finding, Rule, RuleOptions } from "./rules/types.ts";
10
10
 
@@ -44,5 +44,4 @@ export const checkMarkdown = (
44
44
  file: string,
45
45
  flags: RunFlags = {},
46
46
  registry: Registry = BUILTIN,
47
- ): Promise<Finding[]> =>
48
- checkModel(buildDocModel(src, file.toLowerCase().endsWith(".mmd") ? "diagram" : "markdown"), file, flags, registry);
47
+ ): Promise<Finding[]> => checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
package/src/cli.ts CHANGED
@@ -4,23 +4,29 @@
4
4
  import { readFile, writeFile } from "node:fs/promises";
5
5
  import { relative } from "node:path";
6
6
  import { parseArgs } from "node:util";
7
+ import pkg from "../package.json" with { type: "json" };
7
8
  import { checkModel, type RunFlags } from "./check.ts";
8
9
  import { FlagError, MermaidLintError, UsageError } from "./errors.ts";
9
- import { collectFiles } from "./files.ts";
10
+ import { collectFiles, DIAGRAM_FILE_EXTS, SKIP_DIRS } from "./files.ts";
10
11
  import { fixSourceReport } from "./fix/engine.ts";
11
- import { buildDocModel } from "./model.ts";
12
+ import { buildDocModel, DIAGRAM_EXTS, documentKind } from "./model.ts";
12
13
  import { PROFILES } from "./parse/contrast.ts";
13
14
  import { canonicalPreset, PRESET_NAMES } from "./parse/metrics.ts";
14
15
  import type { PluginFailure } from "./plugins/index.ts";
15
16
  import { setUp } from "./project.ts";
16
17
  import { RULES } from "./rules/index.ts";
17
18
  import type { LoadedSource, Registry } from "./rules/registry.ts";
18
- import { CATEGORIES } from "./rules/types.ts";
19
+ import { CATEGORIES, type Finding } from "./rules/types.ts";
19
20
 
20
- const USAGE = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
21
+ // The invocation forms, printed alone after a usage error so the error is not
22
+ // buried, and first in --help.
23
+ const SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
21
24
  [--no-plugins | --lenient-plugins]
22
25
  mermaidjs-lint --list-rules
23
26
  mermaidjs-lint --validate-plugins [--json] [--config FILE]
27
+ mermaidjs-lint --version | -h, --help`;
28
+
29
+ const USAGE = `${SYNOPSIS}
24
30
 
25
31
  Deterministic Mermaid diagram gates (* = autofixable):
26
32
  ${CATEGORIES.map((c) =>
@@ -29,23 +35,29 @@ ${CATEGORIES.map((c) =>
29
35
  ),
30
36
  ).join("\n")}
31
37
 
32
- A path is a .mmd file, a markdown file whose mermaid fences are checked, or a
33
- directory, which is walked for both.
34
- Examples of every rule, with fixes: RULES.md
38
+ A path is a file or a directory, and every path must exist. A file ending in
39
+ ${DIAGRAM_EXTS.join(" or ")} is read as one raw diagram, and any other file is read as markdown
40
+ whose mermaid fences are checked. A directory is walked for ${[...DIAGRAM_FILE_EXTS].join(", ")} files,
41
+ skipping dot-folders and ${[...SKIP_DIRS].join(", ")}.
42
+ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/
35
43
 
36
- --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
37
- (aliases low, medium, high). Rule options and the config can set it per rule.
38
- --profile NAME sets how contrast is read: ${PROFILES.join(", ")}.
39
- auto reads mkdocs-material when an ancestor holds mkdocs.yml, else github.
44
+ -p, --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
45
+ (aliases low, medium, high). It overrides a preset set in the config, and a
46
+ numeric threshold set in the config overrides both.
47
+ --profile NAME sets how contrast is read: ${PROFILES.join(", ")}. It overrides a profile set in the config.
48
+ auto reads mkdocs-material when any parent folder holds mkdocs.yml or mkdocs.yaml, else github.
40
49
  --fix applies every fix it can prove safe, then reports what remains.
41
50
  A fixer that is unsure leaves the text alone and the finding stays.
42
51
  --config FILE reads that config instead of mermaidjs-lint.config.json or .mjs at the project root.
43
52
  Plugins load on their own from .mermaidjs-lint/rules and from dependencies named mermaidjs-lint-plugin-*.
44
- --no-plugins runs the built-in rules only, for a baseline or a repo you do not trust.
53
+ --no-plugins runs the built-in rules only and loads no plugin. It reads a .json config as usual,
54
+ and stops with a usage error rather than import a .mjs config, which is code.
45
55
  --lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
46
56
  --validate-plugins loads and checks every plugin, reports every failure, and runs no check or fix.
47
57
  --list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
48
- Exits 1 when findings remain or a plugin fails to load, 2 on usage error.`;
58
+ --version prints the version, to quote in a bug report.
59
+ Exits 0 when no findings remain, 1 when findings remain or a plugin fails to load (a lenient run
60
+ skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
49
61
 
50
62
  interface Parsed {
51
63
  values: {
@@ -59,6 +71,7 @@ interface Parsed {
59
71
  "lenient-plugins"?: boolean;
60
72
  "validate-plugins"?: boolean;
61
73
  "list-rules"?: boolean;
74
+ version?: boolean;
62
75
  };
63
76
  positionals: string[];
64
77
  }
@@ -77,6 +90,7 @@ const parse = (argv: string[]): Parsed =>
77
90
  "lenient-plugins": { type: "boolean", default: false },
78
91
  "validate-plugins": { type: "boolean", default: false },
79
92
  "list-rules": { type: "boolean", default: false },
93
+ version: { type: "boolean", default: false },
80
94
  },
81
95
  allowPositionals: true,
82
96
  strict: true,
@@ -154,6 +168,13 @@ const validate = (
154
168
  return { code, stdout: line, stderr: [] };
155
169
  };
156
170
 
171
+ // One finding as the text report prints it: file, line or line range, rule
172
+ // and message. The site generator prints its examples with this too.
173
+ export const reportLine = (f: Finding, file: string): string => {
174
+ const where = f.endLine > f.line ? `${f.line}-${f.endLine}` : `${f.line}`;
175
+ return `${file}:${where} ${f.rule} ${f.message}`;
176
+ };
177
+
157
178
  const display = (file: string, cwd: string): string => {
158
179
  const rel = relative(cwd, file);
159
180
  return !rel || rel.startsWith("..") ? file : rel;
@@ -171,6 +192,10 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
171
192
  console.log(USAGE);
172
193
  return 0;
173
194
  }
195
+ if (values.version) {
196
+ console.log(pkg.version);
197
+ return 0;
198
+ }
174
199
  const listing = values["list-rules"];
175
200
  const validating = values["validate-plugins"];
176
201
  if (values.preset !== undefined && canonicalPreset(values.preset) === null) {
@@ -228,9 +253,7 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
228
253
  src = fixed;
229
254
  }
230
255
  }
231
- const kind = file.toLowerCase().endsWith(".mmd") ? "diagram" : "markdown";
232
- const found = await checkModel(buildDocModel(src, kind), display(file, cwd), flags, registry);
233
- return found;
256
+ return checkModel(buildDocModel(src, documentKind(file)), display(file, cwd), flags, registry);
234
257
  }),
235
258
  );
236
259
  const all = perFile.flat();
@@ -246,10 +269,7 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
246
269
  JSON.stringify({ findings: all, files: files.length, plugins: registry.sources, skippedPlugins }, null, 2),
247
270
  );
248
271
  } else {
249
- for (const f of all) {
250
- const where = f.endLine > f.line ? `${f.line}-${f.endLine}` : `${f.line}`;
251
- console.log(`${f.file}:${where} ${f.rule} ${f.message}`);
252
- }
272
+ for (const f of all) console.log(reportLine(f, f.file));
253
273
  console.log(`${all.length} finding(s) in ${files.length} file(s)`);
254
274
  }
255
275
  return all.length > 0 ? 1 : 0;
@@ -261,15 +281,17 @@ export const describeError = (err: unknown): string =>
261
281
  ? `error[${err.code}]: ${err.message}`
262
282
  : `error: ${err instanceof Error ? err.message : String(err)}`;
263
283
 
264
- // A usage error prints its reason and the usage, and exits 2. Anything else is
265
- // a runtime failure and propagates to bin.ts, which exits 1.
284
+ // A usage error prints its reason, the invocation forms and a pointer to
285
+ // --help, and exits 2. Anything else is a runtime failure and propagates to
286
+ // bin.ts, which exits 1.
266
287
  export const main = async (argv: string[] = process.argv.slice(2), cwd: string = process.cwd()): Promise<number> => {
267
288
  try {
268
289
  return await run(argv, cwd);
269
290
  } catch (err) {
270
291
  if (!(err instanceof UsageError)) throw err;
271
292
  console.error(describeError(err));
272
- console.error(USAGE);
293
+ console.error(SYNOPSIS);
294
+ console.error("run mermaidjs-lint --help for more");
273
295
  return 2;
274
296
  }
275
297
  };
package/src/config.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  // The config file: which rules are off, what options each takes, and how
2
2
  // strictly plugins load. It is optional, because everything auto-detected loads
3
3
  // without it. A mistake in it is a named usage error, so a stale or misspelt
4
- // rule id fails the run instead of silently doing nothing.
4
+ // built-in rule id fails the run instead of silently doing nothing. A lenient
5
+ // or collect (validating) run drops a setting for an unloaded plugin rule.
5
6
 
6
7
  import { access, readFile } from "node:fs/promises";
7
8
  import { extname, join } from "node:path";
@@ -76,7 +77,7 @@ export const parseConfig = (file: string, raw: unknown): Config => {
76
77
  };
77
78
  };
78
79
 
79
- // The first config file at the root, or null. Two at once is ambiguous, so it
80
+ // The config file at the root, or null. Two at once is ambiguous, so it
80
81
  // is an error rather than a pick.
81
82
  export const findConfig = async (root: string): Promise<string | null> => {
82
83
  const found: string[] = [];