@jpeakai/mermaidjs-lint 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -9,7 +9,7 @@ import { parseArgs } from "node:util";
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "@jpeakai/mermaidjs-lint",
12
- version: "0.7.1",
12
+ version: "0.7.3",
13
13
  type: "module",
14
14
  description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
15
15
  license: "MIT",
@@ -329,7 +329,7 @@ var parseProperties = (raw, rawStart, line) => {
329
329
  const key = pair.slice(0, colon).trim().toLowerCase();
330
330
  const valueRaw = pair.slice(colon + 1);
331
331
  const value = valueRaw.trim();
332
- if (!key || !value)
332
+ if (!(key && value))
333
333
  continue;
334
334
  properties[key] = value;
335
335
  const lead = valueRaw.length - valueRaw.trimStart().length;
@@ -484,7 +484,7 @@ var parserReason = (err) => {
484
484
  `);
485
485
  if (message.startsWith("No diagram type detected"))
486
486
  return (lines[0] ?? "").trim();
487
- return lines.filter((line, i) => !POINTER.test(line.trim()) && !POINTER.test((lines[i + 1] ?? "").trim())).join(" ").replace(/\s+/g, " ").trim();
487
+ return lines.filter((line, i) => !(POINTER.test(line.trim()) || POINTER.test((lines[i + 1] ?? "").trim()))).join(" ").replace(/\s+/g, " ").trim();
488
488
  };
489
489
  async function langiumParseAny(type, content) {
490
490
  switch (type) {
@@ -515,80 +515,7 @@ async function extractLangiumStats(content, keyword) {
515
515
  if (!mapped)
516
516
  throw new Error(`not a langium keyword: ${keyword}`);
517
517
  const ast = await langiumParseAny(mapped, content);
518
- const nodeIds = [];
519
- const subgraphNames = [];
520
- let edges = 0;
521
- let maxDepth = 0;
522
- let treeDepth = 0;
523
- switch (mapped) {
524
- case "architecture": {
525
- const services = ast.services ?? [];
526
- const groups = ast.groups ?? [];
527
- const edgeArr = ast.edges ?? [];
528
- nodeIds.push(...services.map((s) => s.id ?? ""));
529
- subgraphNames.push(...groups.map((g) => g.title ?? g.id ?? ""));
530
- maxDepth = architectureDepth(groups);
531
- edges = edgeArr.length;
532
- break;
533
- }
534
- case "pie": {
535
- const sections = ast.sections ?? [];
536
- nodeIds.push(...sections.map((s, i) => s.label ?? `slice_${i}`));
537
- break;
538
- }
539
- case "packet": {
540
- const blocks = ast.blocks ?? [];
541
- nodeIds.push(...blocks.map((_, i) => `block_${i}`));
542
- break;
543
- }
544
- case "gitGraph": {
545
- const statements = ast.statements ?? [];
546
- for (const s of statements) {
547
- if (s.$type === "Commit")
548
- nodeIds.push(s.id ?? `commit_${nodeIds.length}`);
549
- else if (s.$type === "Branch")
550
- subgraphNames.push(s.name ?? `branch_${subgraphNames.length}`);
551
- else if (s.$type === "Merge")
552
- edges++;
553
- }
554
- break;
555
- }
556
- case "radar": {
557
- const axes = ast.axes ?? [];
558
- const curves = ast.curves ?? [];
559
- nodeIds.push(...axes.map((a, i) => a.name ?? `axis_${i}`));
560
- nodeIds.push(...curves.map((c, i) => c.name ?? `curve_${i}`));
561
- edges = axes.length * curves.length;
562
- break;
563
- }
564
- case "treeView": {
565
- const nodes = ast.nodes ?? [];
566
- const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
567
- nodeIds.push(...stats.labels);
568
- edges = stats.edges;
569
- treeDepth = stats.depth;
570
- break;
571
- }
572
- case "treemap": {
573
- const rows = ast.TreemapRows ?? [];
574
- const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
575
- nodeIds.push(...stats.labels);
576
- edges = stats.edges;
577
- treeDepth = stats.depth;
578
- break;
579
- }
580
- case "wardley": {
581
- const components = ast.components ?? [];
582
- const anchors = ast.anchors ?? [];
583
- const links = ast.links ?? [];
584
- nodeIds.push(...anchors.map((a, i) => a.name ?? `anchor_${i}`));
585
- nodeIds.push(...components.map((c, i) => c.name ?? `comp_${i}`));
586
- edges = links.length;
587
- break;
588
- }
589
- case "info":
590
- break;
591
- }
518
+ const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = LANGIUM_ADAPTERS[mapped](ast);
592
519
  return {
593
520
  nodes: new Set(nodeIds.filter(Boolean)).size,
594
521
  edges,
@@ -602,6 +529,85 @@ async function extractLangiumStats(content, keyword) {
602
529
  error: null
603
530
  };
604
531
  }
532
+ var LANGIUM_ADAPTERS = {
533
+ architecture: adaptLangiumArchitecture,
534
+ info: adaptLangiumInfo,
535
+ pie: adaptLangiumPie,
536
+ gitGraph: adaptLangiumGitGraph,
537
+ packet: adaptLangiumPacket,
538
+ radar: adaptLangiumRadar,
539
+ treemap: adaptLangiumTreemap,
540
+ treeView: adaptLangiumTreeView,
541
+ wardley: adaptLangiumWardley
542
+ };
543
+ function adaptLangiumArchitecture(ast) {
544
+ const services = ast.services ?? [];
545
+ const groups = ast.groups ?? [];
546
+ const edgeArr = ast.edges ?? [];
547
+ return {
548
+ nodeIds: services.map((s) => s.id ?? ""),
549
+ edges: edgeArr.length,
550
+ subgraphNames: groups.map((g) => g.title ?? g.id ?? ""),
551
+ maxDepth: architectureDepth(groups)
552
+ };
553
+ }
554
+ function adaptLangiumInfo() {
555
+ return { nodeIds: [], edges: 0, subgraphNames: [], maxDepth: 0 };
556
+ }
557
+ function adaptLangiumPie(ast) {
558
+ const sections = ast.sections ?? [];
559
+ return { nodeIds: sections.map((s, i) => s.label ?? `slice_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
560
+ }
561
+ function adaptLangiumPacket(ast) {
562
+ const blocks = ast.blocks ?? [];
563
+ return { nodeIds: blocks.map((_, i) => `block_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
564
+ }
565
+ function adaptLangiumGitGraph(ast) {
566
+ const statements = ast.statements ?? [];
567
+ const nodeIds = [];
568
+ const subgraphNames = [];
569
+ let edges = 0;
570
+ for (const s of statements) {
571
+ if (s.$type === "Commit")
572
+ nodeIds.push(s.id ?? `commit_${nodeIds.length}`);
573
+ else if (s.$type === "Branch")
574
+ subgraphNames.push(s.name ?? `branch_${subgraphNames.length}`);
575
+ else if (s.$type === "Merge")
576
+ edges++;
577
+ }
578
+ return { nodeIds, edges, subgraphNames, maxDepth: 0 };
579
+ }
580
+ function adaptLangiumRadar(ast) {
581
+ const axes = ast.axes ?? [];
582
+ const curves = ast.curves ?? [];
583
+ return {
584
+ nodeIds: [...axes.map((a, i) => a.name ?? `axis_${i}`), ...curves.map((c, i) => c.name ?? `curve_${i}`)],
585
+ edges: axes.length * curves.length,
586
+ subgraphNames: [],
587
+ maxDepth: 0
588
+ };
589
+ }
590
+ function adaptLangiumTreeView(ast) {
591
+ const nodes = ast.nodes ?? [];
592
+ const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
593
+ return { nodeIds: stats.labels, edges: stats.edges, subgraphNames: [], maxDepth: 0, treeDepth: stats.depth };
594
+ }
595
+ function adaptLangiumTreemap(ast) {
596
+ const rows = ast.TreemapRows ?? [];
597
+ const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
598
+ return { nodeIds: stats.labels, edges: stats.edges, subgraphNames: [], maxDepth: 0, treeDepth: stats.depth };
599
+ }
600
+ function adaptLangiumWardley(ast) {
601
+ const components = ast.components ?? [];
602
+ const anchors = ast.anchors ?? [];
603
+ const links = ast.links ?? [];
604
+ return {
605
+ nodeIds: [...anchors.map((a, i) => a.name ?? `anchor_${i}`), ...components.map((c, i) => c.name ?? `comp_${i}`)],
606
+ edges: links.length,
607
+ subgraphNames: [],
608
+ maxDepth: 0
609
+ };
610
+ }
605
611
  function architectureDepth(groups) {
606
612
  const parentOf = new Map;
607
613
  for (const g of groups)
@@ -1025,7 +1031,7 @@ var buildDocModel = (src, kind = "markdown") => {
1025
1031
  collectMermaid(tree, fences);
1026
1032
  const diagrams = fences.flatMap((node, index) => {
1027
1033
  const { start, end } = node.position ?? {};
1028
- if (!start || !end || start.offset === undefined || end.offset === undefined)
1034
+ if (!(start && end) || start.offset === undefined || end.offset === undefined)
1029
1035
  return [];
1030
1036
  return [
1031
1037
  makeDiagram(src, lineStarts, {
@@ -1092,72 +1098,71 @@ var scorePair = (kind, d, fg, bg) => {
1092
1098
  };
1093
1099
  };
1094
1100
  var message = (err) => err instanceof Error ? err.message : String(err);
1095
- var scoreEdgeLabel = (d, canvas, pairs, skipped) => {
1101
+ var skip = (out, d, reason, blocking) => {
1102
+ out.skipped.push({ selector: d.selector, line: d.line, reason, ...blocking && { blocking } });
1103
+ };
1104
+ var scoreEdgeLabel = (d, canvas, out) => {
1096
1105
  const { color } = d.properties;
1097
1106
  if (!color)
1098
1107
  return;
1099
1108
  try {
1100
- pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t, canvas))));
1109
+ out.pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, { fg: color, bg: edgeLabelBackground(t, canvas) })));
1101
1110
  } catch (err) {
1102
- skipped.push({
1103
- selector: d.selector,
1104
- line: d.line,
1105
- reason: `edge label text unparseable: ${message(err)}`,
1106
- blocking: true
1107
- });
1111
+ skip(out, d, `edge label text unparseable: ${message(err)}`, true);
1112
+ }
1113
+ };
1114
+ var githubBoxes = (fill, alpha, canvas) => alpha < 1 ? THEMES.map((t) => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
1115
+ var scoreGithubPair = (out, d, kind, { fg, bg, theme }) => {
1116
+ try {
1117
+ out.pairs.push({ ...scorePair(kind, d, fg, bg), ...theme && { theme } });
1118
+ } catch (err) {
1119
+ skip(out, d, `${kind} pair unparseable: ${message(err)}`, true);
1120
+ }
1121
+ };
1122
+ var fillAlpha = (out, d, fill) => {
1123
+ try {
1124
+ return alphaOf(fill);
1125
+ } catch (err) {
1126
+ skip(out, d, `fill unparseable: ${message(err)}`, true);
1127
+ return null;
1128
+ }
1129
+ };
1130
+ var scoreGithubStyle = (d, canvas, out) => {
1131
+ const { fill, color, stroke } = d.properties;
1132
+ if (!fill) {
1133
+ const reason = color || stroke ? "no fill declared, so text and border contrast have no anchor" : "no color properties";
1134
+ skip(out, d, reason);
1135
+ return;
1136
+ }
1137
+ const alpha = fillAlpha(out, d, fill);
1138
+ if (alpha === null)
1139
+ return;
1140
+ if (alpha === 0) {
1141
+ skip(out, d, "fill is fully transparent, so there is no box to anchor contrast");
1142
+ return;
1143
+ }
1144
+ const boxes = githubBoxes(fill, alpha, canvas);
1145
+ if (!color) {
1146
+ skip(out, d, "no color: declared beside fill:, and the github profile requires one " + "(if the host forces label text, use the mkdocs-material profile)", true);
1147
+ }
1148
+ for (const [bg, theme] of boxes) {
1149
+ if (color)
1150
+ scoreGithubPair(out, d, "text", { fg: color, bg, theme });
1151
+ if (stroke)
1152
+ scoreGithubPair(out, d, "border", { fg: stroke, bg, theme });
1108
1153
  }
1109
1154
  };
1110
1155
  var scoreGithub = (directives, canvas = GITHUB_CANVAS) => {
1111
- const pairs = [];
1112
- const skipped = [];
1156
+ const out = { pairs: [], skipped: [] };
1113
1157
  for (const d of directives) {
1114
1158
  if (d.kind === "linkStyle")
1115
- scoreEdgeLabel(d, canvas, pairs, skipped);
1116
- if (d.kind !== "classDef" && d.kind !== "style")
1117
- continue;
1118
- const { fill, color, stroke } = d.properties;
1119
- const note = (reason, blocking) => {
1120
- skipped.push({ selector: d.selector, line: d.line, reason, ...blocking && { blocking } });
1121
- };
1122
- if (!fill) {
1123
- note(color || stroke ? "no fill declared, so text and border contrast have no anchor" : "no color properties");
1124
- continue;
1125
- }
1126
- let alpha;
1127
- try {
1128
- alpha = alphaOf(fill);
1129
- } catch (err) {
1130
- note(`fill unparseable: ${message(err)}`, true);
1131
- continue;
1132
- }
1133
- if (alpha === 0) {
1134
- note("fill is fully transparent, so there is no box to anchor contrast");
1135
- continue;
1136
- }
1137
- const boxes = alpha < 1 ? THEMES.map((t) => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
1138
- if (!color) {
1139
- note("no color: declared beside fill:, and the github profile requires one " + "(if the host forces label text, use the mkdocs-material profile)", true);
1140
- }
1141
- for (const [box, theme] of boxes) {
1142
- if (color) {
1143
- try {
1144
- pairs.push({ ...scorePair("text", d, color, box), ...theme && { theme } });
1145
- } catch (err) {
1146
- note(`text pair unparseable: ${message(err)}`, true);
1147
- }
1148
- }
1149
- if (stroke) {
1150
- try {
1151
- pairs.push({ ...scorePair("border", d, stroke, box), ...theme && { theme } });
1152
- } catch (err) {
1153
- note(`border pair unparseable: ${message(err)}`, true);
1154
- }
1155
- }
1156
- }
1159
+ scoreEdgeLabel(d, canvas, out);
1160
+ if (d.kind === "classDef" || d.kind === "style")
1161
+ scoreGithubStyle(d, canvas, out);
1157
1162
  }
1158
- return { pairs, skipped };
1163
+ return out;
1159
1164
  };
1160
- var scoreHostPair = (kind, d, theme, fgRaw, box) => {
1165
+ var scoreHostPair = (kind, d, theme, { fg: fgRaw, bg: box }) => {
1161
1166
  const fg = compositeOver(fgRaw, box);
1162
1167
  const assessment = wcagAssess(fg, box);
1163
1168
  return {
@@ -1173,99 +1178,83 @@ var scoreHostPair = (kind, d, theme, fgRaw, box) => {
1173
1178
  advisory: kind === "border"
1174
1179
  };
1175
1180
  };
1176
- var scoreMkdocs = (directives) => {
1177
- const pairs = [];
1178
- const skipped = [];
1179
- for (const d of directives) {
1180
- if (d.kind === "linkStyle" && d.properties.color) {
1181
- skipped.push({
1182
- selector: d.selector,
1183
- line: d.line,
1184
- reason: "color: ignored, because the host theme forces edge label text"
1185
- });
1186
- }
1187
- if (d.kind !== "classDef" && d.kind !== "style")
1188
- continue;
1189
- const { fill, stroke, color } = d.properties;
1190
- const note = (reason) => {
1191
- skipped.push({ selector: d.selector, line: d.line, reason });
1192
- };
1193
- if (!fill) {
1194
- note("no fill declared, and the host theme controls the text");
1195
- continue;
1181
+ var scoreMkdocsStyle = (d, out) => {
1182
+ const { fill, stroke, color } = d.properties;
1183
+ if (!fill) {
1184
+ skip(out, d, "no fill declared, and the host theme controls the text");
1185
+ return;
1186
+ }
1187
+ if (color)
1188
+ skip(out, d, "color: ignored, because the host theme forces label text");
1189
+ for (const theme of THEMES) {
1190
+ let box;
1191
+ try {
1192
+ box = compositeOver(fill, MKDOCS_MATERIAL[theme].bg);
1193
+ } catch (err) {
1194
+ skip(out, d, `fill unparseable: ${message(err)}`, true);
1195
+ break;
1196
1196
  }
1197
- if (color)
1198
- note("color: ignored, because the host theme forces label text");
1199
- for (const theme of THEMES) {
1200
- let box;
1197
+ out.pairs.push(scoreHostPair("text", d, theme, { fg: MKDOCS_MATERIAL[theme].text, bg: box }));
1198
+ if (stroke) {
1201
1199
  try {
1202
- box = compositeOver(fill, MKDOCS_MATERIAL[theme].bg);
1200
+ out.pairs.push(scoreHostPair("border", d, theme, { fg: stroke, bg: box }));
1203
1201
  } catch (err) {
1204
- skipped.push({
1205
- selector: d.selector,
1206
- line: d.line,
1207
- reason: `fill unparseable: ${message(err)}`,
1208
- blocking: true
1209
- });
1210
- break;
1211
- }
1212
- pairs.push(scoreHostPair("text", d, theme, MKDOCS_MATERIAL[theme].text, box));
1213
- if (stroke) {
1214
- try {
1215
- pairs.push(scoreHostPair("border", d, theme, stroke, box));
1216
- } catch (err) {
1217
- skipped.push({
1218
- selector: d.selector,
1219
- line: d.line,
1220
- reason: `border pair unparseable: ${message(err)}`,
1221
- blocking: true
1222
- });
1223
- }
1202
+ skip(out, d, `border pair unparseable: ${message(err)}`, true);
1224
1203
  }
1225
1204
  }
1226
1205
  }
1227
- return { pairs, skipped };
1206
+ };
1207
+ var scoreMkdocs = (directives) => {
1208
+ const out = { pairs: [], skipped: [] };
1209
+ for (const d of directives) {
1210
+ if (d.kind === "linkStyle" && d.properties.color) {
1211
+ skip(out, d, "color: ignored, because the host theme forces edge label text");
1212
+ }
1213
+ if (d.kind === "classDef" || d.kind === "style")
1214
+ scoreMkdocsStyle(d, out);
1215
+ }
1216
+ return out;
1217
+ };
1218
+ var erRows = (theme, fill) => {
1219
+ const anchor = MERMAID_ER[theme];
1220
+ return [
1221
+ ["odd", anchor.odd],
1222
+ ["even", fill ? compositeOver(fill, anchor.even) : anchor.even]
1223
+ ];
1224
+ };
1225
+ var erPairs = (d) => {
1226
+ const { fill, color, stroke } = d.properties;
1227
+ const scored = [];
1228
+ const score = (kind, theme, row, colours) => {
1229
+ scored.push({ ...scoreHostPair(kind, d, theme, colours), row, advisory: kind === "border" });
1230
+ };
1231
+ for (const theme of THEMES) {
1232
+ for (const [row, bg] of erRows(theme, fill)) {
1233
+ if (color || row === "even" && fill)
1234
+ score("text", theme, row, { fg: color ?? MERMAID_ER[theme].label, bg });
1235
+ if (stroke)
1236
+ score("border", theme, row, { fg: stroke, bg });
1237
+ }
1238
+ }
1239
+ return scored;
1228
1240
  };
1229
1241
  var scoreEr = (directives) => {
1230
- const pairs = [];
1231
- const skipped = [];
1242
+ const out = { pairs: [], skipped: [] };
1232
1243
  for (const d of directives) {
1233
1244
  if (d.kind !== "classDef" && d.kind !== "style")
1234
1245
  continue;
1235
1246
  const { fill, color, stroke } = d.properties;
1236
- if (!fill && !color && !stroke) {
1237
- skipped.push({ selector: d.selector, line: d.line, reason: "no color properties" });
1247
+ if (!(fill || color || stroke)) {
1248
+ skip(out, d, "no color properties");
1238
1249
  continue;
1239
1250
  }
1240
- const scored = [];
1241
- const score = (kind, theme, row, fg, bg) => {
1242
- scored.push({ ...scoreHostPair(kind, d, theme, fg, bg), row, advisory: kind === "border" });
1243
- };
1244
1251
  try {
1245
- for (const theme of THEMES) {
1246
- const anchor = MERMAID_ER[theme];
1247
- const rows = [
1248
- ["odd", anchor.odd],
1249
- ["even", fill ? compositeOver(fill, anchor.even) : anchor.even]
1250
- ];
1251
- for (const [row, bg] of rows) {
1252
- if (color || row === "even" && fill)
1253
- score("text", theme, row, color ?? anchor.label, bg);
1254
- if (stroke)
1255
- score("border", theme, row, stroke, bg);
1256
- }
1257
- }
1258
- pairs.push(...scored);
1252
+ out.pairs.push(...erPairs(d));
1259
1253
  } catch (err) {
1260
- skipped.push({
1261
- selector: d.selector,
1262
- line: d.line,
1263
- reason: `erDiagram pair unparseable: ${message(err)}`,
1264
- blocking: true
1265
- });
1254
+ skip(out, d, `erDiagram pair unparseable: ${message(err)}`, true);
1266
1255
  }
1267
1256
  }
1268
- return { pairs, skipped };
1257
+ return out;
1269
1258
  };
1270
1259
  var scoreForProfile = (directives, profile, keyword, canvas = GITHUB_CANVAS) => {
1271
1260
  if (profile === "mkdocs-material")
@@ -1307,9 +1296,7 @@ var themeVariable = (selector, line) => ({
1307
1296
  spans: {},
1308
1297
  line
1309
1298
  });
1310
- var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1311
- const pairs = [];
1312
- const skipped = [];
1299
+ var sequenceLines = (source) => {
1313
1300
  const lines = source.split(`
1314
1301
  `);
1315
1302
  const keywordLine = lines.findIndex((l) => /^\s*sequenceDiagram\b/.test(l)) + 1;
@@ -1317,94 +1304,125 @@ var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1317
1304
  const found = lines.findLastIndex((l) => new RegExp(`\\b${key}["']?\\s*:`).test(l));
1318
1305
  return found < 0 ? keywordLine : found + 1;
1319
1306
  };
1320
- const rectLines = lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []);
1321
- const mermaid = await getMermaidCore();
1322
- let config;
1323
- let db;
1307
+ return { lineOf, rectLines: lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []) };
1308
+ };
1309
+ var parseSequence = async (mermaid, source) => {
1324
1310
  try {
1325
- config = (await mermaid.parse(source)).config;
1326
- db = (await mermaid.mermaidAPI.getDiagramFromText(source)).db;
1311
+ const { config } = await mermaid.parse(source);
1312
+ const { db } = await mermaid.mermaidAPI.getDiagramFromText(source);
1313
+ return { config, db };
1327
1314
  } catch {
1328
- return { pairs, skipped };
1315
+ return null;
1316
+ }
1317
+ };
1318
+ var unmodelled = (config) => {
1319
+ if (config.themeCSS !== undefined) {
1320
+ return { selector: "themeCSS", reason: "themeCSS restyles the diagram with CSS the audit cannot model" };
1329
1321
  }
1330
- const block = (selector, reason) => {
1331
- skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
1332
- return { pairs, skipped };
1333
- };
1334
- if (config.themeCSS !== undefined)
1335
- return block("themeCSS", "themeCSS restyles the diagram with CSS the audit cannot model");
1336
1322
  const pinned = config.theme;
1337
1323
  if (pinned !== undefined && !SEQUENCE_THEMES.includes(String(pinned))) {
1338
- return block("theme", `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`);
1324
+ return {
1325
+ selector: "theme",
1326
+ reason: `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`
1327
+ };
1339
1328
  }
1329
+ return null;
1330
+ };
1331
+ var sequenceThemes = (pinned, host) => {
1332
+ const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
1333
+ return { light: theme("default"), dark: theme(host ? "default" : "dark") };
1334
+ };
1335
+ var sequencePair = ({ theme: t, page, vars, host, lineOf, out }) => (kind, fgKey, bg, { suffix = "", at } = {}) => {
1336
+ const color = (key) => {
1337
+ const value = vars[key];
1338
+ if (typeof value !== "string")
1339
+ throw new Error(`${key} is not a resolved colour`);
1340
+ return value;
1341
+ };
1342
+ const selector = fgKey + suffix;
1343
+ const line = at ?? lineOf(fgKey);
1344
+ try {
1345
+ const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
1346
+ out.pairs.push({
1347
+ ...scoreHostPair(kind, themeVariable(selector, line), t, { fg: color(fgKey), bg: box }),
1348
+ advisory: host
1349
+ });
1350
+ } catch (err) {
1351
+ out.skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
1352
+ }
1353
+ };
1354
+ var scoreHostSequence = (pair, page, features) => {
1355
+ const onPage = { colour: page };
1356
+ pair("border", "actorLineColor", onPage);
1357
+ pair("border", "signalColor", onPage, { suffix: " (arrowhead)" });
1358
+ if (features.activation)
1359
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1360
+ };
1361
+ var scoreAuthorSequence = (pair, page, features, rectLines) => {
1362
+ const onPage = { colour: page };
1363
+ pair("text", "actorTextColor", { key: "actorBkg" });
1364
+ pair("border", "actorBorder", { key: "actorBkg" });
1365
+ if (features.humanActor) {
1366
+ pair("text", "actorTextColor", onPage, { suffix: " (human actor label)" });
1367
+ pair("border", "actorBorder", onPage, { suffix: " (human actor glyph)" });
1368
+ }
1369
+ pair("border", "actorLineColor", onPage);
1370
+ pair("text", "signalTextColor", onPage);
1371
+ pair("border", "signalColor", onPage);
1372
+ if (features.note)
1373
+ pair("text", "noteTextColor", { key: "noteBkgColor" });
1374
+ if (features.frame) {
1375
+ pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
1376
+ pair("text", "loopTextColor", onPage);
1377
+ pair("border", "labelBoxBorderColor", onPage, { suffix: " (loop frame)" });
1378
+ }
1379
+ if (features.activation)
1380
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1381
+ if (features.numbered)
1382
+ pair("text", "sequenceNumberColor", { key: "signalColor" });
1383
+ for (const [i, rect] of features.rects.entries()) {
1384
+ const fill = { colour: rect };
1385
+ const where = { suffix: " (rect)", at: rectLines[i] };
1386
+ pair("text", "signalTextColor", fill, where);
1387
+ pair("border", "signalColor", fill, where);
1388
+ if (features.frame)
1389
+ pair("text", "loopTextColor", fill, where);
1390
+ }
1391
+ };
1392
+ var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1393
+ const out = { pairs: [], skipped: [] };
1394
+ const { lineOf, rectLines } = sequenceLines(source);
1395
+ const mermaid = await getMermaidCore();
1396
+ const parsed = await parseSequence(mermaid, source);
1397
+ if (!parsed)
1398
+ return out;
1399
+ const { config, db } = parsed;
1400
+ const block = (selector, reason) => {
1401
+ out.skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
1402
+ return out;
1403
+ };
1404
+ const problem = unmodelled(config);
1405
+ if (problem)
1406
+ return block(problem.selector, problem.reason);
1340
1407
  const features = sequenceFeatures(db);
1341
1408
  const host = profile === "mkdocs-material";
1342
- const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
1343
1409
  let variables;
1344
1410
  try {
1345
- variables = resolveVariables(mermaid, config, { light: theme("default"), dark: theme(host ? "default" : "dark") });
1411
+ variables = resolveVariables(mermaid, config, sequenceThemes(config.theme, host));
1346
1412
  } catch (err) {
1347
1413
  return block("themeVariables", `Mermaid could not resolve the theme: ${message(err)}`);
1348
1414
  }
1349
1415
  for (const t of THEMES) {
1350
1416
  const page = host ? MKDOCS_MATERIAL[t].bg : canvas[t];
1351
- const vars = variables[t];
1352
- const color = (key) => {
1353
- const value = vars[key];
1354
- if (typeof value !== "string")
1355
- throw new Error(`${key} is not a resolved colour`);
1356
- return value;
1357
- };
1358
- const pair = (kind, fgKey, bg, suffix = "", at) => {
1359
- const selector = fgKey + suffix;
1360
- const line = at ?? lineOf(fgKey);
1361
- try {
1362
- const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
1363
- pairs.push({ ...scoreHostPair(kind, themeVariable(selector, line), t, color(fgKey), box), advisory: host });
1364
- } catch (err) {
1365
- skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
1366
- }
1367
- };
1368
- const onPage = { colour: page };
1369
- if (host) {
1370
- pair("border", "actorLineColor", onPage);
1371
- pair("border", "signalColor", onPage, " (arrowhead)");
1372
- if (features.activation)
1373
- pair("border", "activationBorderColor", { key: "activationBkgColor" });
1374
- continue;
1375
- }
1376
- pair("text", "actorTextColor", { key: "actorBkg" });
1377
- pair("border", "actorBorder", { key: "actorBkg" });
1378
- if (features.humanActor) {
1379
- pair("text", "actorTextColor", onPage, " (human actor label)");
1380
- pair("border", "actorBorder", onPage, " (human actor glyph)");
1381
- }
1382
- pair("border", "actorLineColor", onPage);
1383
- pair("text", "signalTextColor", onPage);
1384
- pair("border", "signalColor", onPage);
1385
- if (features.note)
1386
- pair("text", "noteTextColor", { key: "noteBkgColor" });
1387
- if (features.frame) {
1388
- pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
1389
- pair("text", "loopTextColor", onPage);
1390
- pair("border", "labelBoxBorderColor", onPage, " (loop frame)");
1391
- }
1392
- if (features.activation)
1393
- pair("border", "activationBorderColor", { key: "activationBkgColor" });
1394
- if (features.numbered)
1395
- pair("text", "sequenceNumberColor", { key: "signalColor" });
1396
- for (const [i, rect] of features.rects.entries()) {
1397
- const fill = { colour: rect };
1398
- const at = rectLines[i];
1399
- pair("text", "signalTextColor", fill, " (rect)", at);
1400
- pair("border", "signalColor", fill, " (rect)", at);
1401
- if (features.frame)
1402
- pair("text", "loopTextColor", fill, " (rect)", at);
1403
- }
1417
+ const pair = sequencePair({ theme: t, page, vars: variables[t], host, lineOf, out });
1418
+ if (host)
1419
+ scoreHostSequence(pair, page, features);
1420
+ else
1421
+ scoreAuthorSequence(pair, page, features, rectLines);
1404
1422
  }
1405
- return { pairs, skipped };
1423
+ return out;
1406
1424
  };
1407
- var scoreDiagram = (source, directives, profile, keyword, canvas = GITHUB_CANVAS) => keyword === "sequenceDiagram" ? scoreSequence(source, profile, canvas) : Promise.resolve(scoreForProfile(directives, profile, keyword, canvas));
1425
+ var scoreDiagram = ({ source, directives, keyword }, profile, canvas = GITHUB_CANVAS) => keyword === "sequenceDiagram" ? scoreSequence(source, profile, canvas) : Promise.resolve(scoreForProfile(directives, profile, keyword, canvas));
1408
1426
  var detectProfile = (filePath) => {
1409
1427
  let dir = dirname(resolve(filePath));
1410
1428
  for (let i = 0;i < 20; i++) {
@@ -1496,7 +1514,7 @@ var canonicalPreset = (name) => {
1496
1514
  var configFromPreset = (name) => {
1497
1515
  const canonical = canonicalPreset(name);
1498
1516
  const preset = canonical ? PRESETS[canonical] : undefined;
1499
- if (!canonical || !preset) {
1517
+ if (!(canonical && preset)) {
1500
1518
  throw new Error(`Unknown preset '${name}'. Valid: ${PRESET_NAMES.join(", ")}`);
1501
1519
  }
1502
1520
  return {
@@ -1575,7 +1593,7 @@ var measure = async (diagram, config) => {
1575
1593
  return null;
1576
1594
  return { stats, metrics: calculateComplexity(stats, config) };
1577
1595
  };
1578
- var finding = (ctx, diagram, rule, message2, remediation) => ({
1596
+ var finding = (ctx, diagram, rule, { message: message2, remediation }) => ({
1579
1597
  file: ctx.file,
1580
1598
  line: diagram.startLine,
1581
1599
  endLine: diagram.endLine,
@@ -1626,7 +1644,7 @@ var contrastOf = (diagram, settings) => {
1626
1644
  }
1627
1645
  let result = bySettings.get(key);
1628
1646
  if (!result) {
1629
- result = scoreDiagram(diagram.source, diagram.directives, profile, diagram.keyword, canvas);
1647
+ result = scoreDiagram(diagram, profile, canvas);
1630
1648
  bySettings.set(key, result);
1631
1649
  }
1632
1650
  return result;
@@ -1648,7 +1666,7 @@ var complexityRule = (spec) => ({
1648
1666
  continue;
1649
1667
  const hit = spec.hit(measured, config);
1650
1668
  if (hit)
1651
- found.push(finding(ctx, diagram, spec.id, hit.message, hit.remediation));
1669
+ found.push(finding(ctx, diagram, spec.id, hit));
1652
1670
  }
1653
1671
  return found;
1654
1672
  }
@@ -1793,6 +1811,30 @@ var contrastDirectiveUnauditable = {
1793
1811
  var AA_NORMAL2 = 4.5;
1794
1812
  var CANDIDATES = ["#000000", "#ffffff"];
1795
1813
  var ratioOn = (fg, bg) => wcagRatio(compositeOver(fg, bg), bg);
1814
+ var remediationOf = (profile, kind) => {
1815
+ if (profile === "mkdocs-material") {
1816
+ return "The host theme forces the label text, so change the fill until the host text reads on it in both themes.";
1817
+ }
1818
+ if (kind === "themeVariables") {
1819
+ return "A sequence diagram colour is a theme variable, and text with no box of its own sits on the page, so set the variable to a colour that reads on its background in both themes, or remove it and let the theme set it.";
1820
+ }
1821
+ if (kind === "linkStyle") {
1822
+ return "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.";
1823
+ }
1824
+ return "Darken or lighten the fill, or change color:, until the ratio reaches 4.5:1 in every theme.";
1825
+ };
1826
+ var textFinding = (ctx, diagram, p, profile) => {
1827
+ const where = [p.theme, p.row].filter(Boolean).join("/");
1828
+ const line = fileLine(diagram, p.line);
1829
+ return {
1830
+ file: ctx.file,
1831
+ line,
1832
+ endLine: line,
1833
+ rule: RULE.TEXT,
1834
+ message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1835
+ remediation: remediationOf(profile, p.directive.kind)
1836
+ };
1837
+ };
1796
1838
  var contrastTextBelowAa = {
1797
1839
  id: RULE.TEXT,
1798
1840
  category: "contrast",
@@ -1803,18 +1845,8 @@ var contrastTextBelowAa = {
1803
1845
  const found = [];
1804
1846
  for (const diagram of ctx.doc.diagrams) {
1805
1847
  for (const p of (await contrastOf(diagram, settings)).pairs) {
1806
- if (p.kind !== "text" || p.passes)
1807
- continue;
1808
- const where = [p.theme, p.row].filter(Boolean).join("/");
1809
- const line = fileLine(diagram, p.line);
1810
- found.push({
1811
- file: ctx.file,
1812
- line,
1813
- endLine: line,
1814
- rule: RULE.TEXT,
1815
- message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1816
- remediation: settings.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 === "themeVariables" ? "A sequence diagram colour is a theme variable, and text with no box of its own sits on the page, so set the variable to a colour that reads on its background in both themes, or remove it and let the theme set it." : 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."
1817
- });
1848
+ if (p.kind === "text" && !p.passes)
1849
+ found.push(textFinding(ctx, diagram, p, settings.profile));
1818
1850
  }
1819
1851
  }
1820
1852
  return found;
@@ -1873,7 +1905,7 @@ var parseDiagramUnparseable = {
1873
1905
  if (!parseFailed(diagram.source, stats))
1874
1906
  continue;
1875
1907
  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.";
1876
- found.push(finding(ctx, diagram, RULE.UNPARSEABLE, messageOf(stats), remediation));
1908
+ found.push(finding(ctx, diagram, RULE.UNPARSEABLE, { message: messageOf(stats), remediation }));
1877
1909
  }
1878
1910
  return found;
1879
1911
  }
@@ -1885,7 +1917,10 @@ var parseFenceUnterminated = {
1885
1917
  id: RULE.FENCE_UNTERMINATED,
1886
1918
  category: "parse",
1887
1919
  summary: "a mermaid fence that is never closed",
1888
- check: (ctx) => ctx.doc.diagrams.filter((d) => !d.terminated).map((d) => finding(ctx, d, RULE.FENCE_UNTERMINATED, `mermaid fence opened at line ${d.startLine} is never closed`, "Add a closing fence directly after the last line of the diagram, with the opening line's prefix: `> ` in a quote, the item's indent in a list.")),
1920
+ check: (ctx) => ctx.doc.diagrams.filter((d) => !d.terminated).map((d) => finding(ctx, d, RULE.FENCE_UNTERMINATED, {
1921
+ message: `mermaid fence opened at line ${d.startLine} is never closed`,
1922
+ remediation: "Add a closing fence directly after the last line of the diagram, with the opening line's prefix: `> ` in a quote, the item's indent in a list."
1923
+ })),
1889
1924
  fix: (ctx) => {
1890
1925
  const edits = [];
1891
1926
  const { src } = ctx.doc;
@@ -1982,20 +2017,20 @@ var checkCanvas = (where, rule, name, value) => {
1982
2017
  }
1983
2018
  }
1984
2019
  };
1985
- var checkOptions = (where, rule, options) => {
1986
- const accepted = Object.keys(rule.options ?? {});
1987
- for (const [name, value] of Object.entries(options)) {
1988
- const want = rule.options?.[name];
1989
- if (!want) {
1990
- const list = accepted.length > 0 ? `it accepts ${accepted.join(", ")}` : "it takes no options";
1991
- throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
1992
- }
1993
- if (typeOf(value) !== want) {
1994
- throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be ${describeType(want)}`);
1995
- }
1996
- if (want === "canvas")
1997
- checkCanvas(where, rule, name, value);
2020
+ var checkOption = (where, rule, name, value) => {
2021
+ const want = rule.options?.[name];
2022
+ if (!want) {
2023
+ const accepted = Object.keys(rule.options ?? {});
2024
+ const list = accepted.length > 0 ? `it accepts ${accepted.join(", ")}` : "it takes no options";
2025
+ throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
1998
2026
  }
2027
+ if (typeOf(value) !== want) {
2028
+ throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be ${describeType(want)}`);
2029
+ }
2030
+ if (want === "canvas")
2031
+ checkCanvas(where, rule, name, value);
2032
+ };
2033
+ var checkSharedOptions = (where, rule, options) => {
1999
2034
  if (options.profile === "mkdocs-material" && options.canvas !== undefined && rule.options?.canvas === "canvas") {
2000
2035
  throw new RuleOptionError(where, rule.id, "canvas", `option "canvas" of "${rule.id}" applies under the github profile only, and this rule sets profile mkdocs-material, whose page is Material's own`);
2001
2036
  }
@@ -2006,10 +2041,12 @@ var checkOptions = (where, rule, options) => {
2006
2041
  throw new RuleOptionError(where, rule.id, "profile", `option "profile" of "${rule.id}" must be one of ${PROFILES.join(", ")}`);
2007
2042
  }
2008
2043
  };
2009
- var buildRegistry = (input) => {
2010
- const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
2011
- const selection = input.selection ?? {};
2012
- checkSelection(selection, categories);
2044
+ var checkOptions = (where, rule, options) => {
2045
+ for (const [name, value] of Object.entries(options))
2046
+ checkOption(where, rule, name, value);
2047
+ checkSharedOptions(where, rule, options);
2048
+ };
2049
+ var applyConfig = (rules, config, ignoreUnknownPluginIds) => {
2013
2050
  const off = new Set;
2014
2051
  const options = new Map;
2015
2052
  const ignored = [];
@@ -2028,6 +2065,13 @@ var buildRegistry = (input) => {
2028
2065
  if (setting.severity === "off")
2029
2066
  off.add(id);
2030
2067
  }
2068
+ return { off, options, ignored };
2069
+ };
2070
+ var buildRegistry = (input) => {
2071
+ const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
2072
+ const selection = input.selection ?? {};
2073
+ checkSelection(selection, categories);
2074
+ const { off, options, ignored } = applyConfig(rules, config, ignoreUnknownPluginIds);
2031
2075
  for (const rule of rules) {
2032
2076
  if (selection.enable?.includes(rule.category))
2033
2077
  off.delete(rule.id);
@@ -2080,6 +2124,17 @@ var checkRunFlags = (flags, prefix = "") => {
2080
2124
  var canvasOptions = (rule) => Object.entries(rule.options ?? {}).filter(([, type]) => type === "canvas").map(([name]) => name);
2081
2125
  var hasFlagCanvas = (flags) => THEMES.some((side) => flags.canvas?.[side] !== undefined);
2082
2126
  var setsAside = (rule, options, flags, file) => !hasFlagCanvas(flags) && rule.options?.profile !== undefined && (options.profile ?? "auto") === "auto" && canvasOptions(rule).some((name) => options[name] !== undefined) && detectProfile(file) === "mkdocs-material";
2127
+ var mergeCanvas = (current, canvas) => {
2128
+ const merged = {
2129
+ ...typeof current === "object" && current !== null ? current : {}
2130
+ };
2131
+ for (const side of THEMES) {
2132
+ const value = canvas[side];
2133
+ if (value !== undefined)
2134
+ merged[side] = value;
2135
+ }
2136
+ return merged;
2137
+ };
2083
2138
  var optionsFor = (rule, configured, flags, file) => {
2084
2139
  const options = { ...configured };
2085
2140
  for (const name of ["preset", "profile"]) {
@@ -2091,18 +2146,8 @@ var optionsFor = (rule, configured, flags, file) => {
2091
2146
  options.preset = canonicalPreset(options.preset) ?? options.preset;
2092
2147
  const canvas = flags.canvas;
2093
2148
  if (canvas && hasFlagCanvas(flags)) {
2094
- for (const name of canvasOptions(rule)) {
2095
- const current = options[name];
2096
- const merged = {
2097
- ...typeof current === "object" && current !== null ? current : {}
2098
- };
2099
- for (const side of THEMES) {
2100
- const value = canvas[side];
2101
- if (value !== undefined)
2102
- merged[side] = value;
2103
- }
2104
- options[name] = merged;
2105
- }
2149
+ for (const name of canvasOptions(rule))
2150
+ options[name] = mergeCanvas(options[name], canvas);
2106
2151
  }
2107
2152
  if (file !== undefined && setsAside(rule, options, flags, file)) {
2108
2153
  for (const name of canvasOptions(rule))
@@ -2127,6 +2172,7 @@ import { extname, join as join2, resolve as resolve2 } from "node:path";
2127
2172
  var DIAGRAM_FILE_EXTS = new Set([...DIAGRAM_EXTS, ".md", ".markdown"]);
2128
2173
  var SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tmp", "__pycache__"]);
2129
2174
  var skipDir = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
2175
+ var isDiagramFile = (name) => DIAGRAM_FILE_EXTS.has(extname(name).toLowerCase());
2130
2176
  var linksToDir = (path) => {
2131
2177
  try {
2132
2178
  return statSync(path).isDirectory();
@@ -2139,6 +2185,24 @@ var collectFiles = (paths) => {
2139
2185
  const unreadable = [];
2140
2186
  const symlinks = [];
2141
2187
  const seen = new Set;
2188
+ const add = (abs) => {
2189
+ if (seen.has(abs))
2190
+ return;
2191
+ seen.add(abs);
2192
+ files.push(abs);
2193
+ };
2194
+ const visit = (dir, entry) => {
2195
+ const abs = join2(dir, entry.name);
2196
+ if (entry.isSymbolicLink()) {
2197
+ if (!skipDir(entry.name) && (isDiagramFile(entry.name) || linksToDir(abs)))
2198
+ symlinks.push(abs);
2199
+ } else if (entry.isDirectory()) {
2200
+ if (!skipDir(entry.name))
2201
+ walk(abs);
2202
+ } else if (entry.isFile() && isDiagramFile(entry.name)) {
2203
+ add(abs);
2204
+ }
2205
+ };
2142
2206
  const walk = (dir) => {
2143
2207
  let entries;
2144
2208
  try {
@@ -2147,22 +2211,8 @@ var collectFiles = (paths) => {
2147
2211
  unreadable.push(dir);
2148
2212
  return;
2149
2213
  }
2150
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2151
- const abs = join2(dir, entry.name);
2152
- if (entry.isSymbolicLink()) {
2153
- if (!skipDir(entry.name) && (DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase()) || linksToDir(abs))) {
2154
- symlinks.push(abs);
2155
- }
2156
- } else if (entry.isDirectory()) {
2157
- if (!skipDir(entry.name))
2158
- walk(abs);
2159
- } else if (entry.isFile() && DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase())) {
2160
- if (!seen.has(abs)) {
2161
- seen.add(abs);
2162
- files.push(abs);
2163
- }
2164
- }
2165
- }
2214
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name)))
2215
+ visit(dir, entry);
2166
2216
  };
2167
2217
  for (const p of paths) {
2168
2218
  const abs = resolve2(p);
@@ -2170,9 +2220,8 @@ var collectFiles = (paths) => {
2170
2220
  const st = statSync(abs);
2171
2221
  if (st.isDirectory()) {
2172
2222
  walk(abs);
2173
- } else if (st.isFile() && !seen.has(abs)) {
2174
- seen.add(abs);
2175
- files.push(abs);
2223
+ } else if (st.isFile()) {
2224
+ add(abs);
2176
2225
  }
2177
2226
  } catch {
2178
2227
  unreadable.push(p);
@@ -2186,21 +2235,24 @@ var OMIT = new Set(["position", "data"]);
2186
2235
  var normalise = (node, opts = {}) => {
2187
2236
  const out = {};
2188
2237
  const isDiagram = node.type === "code" && node.lang === "mermaid";
2238
+ const blank = isDiagram && Boolean(opts.blankDiagrams);
2189
2239
  for (const [key, value] of Object.entries(node)) {
2190
2240
  if (OMIT.has(key))
2191
2241
  continue;
2192
- if (key === "children" && Array.isArray(value)) {
2193
- out.children = value.map((child) => normalise(child, opts));
2194
- } else if (key === "value" && typeof value === "string") {
2195
- if (isDiagram && opts.blankDiagrams)
2196
- continue;
2197
- out.value = node.type === "text" ? value.replace(/\s+/g, " ").trim() : value;
2198
- } else {
2199
- out[key] = value;
2200
- }
2242
+ if (blank && isStringValue(key, value))
2243
+ continue;
2244
+ out[key] = normaliseField(node, key, value, opts);
2201
2245
  }
2202
2246
  return out;
2203
2247
  };
2248
+ var isStringValue = (key, value) => key === "value" && typeof value === "string";
2249
+ var normaliseField = (node, key, value, opts) => {
2250
+ if (key === "children" && Array.isArray(value))
2251
+ return value.map((child) => normalise(child, opts));
2252
+ if (isStringValue(key, value))
2253
+ return node.type === "text" ? value.replace(/\s+/g, " ").trim() : value;
2254
+ return value;
2255
+ };
2204
2256
  var sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2205
2257
  var sameTree = (a, b, opts) => sameJson(normalise(a, opts), normalise(b, opts));
2206
2258
  var sameDiagrams = async (before, after) => {
@@ -2265,63 +2317,68 @@ var overlaps = (edits) => {
2265
2317
  return sorted.some((e, i) => i > 0 && e.start < sorted[i - 1].end);
2266
2318
  };
2267
2319
  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";
2320
+ var keyOf = (run, e) => `${e.rule}\x00${run.current.slice(e.start, e.end)}\x00${e.text}`;
2321
+ var attempt = async (run, doc, edits) => {
2322
+ const next = splice(run.current, edits);
2323
+ const after = buildDocModel(next, run.kind);
2324
+ for (const e of edits)
2325
+ if (await verify(e, doc, after) !== "accept")
2326
+ return false;
2327
+ run.current = next;
2328
+ run.applied.push(...edits);
2329
+ return true;
2330
+ };
2331
+ var soundEdits = (run, proposed) => proposed.filter((e) => {
2332
+ if (wellFormed(e, run.current.length))
2333
+ return true;
2334
+ const key = `${e.rule}\x00malformed\x00${e.start}:${e.end}`;
2335
+ if (!run.refusedKeys.has(key)) {
2336
+ run.refusedKeys.add(key);
2337
+ run.refused.push(e);
2338
+ }
2339
+ return false;
2340
+ });
2341
+ var applyRule = async (run, doc, rule) => {
2342
+ const options = optionsFor(rule, run.registry.options.get(rule.id), run.flags, run.file);
2343
+ const proposed = await rule.fix?.({ doc, file: run.file, options, helpers: HELPERS }) ?? [];
2344
+ const edits = soundEdits(run, proposed).filter((e) => run.current.slice(e.start, e.end) !== e.text && !run.refusedKeys.has(keyOf(run, e)));
2345
+ if (edits.length === 0)
2346
+ return false;
2347
+ if (edits.length > 1 && !overlaps(edits) && await attempt(run, doc, edits))
2348
+ return true;
2349
+ for (const edit of edits) {
2350
+ if (await attempt(run, doc, [edit]))
2351
+ return true;
2352
+ run.refusedKeys.add(keyOf(run, edit));
2353
+ run.refused.push(edit);
2354
+ }
2355
+ return false;
2356
+ };
2357
+ var runPass = async (run) => {
2358
+ const doc = buildDocModel(run.current, run.kind);
2359
+ for (const rule of run.registry.fixOrder)
2360
+ if (await applyRule(run, doc, rule))
2361
+ return rule.id;
2362
+ return;
2363
+ };
2268
2364
  var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
2269
2365
  checkRunFlags(flags);
2270
- const kind = documentKind(file);
2271
- const applied = [];
2272
- const refused = [];
2273
- const refusedKeys = new Set;
2274
- let current = src;
2366
+ const run = {
2367
+ file,
2368
+ flags,
2369
+ registry,
2370
+ kind: documentKind(file),
2371
+ current: src,
2372
+ applied: [],
2373
+ refused: [],
2374
+ refusedKeys: new Set
2375
+ };
2275
2376
  const keptBy = [];
2276
2377
  for (let pass = 0;pass < MAX_PASSES; pass++) {
2277
- const doc = buildDocModel(current, kind);
2278
- const keyOf = (e) => `${e.rule}\x00${current.slice(e.start, e.end)}\x00${e.text}`;
2279
- const attempt = async (edits) => {
2280
- const next = splice(current, edits);
2281
- const after = buildDocModel(next, kind);
2282
- for (const e of edits)
2283
- if (await verify(e, doc, after) !== "accept")
2284
- return false;
2285
- current = next;
2286
- applied.push(...edits);
2287
- return true;
2288
- };
2289
- let progressed = false;
2290
- for (const rule of registry.fixOrder) {
2291
- const options = optionsFor(rule, registry.options.get(rule.id), flags, file);
2292
- const proposed = await rule.fix?.({ doc, file, options, helpers: HELPERS }) ?? [];
2293
- const sound = proposed.filter((e) => {
2294
- if (wellFormed(e, current.length))
2295
- return true;
2296
- const key = `${e.rule}\x00malformed\x00${e.start}:${e.end}`;
2297
- if (!refusedKeys.has(key)) {
2298
- refusedKeys.add(key);
2299
- refused.push(e);
2300
- }
2301
- return false;
2302
- });
2303
- const edits = sound.filter((e) => current.slice(e.start, e.end) !== e.text && !refusedKeys.has(keyOf(e)));
2304
- if (edits.length === 0)
2305
- continue;
2306
- if (edits.length > 1 && !overlaps(edits) && await attempt(edits)) {
2307
- progressed = true;
2308
- } else {
2309
- for (const edit of edits) {
2310
- if (await attempt([edit])) {
2311
- progressed = true;
2312
- break;
2313
- }
2314
- refusedKeys.add(keyOf(edit));
2315
- refused.push(edit);
2316
- }
2317
- }
2318
- if (progressed) {
2319
- keptBy.push(rule.id);
2320
- break;
2321
- }
2322
- }
2323
- if (!progressed)
2324
- return { output: current, applied, refused };
2378
+ const kept = await runPass(run);
2379
+ if (kept === undefined)
2380
+ return { output: run.current, applied: run.applied, refused: run.refused };
2381
+ keptBy.push(kept);
2325
2382
  }
2326
2383
  const lingering = [...new Set(keptBy.slice(-RECENT_PASSES))].sort();
2327
2384
  throw new FixNotConvergingError(file, MAX_PASSES, lingering);
@@ -2439,26 +2496,22 @@ var localSources = async (root) => {
2439
2496
  }
2440
2497
  return names.filter((name) => LOCAL_EXTENSIONS.has(extname3(name)) && !name.endsWith(".d.ts") && !name.endsWith(".d.mts")).map((name) => ({ kind: "local", spec: join4(LOCAL_RULES, name), path: join4(dir, name) }));
2441
2498
  };
2499
+ var CONDITIONS = ["node", "import", "default"];
2500
+ var pickFirst = (candidates) => {
2501
+ for (const item of candidates) {
2502
+ const found = pick(item);
2503
+ if (found)
2504
+ return found;
2505
+ }
2506
+ return null;
2507
+ };
2442
2508
  var pick = (target) => {
2443
2509
  if (typeof target === "string")
2444
2510
  return target;
2445
- if (Array.isArray(target)) {
2446
- for (const item of target) {
2447
- const found = pick(item);
2448
- if (found)
2449
- return found;
2450
- }
2451
- return null;
2452
- }
2453
- if (isRecord2(target)) {
2454
- for (const condition of ["node", "import", "default"]) {
2455
- if (condition in target) {
2456
- const found = pick(target[condition]);
2457
- if (found)
2458
- return found;
2459
- }
2460
- }
2461
- }
2511
+ if (Array.isArray(target))
2512
+ return pickFirst(target);
2513
+ if (isRecord2(target))
2514
+ return pickFirst(CONDITIONS.filter((c) => (c in target)).map((c) => target[c]));
2462
2515
  return null;
2463
2516
  };
2464
2517
  var entryOf = (pkg) => {
@@ -2560,23 +2613,40 @@ var importModule = async (source, runtime) => {
2560
2613
  };
2561
2614
  var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]", "canvas"]);
2562
2615
  var RESERVED_OPTIONS = ["preset", "profile"];
2563
- var checkRule = (spec, key, raw, own) => {
2616
+ var checkRuleCategory = (spec, key, category, own) => {
2564
2617
  const where = `rule "${key}"`;
2565
- if (!isRecord3(raw))
2566
- throw new PluginContractError(spec, `${where} must be an object`);
2567
- if (!RULE_KEY.test(key)) {
2568
- throw new PluginContractError(spec, `${where} is not a valid key; use lower-case words joined by hyphens`);
2569
- }
2570
- const { category, summary, options, check, fix } = raw;
2571
2618
  if (typeof category !== "string" || !CATEGORY_WORD.test(category)) {
2572
2619
  throw new PluginContractError(spec, `${where} needs a category of one lower-case word`);
2573
2620
  }
2574
- if (!BUILTIN_CATEGORIES.has(category) && !own.has(category)) {
2621
+ if (!(BUILTIN_CATEGORIES.has(category) || own.has(category))) {
2575
2622
  throw new PluginContractError(spec, `${where} uses the category "${category}", which this plugin does not declare in meta.categories`);
2576
2623
  }
2577
2624
  if (!key.startsWith(`${category}-`)) {
2578
2625
  throw new PluginContractError(spec, `${where} must start with its category, as "${category}-short-name"`);
2579
2626
  }
2627
+ };
2628
+ var checkRuleOptions = (spec, where, options) => {
2629
+ if (options === undefined)
2630
+ return;
2631
+ const valid = isRecord3(options) && Object.values(options).every((t) => OPTION_TYPES.has(t));
2632
+ if (!valid) {
2633
+ throw new PluginContractError(spec, `${where} options must map names to number, string, boolean, string[] or canvas`);
2634
+ }
2635
+ for (const name of RESERVED_OPTIONS) {
2636
+ if (name in options && options[name] !== "string") {
2637
+ 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`);
2638
+ }
2639
+ }
2640
+ };
2641
+ var checkRule = (spec, key, raw, own) => {
2642
+ const where = `rule "${key}"`;
2643
+ if (!isRecord3(raw))
2644
+ throw new PluginContractError(spec, `${where} must be an object`);
2645
+ if (!RULE_KEY.test(key)) {
2646
+ throw new PluginContractError(spec, `${where} is not a valid key; use lower-case words joined by hyphens`);
2647
+ }
2648
+ const { category, summary, options, check, fix } = raw;
2649
+ checkRuleCategory(spec, key, category, own);
2580
2650
  if (typeof summary !== "string" || summary === "")
2581
2651
  throw new PluginContractError(spec, `${where} needs a summary`);
2582
2652
  if (typeof check !== "function")
@@ -2584,17 +2654,7 @@ var checkRule = (spec, key, raw, own) => {
2584
2654
  if (fix !== undefined && typeof fix !== "function") {
2585
2655
  throw new PluginContractError(spec, `${where} fix must be a function`);
2586
2656
  }
2587
- if (options !== undefined) {
2588
- const valid = isRecord3(options) && Object.values(options).every((t) => OPTION_TYPES.has(t));
2589
- if (!valid) {
2590
- throw new PluginContractError(spec, `${where} options must map names to number, string, boolean, string[] or canvas`);
2591
- }
2592
- for (const name of RESERVED_OPTIONS) {
2593
- if (name in options && options[name] !== "string") {
2594
- 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`);
2595
- }
2596
- }
2597
- }
2657
+ checkRuleOptions(spec, where, options);
2598
2658
  return raw;
2599
2659
  };
2600
2660
  var checkCategories = (spec, raw) => {
@@ -2615,7 +2675,7 @@ var checkCategories = (spec, raw) => {
2615
2675
  return raw;
2616
2676
  };
2617
2677
  var parsePlugin = (spec, raw, reserved) => {
2618
- if (!isRecord3(raw) || !isRecord3(raw.meta) || !isRecord3(raw.rules)) {
2678
+ if (!(isRecord3(raw) && isRecord3(raw.meta) && isRecord3(raw.rules))) {
2619
2679
  throw new PluginContractError(spec, "the default export must be { meta, rules }");
2620
2680
  }
2621
2681
  const { name, namespace, apiVersion } = raw.meta;
@@ -2635,6 +2695,31 @@ var parsePlugin = (spec, raw, reserved) => {
2635
2695
  return { meta: { name, namespace, apiVersion, categories }, rules };
2636
2696
  };
2637
2697
  var loadPackage = async (source, reserved) => parsePlugin(source.spec, (await importModule(source)).default, reserved);
2698
+ var keyOf2 = (source) => basename(source.path, extname4(source.path));
2699
+ var groupByKey = (sources) => {
2700
+ const byKey = new Map;
2701
+ for (const source of sources)
2702
+ byKey.set(keyOf2(source), [...byKey.get(keyOf2(source)) ?? [], source]);
2703
+ return byKey;
2704
+ };
2705
+ var duplicateKeyError = (key, group) => 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`);
2706
+ var mergeCategories = (seen, spec, declared) => {
2707
+ for (const [word, description] of Object.entries(declared)) {
2708
+ if (seen.categories[word] !== undefined && seen.categories[word] !== description) {
2709
+ throw new PluginConflictError(spec, seen.owner[word], `declares category "${word}" with a different description than another local file`);
2710
+ }
2711
+ seen.categories[word] = description;
2712
+ seen.owner[word] = spec;
2713
+ }
2714
+ };
2715
+ var importLocal = async (source, seen, runtime) => {
2716
+ const mod = await importModule(source, runtime);
2717
+ if (mod.default === undefined) {
2718
+ throw new PluginContractError(source.spec, "has no default export; export one rule");
2719
+ }
2720
+ mergeCategories(seen, source.spec, checkCategories(source.spec, mod.categories));
2721
+ return mod.default;
2722
+ };
2638
2723
  var loadLocal = async (sources, runtime) => {
2639
2724
  const failures = [];
2640
2725
  const fail = (err) => {
@@ -2642,35 +2727,19 @@ var loadLocal = async (sources, runtime) => {
2642
2727
  throw err;
2643
2728
  failures.push(err);
2644
2729
  };
2645
- const categories = {};
2646
- const owner = {};
2730
+ const seen = { categories: {}, owner: {} };
2731
+ const { categories } = seen;
2647
2732
  const loaded = [];
2648
- const keyOf = (source) => basename(source.path, extname4(source.path));
2649
- const byKey = new Map;
2650
- for (const source of sources)
2651
- byKey.set(keyOf(source), [...byKey.get(keyOf(source)) ?? [], source]);
2733
+ const byKey = groupByKey(sources);
2652
2734
  for (const [key, group] of byKey) {
2653
- if (group.length < 2)
2654
- continue;
2655
- 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`));
2735
+ if (group.length > 1)
2736
+ fail(duplicateKeyError(key, group));
2656
2737
  }
2657
2738
  for (const source of sources) {
2658
- if ((byKey.get(keyOf(source))?.length ?? 0) > 1)
2739
+ if ((byKey.get(keyOf2(source))?.length ?? 0) > 1)
2659
2740
  continue;
2660
2741
  try {
2661
- const mod = await importModule(source, runtime);
2662
- if (mod.default === undefined) {
2663
- throw new PluginContractError(source.spec, "has no default export; export one rule");
2664
- }
2665
- const declared = checkCategories(source.spec, mod.categories);
2666
- for (const [word, description] of Object.entries(declared)) {
2667
- if (categories[word] !== undefined && categories[word] !== description) {
2668
- throw new PluginConflictError(source.spec, owner[word], `declares category "${word}" with a different description than another local file`);
2669
- }
2670
- categories[word] = description;
2671
- owner[word] = source.spec;
2672
- }
2673
- loaded.push({ key: keyOf(source), source, raw: mod.default });
2742
+ loaded.push({ key: keyOf2(source), source, raw: await importLocal(source, seen, runtime) });
2674
2743
  } catch (err) {
2675
2744
  fail(err);
2676
2745
  }
@@ -2701,7 +2770,7 @@ var toRule = (namespace, key, rule) => {
2701
2770
  } catch (err) {
2702
2771
  return report(`threw: ${message2(err)}`);
2703
2772
  }
2704
- if (!Array.isArray(found) || !found.every(validFinding)) {
2773
+ if (!(Array.isArray(found) && found.every(validFinding))) {
2705
2774
  return report("returned something other than a list of { line, message } findings");
2706
2775
  }
2707
2776
  return found.map((f) => ({
@@ -2723,7 +2792,7 @@ var toRule = (namespace, key, rule) => {
2723
2792
  if (!Array.isArray(edits))
2724
2793
  throw new PluginFixerError(id, "fixer returned something other than a list of edits");
2725
2794
  return edits.map((e) => {
2726
- if (!isRecord3(e) || !isRecord3(e.expect) || !EXPECTATIONS.has(e.expect.kind)) {
2795
+ if (!(isRecord3(e) && isRecord3(e.expect) && EXPECTATIONS.has(e.expect.kind))) {
2727
2796
  throw new PluginFixerError(id, "fixer returned an edit with no valid expectation");
2728
2797
  }
2729
2798
  return { ...e, rule: id };
@@ -2733,26 +2802,7 @@ var toRule = (namespace, key, rule) => {
2733
2802
  };
2734
2803
 
2735
2804
  // src/plugins/index.ts
2736
- var loadPlugins = async (root, config, mode) => {
2737
- const { sources: found, failures: unresolved } = await discoverSources(root, config, mode === "off");
2738
- const set = {
2739
- rules: [],
2740
- categories: new Map(BUILTIN_CATEGORIES),
2741
- sources: [],
2742
- skipped: [],
2743
- failures: []
2744
- };
2745
- if (mode === "off") {
2746
- return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((u) => u.error.source)] };
2747
- }
2748
- const settle = (error, kind) => {
2749
- if (mode === "strict")
2750
- throw error;
2751
- if (!set.failures.some((f) => f.source === error.source))
2752
- set.failures.push({ source: error.source, kind, error });
2753
- };
2754
- for (const { error, kind } of unresolved)
2755
- settle(error, kind);
2805
+ var loadAll = async (found, settle) => {
2756
2806
  const loaded = [];
2757
2807
  const locals = found.filter((s) => s.kind === "local");
2758
2808
  if (locals.length > 0) {
@@ -2771,28 +2821,59 @@ var loadPlugins = async (root, config, mode) => {
2771
2821
  settle(err, source.kind);
2772
2822
  }
2773
2823
  }
2774
- const namespaces = new Map;
2775
- const declaredBy = new Map;
2824
+ return loaded;
2825
+ };
2826
+ var conflictOf = (plugin, spec, claims) => {
2827
+ const { namespace } = plugin.meta;
2828
+ const words = Object.keys(plugin.meta.categories ?? {});
2829
+ const taken = claims.namespaces.get(namespace);
2830
+ const clash = words.find((w) => claims.declaredBy.has(w));
2831
+ if (!(taken || clash))
2832
+ return null;
2833
+ const other = taken ?? claims.declaredBy.get(clash);
2834
+ const reason2 = taken ? `namespace "${namespace}" is already used by ${other}` : `category "${clash}" is already declared by ${other}`;
2835
+ return new PluginConflictError(spec, other, reason2);
2836
+ };
2837
+ var acceptClaims = (loaded, set, settle) => {
2838
+ const claims = { namespaces: new Map, declaredBy: new Map };
2776
2839
  const accepted = [];
2777
2840
  for (const entry of loaded) {
2778
2841
  const { plugin, spec, kind } = entry;
2779
- const { namespace } = plugin.meta;
2780
- const words = Object.keys(plugin.meta.categories ?? {});
2781
- const taken = namespaces.get(namespace);
2782
- const clash = words.find((w) => declaredBy.has(w));
2783
- if (taken || clash) {
2784
- const other = taken ?? declaredBy.get(clash);
2785
- const reason2 = taken ? `namespace "${namespace}" is already used by ${other}` : `category "${clash}" is already declared by ${other}`;
2786
- settle(new PluginConflictError(spec, other, reason2), kind);
2842
+ const conflict = conflictOf(plugin, spec, claims);
2843
+ if (conflict) {
2844
+ settle(conflict, kind);
2787
2845
  continue;
2788
2846
  }
2789
- namespaces.set(namespace, spec);
2847
+ claims.namespaces.set(plugin.meta.namespace, spec);
2790
2848
  for (const [word, description] of Object.entries(plugin.meta.categories ?? {})) {
2791
- declaredBy.set(word, spec);
2849
+ claims.declaredBy.set(word, spec);
2792
2850
  set.categories.set(word, description);
2793
2851
  }
2794
2852
  accepted.push(entry);
2795
2853
  }
2854
+ return accepted;
2855
+ };
2856
+ var loadPlugins = async (root, config, mode) => {
2857
+ const { sources: found, failures: unresolved } = await discoverSources(root, config, mode === "off");
2858
+ const set = {
2859
+ rules: [],
2860
+ categories: new Map(BUILTIN_CATEGORIES),
2861
+ sources: [],
2862
+ skipped: [],
2863
+ failures: []
2864
+ };
2865
+ if (mode === "off") {
2866
+ return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((u) => u.error.source)] };
2867
+ }
2868
+ const settle = (error, kind) => {
2869
+ if (mode === "strict")
2870
+ throw error;
2871
+ if (!set.failures.some((f) => f.source === error.source))
2872
+ set.failures.push({ source: error.source, kind, error });
2873
+ };
2874
+ for (const { error, kind } of unresolved)
2875
+ settle(error, kind);
2876
+ const accepted = acceptClaims(await loadAll(found, settle), set, settle);
2796
2877
  for (const { plugin, kind, spec } of accepted) {
2797
2878
  const rules = Object.entries(plugin.rules).map(([key, rule]) => toRule(plugin.meta.namespace, key, rule));
2798
2879
  set.rules.push(...rules);
@@ -3012,84 +3093,72 @@ var refuseOutsideLinks = async (files, root, cwd) => {
3012
3093
  throw new FixOutsideRootError(display(file, cwd), target, root);
3013
3094
  }
3014
3095
  };
3015
- var run = async (argv, cwd) => {
3016
- let parsed;
3096
+ var parseArgv = (argv) => {
3017
3097
  try {
3018
- parsed = parse2(argv);
3098
+ return parse2(argv);
3019
3099
  } catch (err) {
3020
3100
  throw new FlagError("argv", err instanceof Error ? err.message : String(err));
3021
3101
  }
3022
- const { values, positionals } = parsed;
3023
- if (values.help) {
3024
- console.log(USAGE);
3025
- return 0;
3026
- }
3027
- if (values.version) {
3028
- console.log(package_default.version);
3029
- return 0;
3030
- }
3031
- const listing = values["list-rules"];
3032
- const validating = values["validate-plugins"];
3102
+ };
3103
+ var runFlagsOf = (values) => {
3033
3104
  const canvas = {
3034
3105
  ...values["canvas-light"] !== undefined && { light: values["canvas-light"] },
3035
3106
  ...values["canvas-dark"] !== undefined && { dark: values["canvas-dark"] }
3036
3107
  };
3037
- const flags = {
3108
+ return {
3038
3109
  preset: values.preset,
3039
3110
  profile: values.profile,
3040
3111
  ...Object.keys(canvas).length > 0 && { canvas }
3041
3112
  };
3042
- checkRunFlags(flags, "--");
3113
+ };
3114
+ var checkPluginFlags = (values) => {
3043
3115
  if (values["no-plugins"] && values["lenient-plugins"]) {
3044
3116
  throw new FlagError("--lenient-plugins", "--no-plugins and --lenient-plugins contradict each other");
3045
3117
  }
3046
- if (validating && (values["no-plugins"] || values.fix || listing)) {
3118
+ if (values["validate-plugins"] && (values["no-plugins"] || values.fix || values["list-rules"])) {
3047
3119
  throw new FlagError("--validate-plugins", "--validate-plugins takes no --no-plugins, --fix or --list-rules");
3048
3120
  }
3049
- const enable = categoryWords("--category", values.category);
3050
- const disable = categoryWords("--disable-category", values["disable-category"]);
3051
- if (validating && enable.length + disable.length > 0) {
3052
- throw new FlagError("--validate-plugins", "--validate-plugins checks every plugin, so it takes no category flag");
3053
- }
3054
- if (listing && !validating) {
3055
- const ignored = [
3056
- ...["fix", "json", "preset", "profile", "canvas-light", "canvas-dark"].filter((name) => values[name] !== undefined && values[name] !== false).map((name) => `--${name}`),
3057
- ...positionals.length > 0 ? ["a path"] : []
3058
- ];
3059
- if (ignored.length > 0) {
3060
- throw new FlagError("--list-rules", `--list-rules takes only --config and --no-plugins or --lenient-plugins, not ${ignored.join(", ")}`);
3061
- }
3121
+ };
3122
+ var checkListingFlags = (values, positionals) => {
3123
+ const ignored = [
3124
+ ...["fix", "json", "preset", "profile", "canvas-light", "canvas-dark"].filter((name) => values[name] !== undefined && values[name] !== false).map((name) => `--${name}`),
3125
+ ...positionals.length > 0 ? ["a path"] : []
3126
+ ];
3127
+ if (ignored.length > 0) {
3128
+ throw new FlagError("--list-rules", `--list-rules takes only --config and --no-plugins or --lenient-plugins, not ${ignored.join(", ")}`);
3062
3129
  }
3130
+ };
3131
+ var checkPositionals = (values, positionals) => {
3132
+ const validating = values["validate-plugins"];
3063
3133
  if (validating && positionals.length > 0) {
3064
3134
  throw new FlagError("files", "--validate-plugins runs no check, so it takes no files");
3065
3135
  }
3066
- if (positionals.length === 0 && !listing && !validating)
3136
+ if (positionals.length === 0 && !(values["list-rules"] || validating)) {
3067
3137
  throw new FlagError("files", "no files given");
3068
- const { root, registry, skipped, failures } = await setUp({
3069
- cwd,
3070
- configPath: values.config,
3071
- noPlugins: values["no-plugins"],
3072
- lenient: values["lenient-plugins"],
3073
- collect: validating,
3074
- selection: { enable, disable, prefix: "--" }
3075
- });
3076
- if (validating) {
3077
- const result = validate(registry, failures, Boolean(values.json));
3078
- for (const line of result.stderr)
3079
- console.error(line);
3080
- if (result.stdout)
3081
- console.log(result.stdout);
3082
- return result.code;
3083
3138
  }
3084
- const note = loadedLine(registry.sources, skipped);
3085
- if (note)
3086
- console.error(note);
3087
- for (const line of skippedLines(failures, registry.ignoredSettings))
3088
- console.error(line);
3089
- if (listing) {
3090
- console.log(listRules(registry));
3091
- return 0;
3139
+ };
3140
+ var checkCombinations = (values, positionals) => {
3141
+ checkPluginFlags(values);
3142
+ const enable = categoryWords("--category", values.category);
3143
+ const disable = categoryWords("--disable-category", values["disable-category"]);
3144
+ const validating = values["validate-plugins"];
3145
+ if (validating && enable.length + disable.length > 0) {
3146
+ throw new FlagError("--validate-plugins", "--validate-plugins checks every plugin, so it takes no category flag");
3092
3147
  }
3148
+ if (values["list-rules"] && !validating)
3149
+ checkListingFlags(values, positionals);
3150
+ checkPositionals(values, positionals);
3151
+ return { enable, disable };
3152
+ };
3153
+ var printValidation = (registry, failures, json) => {
3154
+ const result = validate(registry, failures, json);
3155
+ for (const line of result.stderr)
3156
+ console.error(line);
3157
+ if (result.stdout)
3158
+ console.log(result.stdout);
3159
+ return result.code;
3160
+ };
3161
+ var filesToCheck = (positionals, cwd) => {
3093
3162
  const { files, unreadable, symlinks } = collectFiles(positionals.map((p) => resolve5(cwd, p)));
3094
3163
  if (unreadable.length > 0)
3095
3164
  throw new FlagError("files", `cannot read ${unreadable.join(", ")}`);
@@ -3098,39 +3167,83 @@ var run = async (argv, cwd) => {
3098
3167
  }
3099
3168
  if (files.length === 0)
3100
3169
  throw new FlagError("files", "no matching files");
3101
- if (values.fix)
3102
- await refuseOutsideLinks(files, root, cwd);
3170
+ return files;
3171
+ };
3172
+ var reportSetAside = (files, flags, registry, cwd) => {
3103
3173
  const setAside = files.filter((file) => canvasSetAside(file, flags, registry)).map((f) => display(f, cwd));
3104
3174
  if (setAside.length > 0) {
3105
3175
  console.error(`mermaidjs-lint: config canvas set aside for ${plural(setAside.length, "file")} scored under mkdocs-material: ${setAside.join(", ")}`);
3106
3176
  }
3107
- const perFile = await Promise.all(files.map(async (file) => {
3108
- let src = await readFile3(file, "utf8");
3109
- if (values.fix) {
3110
- const fixed = (await fixSourceReport(src, file, flags, registry)).output;
3111
- if (fixed !== src) {
3112
- await writeFile(file, fixed);
3113
- src = fixed;
3114
- }
3177
+ return setAside;
3178
+ };
3179
+ var checkFile = async (file, { flags, registry, fix, cwd }) => {
3180
+ let src = await readFile3(file, "utf8");
3181
+ if (fix) {
3182
+ const fixed = (await fixSourceReport(src, file, flags, registry)).output;
3183
+ if (fixed !== src) {
3184
+ await writeFile(file, fixed);
3185
+ src = fixed;
3115
3186
  }
3116
- const shown = display(file, cwd);
3117
- const found = await checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
3118
- return found.map((f) => ({ ...f, file: shown }));
3119
- }));
3120
- const all = perFile.flat();
3121
- if (values.json) {
3187
+ }
3188
+ const shown = display(file, cwd);
3189
+ const found = await checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
3190
+ return found.map((f) => ({ ...f, file: shown }));
3191
+ };
3192
+ var printReport = (json, { all, files, registry, failures, setAside }) => {
3193
+ if (json) {
3122
3194
  const skippedPlugins = failures.map((f) => ({
3123
3195
  source: f.source,
3124
3196
  kind: f.kind,
3125
3197
  code: f.error.code,
3126
3198
  message: reasonOf(f)
3127
3199
  }));
3128
- console.log(JSON.stringify({ findings: all, files: files.length, plugins: registry.sources, skippedPlugins, canvasSetAside: setAside }, null, 2));
3129
- } else {
3130
- for (const f of all)
3131
- console.log(reportLine(f, f.file));
3132
- console.log(`${all.length} finding(s) in ${files.length} file(s)`);
3200
+ console.log(JSON.stringify({ findings: all, files, plugins: registry.sources, skippedPlugins, canvasSetAside: setAside }, null, 2));
3201
+ return;
3202
+ }
3203
+ for (const f of all)
3204
+ console.log(reportLine(f, f.file));
3205
+ console.log(`${all.length} finding(s) in ${files} file(s)`);
3206
+ };
3207
+ var run = async (argv, cwd) => {
3208
+ const { values, positionals } = parseArgv(argv);
3209
+ if (values.help) {
3210
+ console.log(USAGE);
3211
+ return 0;
3212
+ }
3213
+ if (values.version) {
3214
+ console.log(package_default.version);
3215
+ return 0;
3133
3216
  }
3217
+ const flags = runFlagsOf(values);
3218
+ checkRunFlags(flags, "--");
3219
+ const { enable, disable } = checkCombinations(values, positionals);
3220
+ const { root, registry, skipped, failures } = await setUp({
3221
+ cwd,
3222
+ configPath: values.config,
3223
+ noPlugins: values["no-plugins"],
3224
+ lenient: values["lenient-plugins"],
3225
+ collect: values["validate-plugins"],
3226
+ selection: { enable, disable, prefix: "--" }
3227
+ });
3228
+ if (values["validate-plugins"])
3229
+ return printValidation(registry, failures, Boolean(values.json));
3230
+ const note = loadedLine(registry.sources, skipped);
3231
+ if (note)
3232
+ console.error(note);
3233
+ for (const line of skippedLines(failures, registry.ignoredSettings))
3234
+ console.error(line);
3235
+ if (values["list-rules"]) {
3236
+ console.log(listRules(registry));
3237
+ return 0;
3238
+ }
3239
+ const files = filesToCheck(positionals, cwd);
3240
+ const fix = Boolean(values.fix);
3241
+ if (fix)
3242
+ await refuseOutsideLinks(files, root, cwd);
3243
+ const setAside = reportSetAside(files, flags, registry, cwd);
3244
+ const perFile = await Promise.all(files.map((file) => checkFile(file, { flags, registry, fix, cwd })));
3245
+ const all = perFile.flat();
3246
+ printReport(Boolean(values.json), { all, files: files.length, registry, failures, setAside });
3134
3247
  return all.length > 0 ? 1 : 0;
3135
3248
  };
3136
3249
  var describeError = (err) => err instanceof MermaidLintError ? `error[${err.code}]: ${err.message}` : `error: ${err instanceof Error ? err.message : String(err)}`;