@jpeakai/mermaidjs-lint 0.7.0 → 0.7.2

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/index.js CHANGED
@@ -251,7 +251,7 @@ var parseProperties = (raw, rawStart, line) => {
251
251
  const key = pair.slice(0, colon).trim().toLowerCase();
252
252
  const valueRaw = pair.slice(colon + 1);
253
253
  const value = valueRaw.trim();
254
- if (!key || !value)
254
+ if (!(key && value))
255
255
  continue;
256
256
  properties[key] = value;
257
257
  const lead = valueRaw.length - valueRaw.trimStart().length;
@@ -406,7 +406,7 @@ var parserReason = (err) => {
406
406
  `);
407
407
  if (message.startsWith("No diagram type detected"))
408
408
  return (lines[0] ?? "").trim();
409
- return lines.filter((line, i) => !POINTER.test(line.trim()) && !POINTER.test((lines[i + 1] ?? "").trim())).join(" ").replace(/\s+/g, " ").trim();
409
+ return lines.filter((line, i) => !(POINTER.test(line.trim()) || POINTER.test((lines[i + 1] ?? "").trim()))).join(" ").replace(/\s+/g, " ").trim();
410
410
  };
411
411
  async function langiumParseAny(type, content) {
412
412
  switch (type) {
@@ -437,80 +437,7 @@ async function extractLangiumStats(content, keyword) {
437
437
  if (!mapped)
438
438
  throw new Error(`not a langium keyword: ${keyword}`);
439
439
  const ast = await langiumParseAny(mapped, content);
440
- const nodeIds = [];
441
- const subgraphNames = [];
442
- let edges = 0;
443
- let maxDepth = 0;
444
- let treeDepth = 0;
445
- switch (mapped) {
446
- case "architecture": {
447
- const services = ast.services ?? [];
448
- const groups = ast.groups ?? [];
449
- const edgeArr = ast.edges ?? [];
450
- nodeIds.push(...services.map((s) => s.id ?? ""));
451
- subgraphNames.push(...groups.map((g) => g.title ?? g.id ?? ""));
452
- maxDepth = architectureDepth(groups);
453
- edges = edgeArr.length;
454
- break;
455
- }
456
- case "pie": {
457
- const sections = ast.sections ?? [];
458
- nodeIds.push(...sections.map((s, i) => s.label ?? `slice_${i}`));
459
- break;
460
- }
461
- case "packet": {
462
- const blocks = ast.blocks ?? [];
463
- nodeIds.push(...blocks.map((_, i) => `block_${i}`));
464
- break;
465
- }
466
- case "gitGraph": {
467
- const statements = ast.statements ?? [];
468
- for (const s of statements) {
469
- if (s.$type === "Commit")
470
- nodeIds.push(s.id ?? `commit_${nodeIds.length}`);
471
- else if (s.$type === "Branch")
472
- subgraphNames.push(s.name ?? `branch_${subgraphNames.length}`);
473
- else if (s.$type === "Merge")
474
- edges++;
475
- }
476
- break;
477
- }
478
- case "radar": {
479
- const axes = ast.axes ?? [];
480
- const curves = ast.curves ?? [];
481
- nodeIds.push(...axes.map((a, i) => a.name ?? `axis_${i}`));
482
- nodeIds.push(...curves.map((c, i) => c.name ?? `curve_${i}`));
483
- edges = axes.length * curves.length;
484
- break;
485
- }
486
- case "treeView": {
487
- const nodes = ast.nodes ?? [];
488
- const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
489
- nodeIds.push(...stats.labels);
490
- edges = stats.edges;
491
- treeDepth = stats.depth;
492
- break;
493
- }
494
- case "treemap": {
495
- const rows = ast.TreemapRows ?? [];
496
- const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
497
- nodeIds.push(...stats.labels);
498
- edges = stats.edges;
499
- treeDepth = stats.depth;
500
- break;
501
- }
502
- case "wardley": {
503
- const components = ast.components ?? [];
504
- const anchors = ast.anchors ?? [];
505
- const links = ast.links ?? [];
506
- nodeIds.push(...anchors.map((a, i) => a.name ?? `anchor_${i}`));
507
- nodeIds.push(...components.map((c, i) => c.name ?? `comp_${i}`));
508
- edges = links.length;
509
- break;
510
- }
511
- case "info":
512
- break;
513
- }
440
+ const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = LANGIUM_ADAPTERS[mapped](ast);
514
441
  return {
515
442
  nodes: new Set(nodeIds.filter(Boolean)).size,
516
443
  edges,
@@ -524,6 +451,85 @@ async function extractLangiumStats(content, keyword) {
524
451
  error: null
525
452
  };
526
453
  }
454
+ var LANGIUM_ADAPTERS = {
455
+ architecture: adaptLangiumArchitecture,
456
+ info: adaptLangiumInfo,
457
+ pie: adaptLangiumPie,
458
+ gitGraph: adaptLangiumGitGraph,
459
+ packet: adaptLangiumPacket,
460
+ radar: adaptLangiumRadar,
461
+ treemap: adaptLangiumTreemap,
462
+ treeView: adaptLangiumTreeView,
463
+ wardley: adaptLangiumWardley
464
+ };
465
+ function adaptLangiumArchitecture(ast) {
466
+ const services = ast.services ?? [];
467
+ const groups = ast.groups ?? [];
468
+ const edgeArr = ast.edges ?? [];
469
+ return {
470
+ nodeIds: services.map((s) => s.id ?? ""),
471
+ edges: edgeArr.length,
472
+ subgraphNames: groups.map((g) => g.title ?? g.id ?? ""),
473
+ maxDepth: architectureDepth(groups)
474
+ };
475
+ }
476
+ function adaptLangiumInfo() {
477
+ return { nodeIds: [], edges: 0, subgraphNames: [], maxDepth: 0 };
478
+ }
479
+ function adaptLangiumPie(ast) {
480
+ const sections = ast.sections ?? [];
481
+ return { nodeIds: sections.map((s, i) => s.label ?? `slice_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
482
+ }
483
+ function adaptLangiumPacket(ast) {
484
+ const blocks = ast.blocks ?? [];
485
+ return { nodeIds: blocks.map((_, i) => `block_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
486
+ }
487
+ function adaptLangiumGitGraph(ast) {
488
+ const statements = ast.statements ?? [];
489
+ const nodeIds = [];
490
+ const subgraphNames = [];
491
+ let edges = 0;
492
+ for (const s of statements) {
493
+ if (s.$type === "Commit")
494
+ nodeIds.push(s.id ?? `commit_${nodeIds.length}`);
495
+ else if (s.$type === "Branch")
496
+ subgraphNames.push(s.name ?? `branch_${subgraphNames.length}`);
497
+ else if (s.$type === "Merge")
498
+ edges++;
499
+ }
500
+ return { nodeIds, edges, subgraphNames, maxDepth: 0 };
501
+ }
502
+ function adaptLangiumRadar(ast) {
503
+ const axes = ast.axes ?? [];
504
+ const curves = ast.curves ?? [];
505
+ return {
506
+ nodeIds: [...axes.map((a, i) => a.name ?? `axis_${i}`), ...curves.map((c, i) => c.name ?? `curve_${i}`)],
507
+ edges: axes.length * curves.length,
508
+ subgraphNames: [],
509
+ maxDepth: 0
510
+ };
511
+ }
512
+ function adaptLangiumTreeView(ast) {
513
+ const nodes = ast.nodes ?? [];
514
+ const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
515
+ return { nodeIds: stats.labels, edges: stats.edges, subgraphNames: [], maxDepth: 0, treeDepth: stats.depth };
516
+ }
517
+ function adaptLangiumTreemap(ast) {
518
+ const rows = ast.TreemapRows ?? [];
519
+ const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
520
+ return { nodeIds: stats.labels, edges: stats.edges, subgraphNames: [], maxDepth: 0, treeDepth: stats.depth };
521
+ }
522
+ function adaptLangiumWardley(ast) {
523
+ const components = ast.components ?? [];
524
+ const anchors = ast.anchors ?? [];
525
+ const links = ast.links ?? [];
526
+ return {
527
+ nodeIds: [...anchors.map((a, i) => a.name ?? `anchor_${i}`), ...components.map((c, i) => c.name ?? `comp_${i}`)],
528
+ edges: links.length,
529
+ subgraphNames: [],
530
+ maxDepth: 0
531
+ };
532
+ }
527
533
  function architectureDepth(groups) {
528
534
  const parentOf = new Map;
529
535
  for (const g of groups)
@@ -947,7 +953,7 @@ var buildDocModel = (src, kind = "markdown") => {
947
953
  collectMermaid(tree, fences);
948
954
  const diagrams = fences.flatMap((node, index) => {
949
955
  const { start, end } = node.position ?? {};
950
- if (!start || !end || start.offset === undefined || end.offset === undefined)
956
+ if (!(start && end) || start.offset === undefined || end.offset === undefined)
951
957
  return [];
952
958
  return [
953
959
  makeDiagram(src, lineStarts, {
@@ -1014,72 +1020,71 @@ var scorePair = (kind, d, fg, bg) => {
1014
1020
  };
1015
1021
  };
1016
1022
  var message = (err) => err instanceof Error ? err.message : String(err);
1017
- var scoreEdgeLabel = (d, canvas, pairs, skipped) => {
1023
+ var skip = (out, d, reason, blocking) => {
1024
+ out.skipped.push({ selector: d.selector, line: d.line, reason, ...blocking && { blocking } });
1025
+ };
1026
+ var scoreEdgeLabel = (d, canvas, out) => {
1018
1027
  const { color } = d.properties;
1019
1028
  if (!color)
1020
1029
  return;
1021
1030
  try {
1022
- pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t, canvas))));
1031
+ out.pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, { fg: color, bg: edgeLabelBackground(t, canvas) })));
1023
1032
  } catch (err) {
1024
- skipped.push({
1025
- selector: d.selector,
1026
- line: d.line,
1027
- reason: `edge label text unparseable: ${message(err)}`,
1028
- blocking: true
1029
- });
1033
+ skip(out, d, `edge label text unparseable: ${message(err)}`, true);
1034
+ }
1035
+ };
1036
+ var githubBoxes = (fill, alpha, canvas) => alpha < 1 ? THEMES.map((t) => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
1037
+ var scoreGithubPair = (out, d, kind, { fg, bg, theme }) => {
1038
+ try {
1039
+ out.pairs.push({ ...scorePair(kind, d, fg, bg), ...theme && { theme } });
1040
+ } catch (err) {
1041
+ skip(out, d, `${kind} pair unparseable: ${message(err)}`, true);
1042
+ }
1043
+ };
1044
+ var fillAlpha = (out, d, fill) => {
1045
+ try {
1046
+ return alphaOf(fill);
1047
+ } catch (err) {
1048
+ skip(out, d, `fill unparseable: ${message(err)}`, true);
1049
+ return null;
1050
+ }
1051
+ };
1052
+ var scoreGithubStyle = (d, canvas, out) => {
1053
+ const { fill, color, stroke } = d.properties;
1054
+ if (!fill) {
1055
+ const reason = color || stroke ? "no fill declared, so text and border contrast have no anchor" : "no color properties";
1056
+ skip(out, d, reason);
1057
+ return;
1058
+ }
1059
+ const alpha = fillAlpha(out, d, fill);
1060
+ if (alpha === null)
1061
+ return;
1062
+ if (alpha === 0) {
1063
+ skip(out, d, "fill is fully transparent, so there is no box to anchor contrast");
1064
+ return;
1065
+ }
1066
+ const boxes = githubBoxes(fill, alpha, canvas);
1067
+ if (!color) {
1068
+ 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);
1069
+ }
1070
+ for (const [bg, theme] of boxes) {
1071
+ if (color)
1072
+ scoreGithubPair(out, d, "text", { fg: color, bg, theme });
1073
+ if (stroke)
1074
+ scoreGithubPair(out, d, "border", { fg: stroke, bg, theme });
1030
1075
  }
1031
1076
  };
1032
1077
  var scoreGithub = (directives, canvas = GITHUB_CANVAS) => {
1033
- const pairs = [];
1034
- const skipped = [];
1078
+ const out = { pairs: [], skipped: [] };
1035
1079
  for (const d of directives) {
1036
1080
  if (d.kind === "linkStyle")
1037
- scoreEdgeLabel(d, canvas, pairs, skipped);
1038
- if (d.kind !== "classDef" && d.kind !== "style")
1039
- continue;
1040
- const { fill, color, stroke } = d.properties;
1041
- const note = (reason, blocking) => {
1042
- skipped.push({ selector: d.selector, line: d.line, reason, ...blocking && { blocking } });
1043
- };
1044
- if (!fill) {
1045
- note(color || stroke ? "no fill declared, so text and border contrast have no anchor" : "no color properties");
1046
- continue;
1047
- }
1048
- let alpha;
1049
- try {
1050
- alpha = alphaOf(fill);
1051
- } catch (err) {
1052
- note(`fill unparseable: ${message(err)}`, true);
1053
- continue;
1054
- }
1055
- if (alpha === 0) {
1056
- note("fill is fully transparent, so there is no box to anchor contrast");
1057
- continue;
1058
- }
1059
- const boxes = alpha < 1 ? THEMES.map((t) => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
1060
- if (!color) {
1061
- note("no color: declared beside fill:, and the github profile requires one " + "(if the host forces label text, use the mkdocs-material profile)", true);
1062
- }
1063
- for (const [box, theme] of boxes) {
1064
- if (color) {
1065
- try {
1066
- pairs.push({ ...scorePair("text", d, color, box), ...theme && { theme } });
1067
- } catch (err) {
1068
- note(`text pair unparseable: ${message(err)}`, true);
1069
- }
1070
- }
1071
- if (stroke) {
1072
- try {
1073
- pairs.push({ ...scorePair("border", d, stroke, box), ...theme && { theme } });
1074
- } catch (err) {
1075
- note(`border pair unparseable: ${message(err)}`, true);
1076
- }
1077
- }
1078
- }
1081
+ scoreEdgeLabel(d, canvas, out);
1082
+ if (d.kind === "classDef" || d.kind === "style")
1083
+ scoreGithubStyle(d, canvas, out);
1079
1084
  }
1080
- return { pairs, skipped };
1085
+ return out;
1081
1086
  };
1082
- var scoreHostPair = (kind, d, theme, fgRaw, box) => {
1087
+ var scoreHostPair = (kind, d, theme, { fg: fgRaw, bg: box }) => {
1083
1088
  const fg = compositeOver(fgRaw, box);
1084
1089
  const assessment = wcagAssess(fg, box);
1085
1090
  return {
@@ -1095,99 +1100,83 @@ var scoreHostPair = (kind, d, theme, fgRaw, box) => {
1095
1100
  advisory: kind === "border"
1096
1101
  };
1097
1102
  };
1098
- var scoreMkdocs = (directives) => {
1099
- const pairs = [];
1100
- const skipped = [];
1101
- for (const d of directives) {
1102
- if (d.kind === "linkStyle" && d.properties.color) {
1103
- skipped.push({
1104
- selector: d.selector,
1105
- line: d.line,
1106
- reason: "color: ignored, because the host theme forces edge label text"
1107
- });
1108
- }
1109
- if (d.kind !== "classDef" && d.kind !== "style")
1110
- continue;
1111
- const { fill, stroke, color } = d.properties;
1112
- const note = (reason) => {
1113
- skipped.push({ selector: d.selector, line: d.line, reason });
1114
- };
1115
- if (!fill) {
1116
- note("no fill declared, and the host theme controls the text");
1117
- continue;
1103
+ var scoreMkdocsStyle = (d, out) => {
1104
+ const { fill, stroke, color } = d.properties;
1105
+ if (!fill) {
1106
+ skip(out, d, "no fill declared, and the host theme controls the text");
1107
+ return;
1108
+ }
1109
+ if (color)
1110
+ skip(out, d, "color: ignored, because the host theme forces label text");
1111
+ for (const theme of THEMES) {
1112
+ let box;
1113
+ try {
1114
+ box = compositeOver(fill, MKDOCS_MATERIAL[theme].bg);
1115
+ } catch (err) {
1116
+ skip(out, d, `fill unparseable: ${message(err)}`, true);
1117
+ break;
1118
1118
  }
1119
- if (color)
1120
- note("color: ignored, because the host theme forces label text");
1121
- for (const theme of THEMES) {
1122
- let box;
1119
+ out.pairs.push(scoreHostPair("text", d, theme, { fg: MKDOCS_MATERIAL[theme].text, bg: box }));
1120
+ if (stroke) {
1123
1121
  try {
1124
- box = compositeOver(fill, MKDOCS_MATERIAL[theme].bg);
1122
+ out.pairs.push(scoreHostPair("border", d, theme, { fg: stroke, bg: box }));
1125
1123
  } catch (err) {
1126
- skipped.push({
1127
- selector: d.selector,
1128
- line: d.line,
1129
- reason: `fill unparseable: ${message(err)}`,
1130
- blocking: true
1131
- });
1132
- break;
1133
- }
1134
- pairs.push(scoreHostPair("text", d, theme, MKDOCS_MATERIAL[theme].text, box));
1135
- if (stroke) {
1136
- try {
1137
- pairs.push(scoreHostPair("border", d, theme, stroke, box));
1138
- } catch (err) {
1139
- skipped.push({
1140
- selector: d.selector,
1141
- line: d.line,
1142
- reason: `border pair unparseable: ${message(err)}`,
1143
- blocking: true
1144
- });
1145
- }
1124
+ skip(out, d, `border pair unparseable: ${message(err)}`, true);
1146
1125
  }
1147
1126
  }
1148
1127
  }
1149
- return { pairs, skipped };
1128
+ };
1129
+ var scoreMkdocs = (directives) => {
1130
+ const out = { pairs: [], skipped: [] };
1131
+ for (const d of directives) {
1132
+ if (d.kind === "linkStyle" && d.properties.color) {
1133
+ skip(out, d, "color: ignored, because the host theme forces edge label text");
1134
+ }
1135
+ if (d.kind === "classDef" || d.kind === "style")
1136
+ scoreMkdocsStyle(d, out);
1137
+ }
1138
+ return out;
1139
+ };
1140
+ var erRows = (theme, fill) => {
1141
+ const anchor = MERMAID_ER[theme];
1142
+ return [
1143
+ ["odd", anchor.odd],
1144
+ ["even", fill ? compositeOver(fill, anchor.even) : anchor.even]
1145
+ ];
1146
+ };
1147
+ var erPairs = (d) => {
1148
+ const { fill, color, stroke } = d.properties;
1149
+ const scored = [];
1150
+ const score = (kind, theme, row, colours) => {
1151
+ scored.push({ ...scoreHostPair(kind, d, theme, colours), row, advisory: kind === "border" });
1152
+ };
1153
+ for (const theme of THEMES) {
1154
+ for (const [row, bg] of erRows(theme, fill)) {
1155
+ if (color || row === "even" && fill)
1156
+ score("text", theme, row, { fg: color ?? MERMAID_ER[theme].label, bg });
1157
+ if (stroke)
1158
+ score("border", theme, row, { fg: stroke, bg });
1159
+ }
1160
+ }
1161
+ return scored;
1150
1162
  };
1151
1163
  var scoreEr = (directives) => {
1152
- const pairs = [];
1153
- const skipped = [];
1164
+ const out = { pairs: [], skipped: [] };
1154
1165
  for (const d of directives) {
1155
1166
  if (d.kind !== "classDef" && d.kind !== "style")
1156
1167
  continue;
1157
1168
  const { fill, color, stroke } = d.properties;
1158
- if (!fill && !color && !stroke) {
1159
- skipped.push({ selector: d.selector, line: d.line, reason: "no color properties" });
1169
+ if (!(fill || color || stroke)) {
1170
+ skip(out, d, "no color properties");
1160
1171
  continue;
1161
1172
  }
1162
- const scored = [];
1163
- const score = (kind, theme, row, fg, bg) => {
1164
- scored.push({ ...scoreHostPair(kind, d, theme, fg, bg), row, advisory: kind === "border" });
1165
- };
1166
1173
  try {
1167
- for (const theme of THEMES) {
1168
- const anchor = MERMAID_ER[theme];
1169
- const rows = [
1170
- ["odd", anchor.odd],
1171
- ["even", fill ? compositeOver(fill, anchor.even) : anchor.even]
1172
- ];
1173
- for (const [row, bg] of rows) {
1174
- if (color || row === "even" && fill)
1175
- score("text", theme, row, color ?? anchor.label, bg);
1176
- if (stroke)
1177
- score("border", theme, row, stroke, bg);
1178
- }
1179
- }
1180
- pairs.push(...scored);
1174
+ out.pairs.push(...erPairs(d));
1181
1175
  } catch (err) {
1182
- skipped.push({
1183
- selector: d.selector,
1184
- line: d.line,
1185
- reason: `erDiagram pair unparseable: ${message(err)}`,
1186
- blocking: true
1187
- });
1176
+ skip(out, d, `erDiagram pair unparseable: ${message(err)}`, true);
1188
1177
  }
1189
1178
  }
1190
- return { pairs, skipped };
1179
+ return out;
1191
1180
  };
1192
1181
  var scoreForProfile = (directives, profile, keyword, canvas = GITHUB_CANVAS) => {
1193
1182
  if (profile === "mkdocs-material")
@@ -1229,9 +1218,7 @@ var themeVariable = (selector, line) => ({
1229
1218
  spans: {},
1230
1219
  line
1231
1220
  });
1232
- var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1233
- const pairs = [];
1234
- const skipped = [];
1221
+ var sequenceLines = (source) => {
1235
1222
  const lines = source.split(`
1236
1223
  `);
1237
1224
  const keywordLine = lines.findIndex((l) => /^\s*sequenceDiagram\b/.test(l)) + 1;
@@ -1239,94 +1226,125 @@ var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1239
1226
  const found = lines.findLastIndex((l) => new RegExp(`\\b${key}["']?\\s*:`).test(l));
1240
1227
  return found < 0 ? keywordLine : found + 1;
1241
1228
  };
1242
- const rectLines = lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []);
1243
- const mermaid = await getMermaidCore();
1244
- let config;
1245
- let db;
1229
+ return { lineOf, rectLines: lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []) };
1230
+ };
1231
+ var parseSequence = async (mermaid, source) => {
1246
1232
  try {
1247
- config = (await mermaid.parse(source)).config;
1248
- db = (await mermaid.mermaidAPI.getDiagramFromText(source)).db;
1233
+ const { config } = await mermaid.parse(source);
1234
+ const { db } = await mermaid.mermaidAPI.getDiagramFromText(source);
1235
+ return { config, db };
1249
1236
  } catch {
1250
- return { pairs, skipped };
1237
+ return null;
1238
+ }
1239
+ };
1240
+ var unmodelled = (config) => {
1241
+ if (config.themeCSS !== undefined) {
1242
+ return { selector: "themeCSS", reason: "themeCSS restyles the diagram with CSS the audit cannot model" };
1251
1243
  }
1252
- const block = (selector, reason) => {
1253
- skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
1254
- return { pairs, skipped };
1255
- };
1256
- if (config.themeCSS !== undefined)
1257
- return block("themeCSS", "themeCSS restyles the diagram with CSS the audit cannot model");
1258
1244
  const pinned = config.theme;
1259
1245
  if (pinned !== undefined && !SEQUENCE_THEMES.includes(String(pinned))) {
1260
- return block("theme", `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`);
1246
+ return {
1247
+ selector: "theme",
1248
+ reason: `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`
1249
+ };
1261
1250
  }
1251
+ return null;
1252
+ };
1253
+ var sequenceThemes = (pinned, host) => {
1254
+ const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
1255
+ return { light: theme("default"), dark: theme(host ? "default" : "dark") };
1256
+ };
1257
+ var sequencePair = ({ theme: t, page, vars, host, lineOf, out }) => (kind, fgKey, bg, { suffix = "", at } = {}) => {
1258
+ const color = (key) => {
1259
+ const value = vars[key];
1260
+ if (typeof value !== "string")
1261
+ throw new Error(`${key} is not a resolved colour`);
1262
+ return value;
1263
+ };
1264
+ const selector = fgKey + suffix;
1265
+ const line = at ?? lineOf(fgKey);
1266
+ try {
1267
+ const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
1268
+ out.pairs.push({
1269
+ ...scoreHostPair(kind, themeVariable(selector, line), t, { fg: color(fgKey), bg: box }),
1270
+ advisory: host
1271
+ });
1272
+ } catch (err) {
1273
+ out.skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
1274
+ }
1275
+ };
1276
+ var scoreHostSequence = (pair, page, features) => {
1277
+ const onPage = { colour: page };
1278
+ pair("border", "actorLineColor", onPage);
1279
+ pair("border", "signalColor", onPage, { suffix: " (arrowhead)" });
1280
+ if (features.activation)
1281
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1282
+ };
1283
+ var scoreAuthorSequence = (pair, page, features, rectLines) => {
1284
+ const onPage = { colour: page };
1285
+ pair("text", "actorTextColor", { key: "actorBkg" });
1286
+ pair("border", "actorBorder", { key: "actorBkg" });
1287
+ if (features.humanActor) {
1288
+ pair("text", "actorTextColor", onPage, { suffix: " (human actor label)" });
1289
+ pair("border", "actorBorder", onPage, { suffix: " (human actor glyph)" });
1290
+ }
1291
+ pair("border", "actorLineColor", onPage);
1292
+ pair("text", "signalTextColor", onPage);
1293
+ pair("border", "signalColor", onPage);
1294
+ if (features.note)
1295
+ pair("text", "noteTextColor", { key: "noteBkgColor" });
1296
+ if (features.frame) {
1297
+ pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
1298
+ pair("text", "loopTextColor", onPage);
1299
+ pair("border", "labelBoxBorderColor", onPage, { suffix: " (loop frame)" });
1300
+ }
1301
+ if (features.activation)
1302
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1303
+ if (features.numbered)
1304
+ pair("text", "sequenceNumberColor", { key: "signalColor" });
1305
+ for (const [i, rect] of features.rects.entries()) {
1306
+ const fill = { colour: rect };
1307
+ const where = { suffix: " (rect)", at: rectLines[i] };
1308
+ pair("text", "signalTextColor", fill, where);
1309
+ pair("border", "signalColor", fill, where);
1310
+ if (features.frame)
1311
+ pair("text", "loopTextColor", fill, where);
1312
+ }
1313
+ };
1314
+ var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1315
+ const out = { pairs: [], skipped: [] };
1316
+ const { lineOf, rectLines } = sequenceLines(source);
1317
+ const mermaid = await getMermaidCore();
1318
+ const parsed = await parseSequence(mermaid, source);
1319
+ if (!parsed)
1320
+ return out;
1321
+ const { config, db } = parsed;
1322
+ const block = (selector, reason) => {
1323
+ out.skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
1324
+ return out;
1325
+ };
1326
+ const problem = unmodelled(config);
1327
+ if (problem)
1328
+ return block(problem.selector, problem.reason);
1262
1329
  const features = sequenceFeatures(db);
1263
1330
  const host = profile === "mkdocs-material";
1264
- const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
1265
1331
  let variables;
1266
1332
  try {
1267
- variables = resolveVariables(mermaid, config, { light: theme("default"), dark: theme(host ? "default" : "dark") });
1333
+ variables = resolveVariables(mermaid, config, sequenceThemes(config.theme, host));
1268
1334
  } catch (err) {
1269
1335
  return block("themeVariables", `Mermaid could not resolve the theme: ${message(err)}`);
1270
1336
  }
1271
1337
  for (const t of THEMES) {
1272
1338
  const page = host ? MKDOCS_MATERIAL[t].bg : canvas[t];
1273
- const vars = variables[t];
1274
- const color = (key) => {
1275
- const value = vars[key];
1276
- if (typeof value !== "string")
1277
- throw new Error(`${key} is not a resolved colour`);
1278
- return value;
1279
- };
1280
- const pair = (kind, fgKey, bg, suffix = "", at) => {
1281
- const selector = fgKey + suffix;
1282
- const line = at ?? lineOf(fgKey);
1283
- try {
1284
- const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
1285
- pairs.push({ ...scoreHostPair(kind, themeVariable(selector, line), t, color(fgKey), box), advisory: host });
1286
- } catch (err) {
1287
- skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
1288
- }
1289
- };
1290
- const onPage = { colour: page };
1291
- if (host) {
1292
- pair("border", "actorLineColor", onPage);
1293
- pair("border", "signalColor", onPage, " (arrowhead)");
1294
- if (features.activation)
1295
- pair("border", "activationBorderColor", { key: "activationBkgColor" });
1296
- continue;
1297
- }
1298
- pair("text", "actorTextColor", { key: "actorBkg" });
1299
- pair("border", "actorBorder", { key: "actorBkg" });
1300
- if (features.humanActor) {
1301
- pair("text", "actorTextColor", onPage, " (human actor label)");
1302
- pair("border", "actorBorder", onPage, " (human actor glyph)");
1303
- }
1304
- pair("border", "actorLineColor", onPage);
1305
- pair("text", "signalTextColor", onPage);
1306
- pair("border", "signalColor", onPage);
1307
- if (features.note)
1308
- pair("text", "noteTextColor", { key: "noteBkgColor" });
1309
- if (features.frame) {
1310
- pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
1311
- pair("text", "loopTextColor", onPage);
1312
- pair("border", "labelBoxBorderColor", onPage, " (loop frame)");
1313
- }
1314
- if (features.activation)
1315
- pair("border", "activationBorderColor", { key: "activationBkgColor" });
1316
- if (features.numbered)
1317
- pair("text", "sequenceNumberColor", { key: "signalColor" });
1318
- for (const [i, rect] of features.rects.entries()) {
1319
- const fill = { colour: rect };
1320
- const at = rectLines[i];
1321
- pair("text", "signalTextColor", fill, " (rect)", at);
1322
- pair("border", "signalColor", fill, " (rect)", at);
1323
- if (features.frame)
1324
- pair("text", "loopTextColor", fill, " (rect)", at);
1325
- }
1339
+ const pair = sequencePair({ theme: t, page, vars: variables[t], host, lineOf, out });
1340
+ if (host)
1341
+ scoreHostSequence(pair, page, features);
1342
+ else
1343
+ scoreAuthorSequence(pair, page, features, rectLines);
1326
1344
  }
1327
- return { pairs, skipped };
1345
+ return out;
1328
1346
  };
1329
- var scoreDiagram = (source, directives, profile, keyword, canvas = GITHUB_CANVAS) => keyword === "sequenceDiagram" ? scoreSequence(source, profile, canvas) : Promise.resolve(scoreForProfile(directives, profile, keyword, canvas));
1347
+ var scoreDiagram = ({ source, directives, keyword }, profile, canvas = GITHUB_CANVAS) => keyword === "sequenceDiagram" ? scoreSequence(source, profile, canvas) : Promise.resolve(scoreForProfile(directives, profile, keyword, canvas));
1330
1348
  var detectProfile = (filePath) => {
1331
1349
  let dir = dirname(resolve(filePath));
1332
1350
  for (let i = 0;i < 20; i++) {
@@ -1418,7 +1436,7 @@ var canonicalPreset = (name) => {
1418
1436
  var configFromPreset = (name) => {
1419
1437
  const canonical = canonicalPreset(name);
1420
1438
  const preset = canonical ? PRESETS[canonical] : undefined;
1421
- if (!canonical || !preset) {
1439
+ if (!(canonical && preset)) {
1422
1440
  throw new Error(`Unknown preset '${name}'. Valid: ${PRESET_NAMES.join(", ")}`);
1423
1441
  }
1424
1442
  return {
@@ -1497,7 +1515,7 @@ var measure = async (diagram, config) => {
1497
1515
  return null;
1498
1516
  return { stats, metrics: calculateComplexity(stats, config) };
1499
1517
  };
1500
- var finding = (ctx, diagram, rule, message2, remediation) => ({
1518
+ var finding = (ctx, diagram, rule, { message: message2, remediation }) => ({
1501
1519
  file: ctx.file,
1502
1520
  line: diagram.startLine,
1503
1521
  endLine: diagram.endLine,
@@ -1548,7 +1566,7 @@ var contrastOf = (diagram, settings) => {
1548
1566
  }
1549
1567
  let result = bySettings.get(key);
1550
1568
  if (!result) {
1551
- result = scoreDiagram(diagram.source, diagram.directives, profile, diagram.keyword, canvas);
1569
+ result = scoreDiagram(diagram, profile, canvas);
1552
1570
  bySettings.set(key, result);
1553
1571
  }
1554
1572
  return result;
@@ -1570,7 +1588,7 @@ var complexityRule = (spec) => ({
1570
1588
  continue;
1571
1589
  const hit = spec.hit(measured, config);
1572
1590
  if (hit)
1573
- found.push(finding(ctx, diagram, spec.id, hit.message, hit.remediation));
1591
+ found.push(finding(ctx, diagram, spec.id, hit));
1574
1592
  }
1575
1593
  return found;
1576
1594
  }
@@ -1715,6 +1733,30 @@ var contrastDirectiveUnauditable = {
1715
1733
  var AA_NORMAL2 = 4.5;
1716
1734
  var CANDIDATES = ["#000000", "#ffffff"];
1717
1735
  var ratioOn = (fg, bg) => wcagRatio(compositeOver(fg, bg), bg);
1736
+ var remediationOf = (profile, kind) => {
1737
+ if (profile === "mkdocs-material") {
1738
+ return "The host theme forces the label text, so change the fill until the host text reads on it in both themes.";
1739
+ }
1740
+ if (kind === "themeVariables") {
1741
+ 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.";
1742
+ }
1743
+ if (kind === "linkStyle") {
1744
+ 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.";
1745
+ }
1746
+ return "Darken or lighten the fill, or change color:, until the ratio reaches 4.5:1 in every theme.";
1747
+ };
1748
+ var textFinding = (ctx, diagram, p, profile) => {
1749
+ const where = [p.theme, p.row].filter(Boolean).join("/");
1750
+ const line = fileLine(diagram, p.line);
1751
+ return {
1752
+ file: ctx.file,
1753
+ line,
1754
+ endLine: line,
1755
+ rule: RULE.TEXT,
1756
+ message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1757
+ remediation: remediationOf(profile, p.directive.kind)
1758
+ };
1759
+ };
1718
1760
  var contrastTextBelowAa = {
1719
1761
  id: RULE.TEXT,
1720
1762
  category: "contrast",
@@ -1725,18 +1767,8 @@ var contrastTextBelowAa = {
1725
1767
  const found = [];
1726
1768
  for (const diagram of ctx.doc.diagrams) {
1727
1769
  for (const p of (await contrastOf(diagram, settings)).pairs) {
1728
- if (p.kind !== "text" || p.passes)
1729
- continue;
1730
- const where = [p.theme, p.row].filter(Boolean).join("/");
1731
- const line = fileLine(diagram, p.line);
1732
- found.push({
1733
- file: ctx.file,
1734
- line,
1735
- endLine: line,
1736
- rule: RULE.TEXT,
1737
- message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1738
- 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."
1739
- });
1770
+ if (p.kind === "text" && !p.passes)
1771
+ found.push(textFinding(ctx, diagram, p, settings.profile));
1740
1772
  }
1741
1773
  }
1742
1774
  return found;
@@ -1795,7 +1827,7 @@ var parseDiagramUnparseable = {
1795
1827
  if (!parseFailed(diagram.source, stats))
1796
1828
  continue;
1797
1829
  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.";
1798
- found.push(finding(ctx, diagram, RULE.UNPARSEABLE, messageOf(stats), remediation));
1830
+ found.push(finding(ctx, diagram, RULE.UNPARSEABLE, { message: messageOf(stats), remediation }));
1799
1831
  }
1800
1832
  return found;
1801
1833
  }
@@ -1807,7 +1839,10 @@ var parseFenceUnterminated = {
1807
1839
  id: RULE.FENCE_UNTERMINATED,
1808
1840
  category: "parse",
1809
1841
  summary: "a mermaid fence that is never closed",
1810
- 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.")),
1842
+ check: (ctx) => ctx.doc.diagrams.filter((d) => !d.terminated).map((d) => finding(ctx, d, RULE.FENCE_UNTERMINATED, {
1843
+ message: `mermaid fence opened at line ${d.startLine} is never closed`,
1844
+ 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."
1845
+ })),
1811
1846
  fix: (ctx) => {
1812
1847
  const edits = [];
1813
1848
  const { src } = ctx.doc;
@@ -1904,20 +1939,20 @@ var checkCanvas = (where, rule, name, value) => {
1904
1939
  }
1905
1940
  }
1906
1941
  };
1907
- var checkOptions = (where, rule, options) => {
1908
- const accepted = Object.keys(rule.options ?? {});
1909
- for (const [name, value] of Object.entries(options)) {
1910
- const want = rule.options?.[name];
1911
- if (!want) {
1912
- const list = accepted.length > 0 ? `it accepts ${accepted.join(", ")}` : "it takes no options";
1913
- throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
1914
- }
1915
- if (typeOf(value) !== want) {
1916
- throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be ${describeType(want)}`);
1917
- }
1918
- if (want === "canvas")
1919
- checkCanvas(where, rule, name, value);
1942
+ var checkOption = (where, rule, name, value) => {
1943
+ const want = rule.options?.[name];
1944
+ if (!want) {
1945
+ const accepted = Object.keys(rule.options ?? {});
1946
+ const list = accepted.length > 0 ? `it accepts ${accepted.join(", ")}` : "it takes no options";
1947
+ throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
1920
1948
  }
1949
+ if (typeOf(value) !== want) {
1950
+ throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be ${describeType(want)}`);
1951
+ }
1952
+ if (want === "canvas")
1953
+ checkCanvas(where, rule, name, value);
1954
+ };
1955
+ var checkSharedOptions = (where, rule, options) => {
1921
1956
  if (options.profile === "mkdocs-material" && options.canvas !== undefined && rule.options?.canvas === "canvas") {
1922
1957
  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`);
1923
1958
  }
@@ -1928,10 +1963,12 @@ var checkOptions = (where, rule, options) => {
1928
1963
  throw new RuleOptionError(where, rule.id, "profile", `option "profile" of "${rule.id}" must be one of ${PROFILES.join(", ")}`);
1929
1964
  }
1930
1965
  };
1931
- var buildRegistry = (input) => {
1932
- const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
1933
- const selection = input.selection ?? {};
1934
- checkSelection(selection, categories);
1966
+ var checkOptions = (where, rule, options) => {
1967
+ for (const [name, value] of Object.entries(options))
1968
+ checkOption(where, rule, name, value);
1969
+ checkSharedOptions(where, rule, options);
1970
+ };
1971
+ var applyConfig = (rules, config, ignoreUnknownPluginIds) => {
1935
1972
  const off = new Set;
1936
1973
  const options = new Map;
1937
1974
  const ignored = [];
@@ -1950,6 +1987,13 @@ var buildRegistry = (input) => {
1950
1987
  if (setting.severity === "off")
1951
1988
  off.add(id);
1952
1989
  }
1990
+ return { off, options, ignored };
1991
+ };
1992
+ var buildRegistry = (input) => {
1993
+ const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
1994
+ const selection = input.selection ?? {};
1995
+ checkSelection(selection, categories);
1996
+ const { off, options, ignored } = applyConfig(rules, config, ignoreUnknownPluginIds);
1953
1997
  for (const rule of rules) {
1954
1998
  if (selection.enable?.includes(rule.category))
1955
1999
  off.delete(rule.id);
@@ -2002,6 +2046,17 @@ var checkRunFlags = (flags, prefix = "") => {
2002
2046
  var canvasOptions = (rule) => Object.entries(rule.options ?? {}).filter(([, type]) => type === "canvas").map(([name]) => name);
2003
2047
  var hasFlagCanvas = (flags) => THEMES.some((side) => flags.canvas?.[side] !== undefined);
2004
2048
  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";
2049
+ var mergeCanvas = (current, canvas) => {
2050
+ const merged = {
2051
+ ...typeof current === "object" && current !== null ? current : {}
2052
+ };
2053
+ for (const side of THEMES) {
2054
+ const value = canvas[side];
2055
+ if (value !== undefined)
2056
+ merged[side] = value;
2057
+ }
2058
+ return merged;
2059
+ };
2005
2060
  var optionsFor = (rule, configured, flags, file) => {
2006
2061
  const options = { ...configured };
2007
2062
  for (const name of ["preset", "profile"]) {
@@ -2013,18 +2068,8 @@ var optionsFor = (rule, configured, flags, file) => {
2013
2068
  options.preset = canonicalPreset(options.preset) ?? options.preset;
2014
2069
  const canvas = flags.canvas;
2015
2070
  if (canvas && hasFlagCanvas(flags)) {
2016
- for (const name of canvasOptions(rule)) {
2017
- const current = options[name];
2018
- const merged = {
2019
- ...typeof current === "object" && current !== null ? current : {}
2020
- };
2021
- for (const side of THEMES) {
2022
- const value = canvas[side];
2023
- if (value !== undefined)
2024
- merged[side] = value;
2025
- }
2026
- options[name] = merged;
2027
- }
2071
+ for (const name of canvasOptions(rule))
2072
+ options[name] = mergeCanvas(options[name], canvas);
2028
2073
  }
2029
2074
  if (file !== undefined && setsAside(rule, options, flags, file)) {
2030
2075
  for (const name of canvasOptions(rule))
@@ -2050,7 +2095,7 @@ import { parseArgs } from "node:util";
2050
2095
  // package.json
2051
2096
  var package_default = {
2052
2097
  name: "@jpeakai/mermaidjs-lint",
2053
- version: "0.7.0",
2098
+ version: "0.7.2",
2054
2099
  type: "module",
2055
2100
  description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
2056
2101
  license: "MIT",
@@ -2126,6 +2171,7 @@ import { extname, join as join2, resolve as resolve2 } from "node:path";
2126
2171
  var DIAGRAM_FILE_EXTS = new Set([...DIAGRAM_EXTS, ".md", ".markdown"]);
2127
2172
  var SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tmp", "__pycache__"]);
2128
2173
  var skipDir = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
2174
+ var isDiagramFile = (name) => DIAGRAM_FILE_EXTS.has(extname(name).toLowerCase());
2129
2175
  var linksToDir = (path) => {
2130
2176
  try {
2131
2177
  return statSync(path).isDirectory();
@@ -2138,6 +2184,24 @@ var collectFiles = (paths) => {
2138
2184
  const unreadable = [];
2139
2185
  const symlinks = [];
2140
2186
  const seen = new Set;
2187
+ const add = (abs) => {
2188
+ if (seen.has(abs))
2189
+ return;
2190
+ seen.add(abs);
2191
+ files.push(abs);
2192
+ };
2193
+ const visit = (dir, entry) => {
2194
+ const abs = join2(dir, entry.name);
2195
+ if (entry.isSymbolicLink()) {
2196
+ if (!skipDir(entry.name) && (isDiagramFile(entry.name) || linksToDir(abs)))
2197
+ symlinks.push(abs);
2198
+ } else if (entry.isDirectory()) {
2199
+ if (!skipDir(entry.name))
2200
+ walk(abs);
2201
+ } else if (entry.isFile() && isDiagramFile(entry.name)) {
2202
+ add(abs);
2203
+ }
2204
+ };
2141
2205
  const walk = (dir) => {
2142
2206
  let entries;
2143
2207
  try {
@@ -2146,22 +2210,8 @@ var collectFiles = (paths) => {
2146
2210
  unreadable.push(dir);
2147
2211
  return;
2148
2212
  }
2149
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2150
- const abs = join2(dir, entry.name);
2151
- if (entry.isSymbolicLink()) {
2152
- if (!skipDir(entry.name) && (DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase()) || linksToDir(abs))) {
2153
- symlinks.push(abs);
2154
- }
2155
- } else if (entry.isDirectory()) {
2156
- if (!skipDir(entry.name))
2157
- walk(abs);
2158
- } else if (entry.isFile() && DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase())) {
2159
- if (!seen.has(abs)) {
2160
- seen.add(abs);
2161
- files.push(abs);
2162
- }
2163
- }
2164
- }
2213
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name)))
2214
+ visit(dir, entry);
2165
2215
  };
2166
2216
  for (const p of paths) {
2167
2217
  const abs = resolve2(p);
@@ -2169,9 +2219,8 @@ var collectFiles = (paths) => {
2169
2219
  const st = statSync(abs);
2170
2220
  if (st.isDirectory()) {
2171
2221
  walk(abs);
2172
- } else if (st.isFile() && !seen.has(abs)) {
2173
- seen.add(abs);
2174
- files.push(abs);
2222
+ } else if (st.isFile()) {
2223
+ add(abs);
2175
2224
  }
2176
2225
  } catch {
2177
2226
  unreadable.push(p);
@@ -2185,21 +2234,24 @@ var OMIT = new Set(["position", "data"]);
2185
2234
  var normalise = (node, opts = {}) => {
2186
2235
  const out = {};
2187
2236
  const isDiagram = node.type === "code" && node.lang === "mermaid";
2237
+ const blank = isDiagram && Boolean(opts.blankDiagrams);
2188
2238
  for (const [key, value] of Object.entries(node)) {
2189
2239
  if (OMIT.has(key))
2190
2240
  continue;
2191
- if (key === "children" && Array.isArray(value)) {
2192
- out.children = value.map((child) => normalise(child, opts));
2193
- } else if (key === "value" && typeof value === "string") {
2194
- if (isDiagram && opts.blankDiagrams)
2195
- continue;
2196
- out.value = node.type === "text" ? value.replace(/\s+/g, " ").trim() : value;
2197
- } else {
2198
- out[key] = value;
2199
- }
2241
+ if (blank && isStringValue(key, value))
2242
+ continue;
2243
+ out[key] = normaliseField(node, key, value, opts);
2200
2244
  }
2201
2245
  return out;
2202
2246
  };
2247
+ var isStringValue = (key, value) => key === "value" && typeof value === "string";
2248
+ var normaliseField = (node, key, value, opts) => {
2249
+ if (key === "children" && Array.isArray(value))
2250
+ return value.map((child) => normalise(child, opts));
2251
+ if (isStringValue(key, value))
2252
+ return node.type === "text" ? value.replace(/\s+/g, " ").trim() : value;
2253
+ return value;
2254
+ };
2203
2255
  var sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2204
2256
  var sameTree = (a, b, opts) => sameJson(normalise(a, opts), normalise(b, opts));
2205
2257
  var sameDiagrams = async (before, after) => {
@@ -2264,63 +2316,68 @@ var overlaps = (edits) => {
2264
2316
  return sorted.some((e, i) => i > 0 && e.start < sorted[i - 1].end);
2265
2317
  };
2266
2318
  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";
2319
+ var keyOf = (run, e) => `${e.rule}\x00${run.current.slice(e.start, e.end)}\x00${e.text}`;
2320
+ var attempt = async (run, doc, edits) => {
2321
+ const next = splice(run.current, edits);
2322
+ const after = buildDocModel(next, run.kind);
2323
+ for (const e of edits)
2324
+ if (await verify(e, doc, after) !== "accept")
2325
+ return false;
2326
+ run.current = next;
2327
+ run.applied.push(...edits);
2328
+ return true;
2329
+ };
2330
+ var soundEdits = (run, proposed) => proposed.filter((e) => {
2331
+ if (wellFormed(e, run.current.length))
2332
+ return true;
2333
+ const key = `${e.rule}\x00malformed\x00${e.start}:${e.end}`;
2334
+ if (!run.refusedKeys.has(key)) {
2335
+ run.refusedKeys.add(key);
2336
+ run.refused.push(e);
2337
+ }
2338
+ return false;
2339
+ });
2340
+ var applyRule = async (run, doc, rule) => {
2341
+ const options = optionsFor(rule, run.registry.options.get(rule.id), run.flags, run.file);
2342
+ const proposed = await rule.fix?.({ doc, file: run.file, options, helpers: HELPERS }) ?? [];
2343
+ const edits = soundEdits(run, proposed).filter((e) => run.current.slice(e.start, e.end) !== e.text && !run.refusedKeys.has(keyOf(run, e)));
2344
+ if (edits.length === 0)
2345
+ return false;
2346
+ if (edits.length > 1 && !overlaps(edits) && await attempt(run, doc, edits))
2347
+ return true;
2348
+ for (const edit of edits) {
2349
+ if (await attempt(run, doc, [edit]))
2350
+ return true;
2351
+ run.refusedKeys.add(keyOf(run, edit));
2352
+ run.refused.push(edit);
2353
+ }
2354
+ return false;
2355
+ };
2356
+ var runPass = async (run) => {
2357
+ const doc = buildDocModel(run.current, run.kind);
2358
+ for (const rule of run.registry.fixOrder)
2359
+ if (await applyRule(run, doc, rule))
2360
+ return rule.id;
2361
+ return;
2362
+ };
2267
2363
  var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
2268
2364
  checkRunFlags(flags);
2269
- const kind = documentKind(file);
2270
- const applied = [];
2271
- const refused = [];
2272
- const refusedKeys = new Set;
2273
- let current = src;
2365
+ const run = {
2366
+ file,
2367
+ flags,
2368
+ registry,
2369
+ kind: documentKind(file),
2370
+ current: src,
2371
+ applied: [],
2372
+ refused: [],
2373
+ refusedKeys: new Set
2374
+ };
2274
2375
  const keptBy = [];
2275
2376
  for (let pass = 0;pass < MAX_PASSES; pass++) {
2276
- const doc = buildDocModel(current, kind);
2277
- const keyOf = (e) => `${e.rule}\x00${current.slice(e.start, e.end)}\x00${e.text}`;
2278
- const attempt = async (edits) => {
2279
- const next = splice(current, edits);
2280
- const after = buildDocModel(next, kind);
2281
- for (const e of edits)
2282
- if (await verify(e, doc, after) !== "accept")
2283
- return false;
2284
- current = next;
2285
- applied.push(...edits);
2286
- return true;
2287
- };
2288
- let progressed = false;
2289
- for (const rule of registry.fixOrder) {
2290
- const options = optionsFor(rule, registry.options.get(rule.id), flags, file);
2291
- const proposed = await rule.fix?.({ doc, file, options, helpers: HELPERS }) ?? [];
2292
- const sound = proposed.filter((e) => {
2293
- if (wellFormed(e, current.length))
2294
- return true;
2295
- const key = `${e.rule}\x00malformed\x00${e.start}:${e.end}`;
2296
- if (!refusedKeys.has(key)) {
2297
- refusedKeys.add(key);
2298
- refused.push(e);
2299
- }
2300
- return false;
2301
- });
2302
- const edits = sound.filter((e) => current.slice(e.start, e.end) !== e.text && !refusedKeys.has(keyOf(e)));
2303
- if (edits.length === 0)
2304
- continue;
2305
- if (edits.length > 1 && !overlaps(edits) && await attempt(edits)) {
2306
- progressed = true;
2307
- } else {
2308
- for (const edit of edits) {
2309
- if (await attempt([edit])) {
2310
- progressed = true;
2311
- break;
2312
- }
2313
- refusedKeys.add(keyOf(edit));
2314
- refused.push(edit);
2315
- }
2316
- }
2317
- if (progressed) {
2318
- keptBy.push(rule.id);
2319
- break;
2320
- }
2321
- }
2322
- if (!progressed)
2323
- return { output: current, applied, refused };
2377
+ const kept = await runPass(run);
2378
+ if (kept === undefined)
2379
+ return { output: run.current, applied: run.applied, refused: run.refused };
2380
+ keptBy.push(kept);
2324
2381
  }
2325
2382
  const lingering = [...new Set(keptBy.slice(-RECENT_PASSES))].sort();
2326
2383
  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)}`;