@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/testing.js CHANGED
@@ -149,7 +149,7 @@ var parseProperties = (raw, rawStart, line) => {
149
149
  const key = pair.slice(0, colon).trim().toLowerCase();
150
150
  const valueRaw = pair.slice(colon + 1);
151
151
  const value = valueRaw.trim();
152
- if (!key || !value)
152
+ if (!(key && value))
153
153
  continue;
154
154
  properties[key] = value;
155
155
  const lead = valueRaw.length - valueRaw.trimStart().length;
@@ -304,7 +304,7 @@ var parserReason = (err) => {
304
304
  `);
305
305
  if (message.startsWith("No diagram type detected"))
306
306
  return (lines[0] ?? "").trim();
307
- return lines.filter((line, i) => !POINTER.test(line.trim()) && !POINTER.test((lines[i + 1] ?? "").trim())).join(" ").replace(/\s+/g, " ").trim();
307
+ return lines.filter((line, i) => !(POINTER.test(line.trim()) || POINTER.test((lines[i + 1] ?? "").trim()))).join(" ").replace(/\s+/g, " ").trim();
308
308
  };
309
309
  async function langiumParseAny(type, content) {
310
310
  switch (type) {
@@ -335,80 +335,7 @@ async function extractLangiumStats(content, keyword) {
335
335
  if (!mapped)
336
336
  throw new Error(`not a langium keyword: ${keyword}`);
337
337
  const ast = await langiumParseAny(mapped, content);
338
- const nodeIds = [];
339
- const subgraphNames = [];
340
- let edges = 0;
341
- let maxDepth = 0;
342
- let treeDepth = 0;
343
- switch (mapped) {
344
- case "architecture": {
345
- const services = ast.services ?? [];
346
- const groups = ast.groups ?? [];
347
- const edgeArr = ast.edges ?? [];
348
- nodeIds.push(...services.map((s) => s.id ?? ""));
349
- subgraphNames.push(...groups.map((g) => g.title ?? g.id ?? ""));
350
- maxDepth = architectureDepth(groups);
351
- edges = edgeArr.length;
352
- break;
353
- }
354
- case "pie": {
355
- const sections = ast.sections ?? [];
356
- nodeIds.push(...sections.map((s, i) => s.label ?? `slice_${i}`));
357
- break;
358
- }
359
- case "packet": {
360
- const blocks = ast.blocks ?? [];
361
- nodeIds.push(...blocks.map((_, i) => `block_${i}`));
362
- break;
363
- }
364
- case "gitGraph": {
365
- const statements = ast.statements ?? [];
366
- for (const s of statements) {
367
- if (s.$type === "Commit")
368
- nodeIds.push(s.id ?? `commit_${nodeIds.length}`);
369
- else if (s.$type === "Branch")
370
- subgraphNames.push(s.name ?? `branch_${subgraphNames.length}`);
371
- else if (s.$type === "Merge")
372
- edges++;
373
- }
374
- break;
375
- }
376
- case "radar": {
377
- const axes = ast.axes ?? [];
378
- const curves = ast.curves ?? [];
379
- nodeIds.push(...axes.map((a, i) => a.name ?? `axis_${i}`));
380
- nodeIds.push(...curves.map((c, i) => c.name ?? `curve_${i}`));
381
- edges = axes.length * curves.length;
382
- break;
383
- }
384
- case "treeView": {
385
- const nodes = ast.nodes ?? [];
386
- const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
387
- nodeIds.push(...stats.labels);
388
- edges = stats.edges;
389
- treeDepth = stats.depth;
390
- break;
391
- }
392
- case "treemap": {
393
- const rows = ast.TreemapRows ?? [];
394
- const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
395
- nodeIds.push(...stats.labels);
396
- edges = stats.edges;
397
- treeDepth = stats.depth;
398
- break;
399
- }
400
- case "wardley": {
401
- const components = ast.components ?? [];
402
- const anchors = ast.anchors ?? [];
403
- const links = ast.links ?? [];
404
- nodeIds.push(...anchors.map((a, i) => a.name ?? `anchor_${i}`));
405
- nodeIds.push(...components.map((c, i) => c.name ?? `comp_${i}`));
406
- edges = links.length;
407
- break;
408
- }
409
- case "info":
410
- break;
411
- }
338
+ const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = LANGIUM_ADAPTERS[mapped](ast);
412
339
  return {
413
340
  nodes: new Set(nodeIds.filter(Boolean)).size,
414
341
  edges,
@@ -422,6 +349,85 @@ async function extractLangiumStats(content, keyword) {
422
349
  error: null
423
350
  };
424
351
  }
352
+ var LANGIUM_ADAPTERS = {
353
+ architecture: adaptLangiumArchitecture,
354
+ info: adaptLangiumInfo,
355
+ pie: adaptLangiumPie,
356
+ gitGraph: adaptLangiumGitGraph,
357
+ packet: adaptLangiumPacket,
358
+ radar: adaptLangiumRadar,
359
+ treemap: adaptLangiumTreemap,
360
+ treeView: adaptLangiumTreeView,
361
+ wardley: adaptLangiumWardley
362
+ };
363
+ function adaptLangiumArchitecture(ast) {
364
+ const services = ast.services ?? [];
365
+ const groups = ast.groups ?? [];
366
+ const edgeArr = ast.edges ?? [];
367
+ return {
368
+ nodeIds: services.map((s) => s.id ?? ""),
369
+ edges: edgeArr.length,
370
+ subgraphNames: groups.map((g) => g.title ?? g.id ?? ""),
371
+ maxDepth: architectureDepth(groups)
372
+ };
373
+ }
374
+ function adaptLangiumInfo() {
375
+ return { nodeIds: [], edges: 0, subgraphNames: [], maxDepth: 0 };
376
+ }
377
+ function adaptLangiumPie(ast) {
378
+ const sections = ast.sections ?? [];
379
+ return { nodeIds: sections.map((s, i) => s.label ?? `slice_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
380
+ }
381
+ function adaptLangiumPacket(ast) {
382
+ const blocks = ast.blocks ?? [];
383
+ return { nodeIds: blocks.map((_, i) => `block_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
384
+ }
385
+ function adaptLangiumGitGraph(ast) {
386
+ const statements = ast.statements ?? [];
387
+ const nodeIds = [];
388
+ const subgraphNames = [];
389
+ let edges = 0;
390
+ for (const s of statements) {
391
+ if (s.$type === "Commit")
392
+ nodeIds.push(s.id ?? `commit_${nodeIds.length}`);
393
+ else if (s.$type === "Branch")
394
+ subgraphNames.push(s.name ?? `branch_${subgraphNames.length}`);
395
+ else if (s.$type === "Merge")
396
+ edges++;
397
+ }
398
+ return { nodeIds, edges, subgraphNames, maxDepth: 0 };
399
+ }
400
+ function adaptLangiumRadar(ast) {
401
+ const axes = ast.axes ?? [];
402
+ const curves = ast.curves ?? [];
403
+ return {
404
+ nodeIds: [...axes.map((a, i) => a.name ?? `axis_${i}`), ...curves.map((c, i) => c.name ?? `curve_${i}`)],
405
+ edges: axes.length * curves.length,
406
+ subgraphNames: [],
407
+ maxDepth: 0
408
+ };
409
+ }
410
+ function adaptLangiumTreeView(ast) {
411
+ const nodes = ast.nodes ?? [];
412
+ const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
413
+ return { nodeIds: stats.labels, edges: stats.edges, subgraphNames: [], maxDepth: 0, treeDepth: stats.depth };
414
+ }
415
+ function adaptLangiumTreemap(ast) {
416
+ const rows = ast.TreemapRows ?? [];
417
+ const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
418
+ return { nodeIds: stats.labels, edges: stats.edges, subgraphNames: [], maxDepth: 0, treeDepth: stats.depth };
419
+ }
420
+ function adaptLangiumWardley(ast) {
421
+ const components = ast.components ?? [];
422
+ const anchors = ast.anchors ?? [];
423
+ const links = ast.links ?? [];
424
+ return {
425
+ nodeIds: [...anchors.map((a, i) => a.name ?? `anchor_${i}`), ...components.map((c, i) => c.name ?? `comp_${i}`)],
426
+ edges: links.length,
427
+ subgraphNames: [],
428
+ maxDepth: 0
429
+ };
430
+ }
425
431
  function architectureDepth(groups) {
426
432
  const parentOf = new Map;
427
433
  for (const g of groups)
@@ -845,7 +851,7 @@ var buildDocModel = (src, kind = "markdown") => {
845
851
  collectMermaid(tree, fences);
846
852
  const diagrams = fences.flatMap((node, index) => {
847
853
  const { start, end } = node.position ?? {};
848
- if (!start || !end || start.offset === undefined || end.offset === undefined)
854
+ if (!(start && end) || start.offset === undefined || end.offset === undefined)
849
855
  return [];
850
856
  return [
851
857
  makeDiagram(src, lineStarts, {
@@ -912,72 +918,71 @@ var scorePair = (kind, d, fg, bg) => {
912
918
  };
913
919
  };
914
920
  var message = (err) => err instanceof Error ? err.message : String(err);
915
- var scoreEdgeLabel = (d, canvas, pairs, skipped) => {
921
+ var skip = (out, d, reason, blocking) => {
922
+ out.skipped.push({ selector: d.selector, line: d.line, reason, ...blocking && { blocking } });
923
+ };
924
+ var scoreEdgeLabel = (d, canvas, out) => {
916
925
  const { color } = d.properties;
917
926
  if (!color)
918
927
  return;
919
928
  try {
920
- pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t, canvas))));
929
+ out.pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, { fg: color, bg: edgeLabelBackground(t, canvas) })));
921
930
  } catch (err) {
922
- skipped.push({
923
- selector: d.selector,
924
- line: d.line,
925
- reason: `edge label text unparseable: ${message(err)}`,
926
- blocking: true
927
- });
931
+ skip(out, d, `edge label text unparseable: ${message(err)}`, true);
932
+ }
933
+ };
934
+ var githubBoxes = (fill, alpha, canvas) => alpha < 1 ? THEMES.map((t) => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
935
+ var scoreGithubPair = (out, d, kind, { fg, bg, theme }) => {
936
+ try {
937
+ out.pairs.push({ ...scorePair(kind, d, fg, bg), ...theme && { theme } });
938
+ } catch (err) {
939
+ skip(out, d, `${kind} pair unparseable: ${message(err)}`, true);
940
+ }
941
+ };
942
+ var fillAlpha = (out, d, fill) => {
943
+ try {
944
+ return alphaOf(fill);
945
+ } catch (err) {
946
+ skip(out, d, `fill unparseable: ${message(err)}`, true);
947
+ return null;
948
+ }
949
+ };
950
+ var scoreGithubStyle = (d, canvas, out) => {
951
+ const { fill, color, stroke } = d.properties;
952
+ if (!fill) {
953
+ const reason = color || stroke ? "no fill declared, so text and border contrast have no anchor" : "no color properties";
954
+ skip(out, d, reason);
955
+ return;
956
+ }
957
+ const alpha = fillAlpha(out, d, fill);
958
+ if (alpha === null)
959
+ return;
960
+ if (alpha === 0) {
961
+ skip(out, d, "fill is fully transparent, so there is no box to anchor contrast");
962
+ return;
963
+ }
964
+ const boxes = githubBoxes(fill, alpha, canvas);
965
+ if (!color) {
966
+ 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);
967
+ }
968
+ for (const [bg, theme] of boxes) {
969
+ if (color)
970
+ scoreGithubPair(out, d, "text", { fg: color, bg, theme });
971
+ if (stroke)
972
+ scoreGithubPair(out, d, "border", { fg: stroke, bg, theme });
928
973
  }
929
974
  };
930
975
  var scoreGithub = (directives, canvas = GITHUB_CANVAS) => {
931
- const pairs = [];
932
- const skipped = [];
976
+ const out = { pairs: [], skipped: [] };
933
977
  for (const d of directives) {
934
978
  if (d.kind === "linkStyle")
935
- scoreEdgeLabel(d, canvas, pairs, skipped);
936
- if (d.kind !== "classDef" && d.kind !== "style")
937
- continue;
938
- const { fill, color, stroke } = d.properties;
939
- const note = (reason, blocking) => {
940
- skipped.push({ selector: d.selector, line: d.line, reason, ...blocking && { blocking } });
941
- };
942
- if (!fill) {
943
- note(color || stroke ? "no fill declared, so text and border contrast have no anchor" : "no color properties");
944
- continue;
945
- }
946
- let alpha;
947
- try {
948
- alpha = alphaOf(fill);
949
- } catch (err) {
950
- note(`fill unparseable: ${message(err)}`, true);
951
- continue;
952
- }
953
- if (alpha === 0) {
954
- note("fill is fully transparent, so there is no box to anchor contrast");
955
- continue;
956
- }
957
- const boxes = alpha < 1 ? THEMES.map((t) => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
958
- if (!color) {
959
- note("no color: declared beside fill:, and the github profile requires one " + "(if the host forces label text, use the mkdocs-material profile)", true);
960
- }
961
- for (const [box, theme] of boxes) {
962
- if (color) {
963
- try {
964
- pairs.push({ ...scorePair("text", d, color, box), ...theme && { theme } });
965
- } catch (err) {
966
- note(`text pair unparseable: ${message(err)}`, true);
967
- }
968
- }
969
- if (stroke) {
970
- try {
971
- pairs.push({ ...scorePair("border", d, stroke, box), ...theme && { theme } });
972
- } catch (err) {
973
- note(`border pair unparseable: ${message(err)}`, true);
974
- }
975
- }
976
- }
979
+ scoreEdgeLabel(d, canvas, out);
980
+ if (d.kind === "classDef" || d.kind === "style")
981
+ scoreGithubStyle(d, canvas, out);
977
982
  }
978
- return { pairs, skipped };
983
+ return out;
979
984
  };
980
- var scoreHostPair = (kind, d, theme, fgRaw, box) => {
985
+ var scoreHostPair = (kind, d, theme, { fg: fgRaw, bg: box }) => {
981
986
  const fg = compositeOver(fgRaw, box);
982
987
  const assessment = wcagAssess(fg, box);
983
988
  return {
@@ -993,99 +998,83 @@ var scoreHostPair = (kind, d, theme, fgRaw, box) => {
993
998
  advisory: kind === "border"
994
999
  };
995
1000
  };
996
- var scoreMkdocs = (directives) => {
997
- const pairs = [];
998
- const skipped = [];
999
- for (const d of directives) {
1000
- if (d.kind === "linkStyle" && d.properties.color) {
1001
- skipped.push({
1002
- selector: d.selector,
1003
- line: d.line,
1004
- reason: "color: ignored, because the host theme forces edge label text"
1005
- });
1006
- }
1007
- if (d.kind !== "classDef" && d.kind !== "style")
1008
- continue;
1009
- const { fill, stroke, color } = d.properties;
1010
- const note = (reason) => {
1011
- skipped.push({ selector: d.selector, line: d.line, reason });
1012
- };
1013
- if (!fill) {
1014
- note("no fill declared, and the host theme controls the text");
1015
- continue;
1001
+ var scoreMkdocsStyle = (d, out) => {
1002
+ const { fill, stroke, color } = d.properties;
1003
+ if (!fill) {
1004
+ skip(out, d, "no fill declared, and the host theme controls the text");
1005
+ return;
1006
+ }
1007
+ if (color)
1008
+ skip(out, d, "color: ignored, because the host theme forces label text");
1009
+ for (const theme of THEMES) {
1010
+ let box;
1011
+ try {
1012
+ box = compositeOver(fill, MKDOCS_MATERIAL[theme].bg);
1013
+ } catch (err) {
1014
+ skip(out, d, `fill unparseable: ${message(err)}`, true);
1015
+ break;
1016
1016
  }
1017
- if (color)
1018
- note("color: ignored, because the host theme forces label text");
1019
- for (const theme of THEMES) {
1020
- let box;
1017
+ out.pairs.push(scoreHostPair("text", d, theme, { fg: MKDOCS_MATERIAL[theme].text, bg: box }));
1018
+ if (stroke) {
1021
1019
  try {
1022
- box = compositeOver(fill, MKDOCS_MATERIAL[theme].bg);
1020
+ out.pairs.push(scoreHostPair("border", d, theme, { fg: stroke, bg: box }));
1023
1021
  } catch (err) {
1024
- skipped.push({
1025
- selector: d.selector,
1026
- line: d.line,
1027
- reason: `fill unparseable: ${message(err)}`,
1028
- blocking: true
1029
- });
1030
- break;
1031
- }
1032
- pairs.push(scoreHostPair("text", d, theme, MKDOCS_MATERIAL[theme].text, box));
1033
- if (stroke) {
1034
- try {
1035
- pairs.push(scoreHostPair("border", d, theme, stroke, box));
1036
- } catch (err) {
1037
- skipped.push({
1038
- selector: d.selector,
1039
- line: d.line,
1040
- reason: `border pair unparseable: ${message(err)}`,
1041
- blocking: true
1042
- });
1043
- }
1022
+ skip(out, d, `border pair unparseable: ${message(err)}`, true);
1044
1023
  }
1045
1024
  }
1046
1025
  }
1047
- return { pairs, skipped };
1026
+ };
1027
+ var scoreMkdocs = (directives) => {
1028
+ const out = { pairs: [], skipped: [] };
1029
+ for (const d of directives) {
1030
+ if (d.kind === "linkStyle" && d.properties.color) {
1031
+ skip(out, d, "color: ignored, because the host theme forces edge label text");
1032
+ }
1033
+ if (d.kind === "classDef" || d.kind === "style")
1034
+ scoreMkdocsStyle(d, out);
1035
+ }
1036
+ return out;
1037
+ };
1038
+ var erRows = (theme, fill) => {
1039
+ const anchor = MERMAID_ER[theme];
1040
+ return [
1041
+ ["odd", anchor.odd],
1042
+ ["even", fill ? compositeOver(fill, anchor.even) : anchor.even]
1043
+ ];
1044
+ };
1045
+ var erPairs = (d) => {
1046
+ const { fill, color, stroke } = d.properties;
1047
+ const scored = [];
1048
+ const score = (kind, theme, row, colours) => {
1049
+ scored.push({ ...scoreHostPair(kind, d, theme, colours), row, advisory: kind === "border" });
1050
+ };
1051
+ for (const theme of THEMES) {
1052
+ for (const [row, bg] of erRows(theme, fill)) {
1053
+ if (color || row === "even" && fill)
1054
+ score("text", theme, row, { fg: color ?? MERMAID_ER[theme].label, bg });
1055
+ if (stroke)
1056
+ score("border", theme, row, { fg: stroke, bg });
1057
+ }
1058
+ }
1059
+ return scored;
1048
1060
  };
1049
1061
  var scoreEr = (directives) => {
1050
- const pairs = [];
1051
- const skipped = [];
1062
+ const out = { pairs: [], skipped: [] };
1052
1063
  for (const d of directives) {
1053
1064
  if (d.kind !== "classDef" && d.kind !== "style")
1054
1065
  continue;
1055
1066
  const { fill, color, stroke } = d.properties;
1056
- if (!fill && !color && !stroke) {
1057
- skipped.push({ selector: d.selector, line: d.line, reason: "no color properties" });
1067
+ if (!(fill || color || stroke)) {
1068
+ skip(out, d, "no color properties");
1058
1069
  continue;
1059
1070
  }
1060
- const scored = [];
1061
- const score = (kind, theme, row, fg, bg) => {
1062
- scored.push({ ...scoreHostPair(kind, d, theme, fg, bg), row, advisory: kind === "border" });
1063
- };
1064
1071
  try {
1065
- for (const theme of THEMES) {
1066
- const anchor = MERMAID_ER[theme];
1067
- const rows = [
1068
- ["odd", anchor.odd],
1069
- ["even", fill ? compositeOver(fill, anchor.even) : anchor.even]
1070
- ];
1071
- for (const [row, bg] of rows) {
1072
- if (color || row === "even" && fill)
1073
- score("text", theme, row, color ?? anchor.label, bg);
1074
- if (stroke)
1075
- score("border", theme, row, stroke, bg);
1076
- }
1077
- }
1078
- pairs.push(...scored);
1072
+ out.pairs.push(...erPairs(d));
1079
1073
  } catch (err) {
1080
- skipped.push({
1081
- selector: d.selector,
1082
- line: d.line,
1083
- reason: `erDiagram pair unparseable: ${message(err)}`,
1084
- blocking: true
1085
- });
1074
+ skip(out, d, `erDiagram pair unparseable: ${message(err)}`, true);
1086
1075
  }
1087
1076
  }
1088
- return { pairs, skipped };
1077
+ return out;
1089
1078
  };
1090
1079
  var scoreForProfile = (directives, profile, keyword, canvas = GITHUB_CANVAS) => {
1091
1080
  if (profile === "mkdocs-material")
@@ -1127,9 +1116,7 @@ var themeVariable = (selector, line) => ({
1127
1116
  spans: {},
1128
1117
  line
1129
1118
  });
1130
- var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1131
- const pairs = [];
1132
- const skipped = [];
1119
+ var sequenceLines = (source) => {
1133
1120
  const lines = source.split(`
1134
1121
  `);
1135
1122
  const keywordLine = lines.findIndex((l) => /^\s*sequenceDiagram\b/.test(l)) + 1;
@@ -1137,94 +1124,125 @@ var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1137
1124
  const found = lines.findLastIndex((l) => new RegExp(`\\b${key}["']?\\s*:`).test(l));
1138
1125
  return found < 0 ? keywordLine : found + 1;
1139
1126
  };
1140
- const rectLines = lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []);
1141
- const mermaid = await getMermaidCore();
1142
- let config;
1143
- let db;
1127
+ return { lineOf, rectLines: lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []) };
1128
+ };
1129
+ var parseSequence = async (mermaid, source) => {
1144
1130
  try {
1145
- config = (await mermaid.parse(source)).config;
1146
- db = (await mermaid.mermaidAPI.getDiagramFromText(source)).db;
1131
+ const { config } = await mermaid.parse(source);
1132
+ const { db } = await mermaid.mermaidAPI.getDiagramFromText(source);
1133
+ return { config, db };
1147
1134
  } catch {
1148
- return { pairs, skipped };
1135
+ return null;
1136
+ }
1137
+ };
1138
+ var unmodelled = (config) => {
1139
+ if (config.themeCSS !== undefined) {
1140
+ return { selector: "themeCSS", reason: "themeCSS restyles the diagram with CSS the audit cannot model" };
1149
1141
  }
1150
- const block = (selector, reason) => {
1151
- skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
1152
- return { pairs, skipped };
1153
- };
1154
- if (config.themeCSS !== undefined)
1155
- return block("themeCSS", "themeCSS restyles the diagram with CSS the audit cannot model");
1156
1142
  const pinned = config.theme;
1157
1143
  if (pinned !== undefined && !SEQUENCE_THEMES.includes(String(pinned))) {
1158
- return block("theme", `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`);
1144
+ return {
1145
+ selector: "theme",
1146
+ reason: `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`
1147
+ };
1148
+ }
1149
+ return null;
1150
+ };
1151
+ var sequenceThemes = (pinned, host) => {
1152
+ const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
1153
+ return { light: theme("default"), dark: theme(host ? "default" : "dark") };
1154
+ };
1155
+ var sequencePair = ({ theme: t, page, vars, host, lineOf, out }) => (kind, fgKey, bg, { suffix = "", at } = {}) => {
1156
+ const color = (key) => {
1157
+ const value = vars[key];
1158
+ if (typeof value !== "string")
1159
+ throw new Error(`${key} is not a resolved colour`);
1160
+ return value;
1161
+ };
1162
+ const selector = fgKey + suffix;
1163
+ const line = at ?? lineOf(fgKey);
1164
+ try {
1165
+ const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
1166
+ out.pairs.push({
1167
+ ...scoreHostPair(kind, themeVariable(selector, line), t, { fg: color(fgKey), bg: box }),
1168
+ advisory: host
1169
+ });
1170
+ } catch (err) {
1171
+ out.skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
1172
+ }
1173
+ };
1174
+ var scoreHostSequence = (pair, page, features) => {
1175
+ const onPage = { colour: page };
1176
+ pair("border", "actorLineColor", onPage);
1177
+ pair("border", "signalColor", onPage, { suffix: " (arrowhead)" });
1178
+ if (features.activation)
1179
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1180
+ };
1181
+ var scoreAuthorSequence = (pair, page, features, rectLines) => {
1182
+ const onPage = { colour: page };
1183
+ pair("text", "actorTextColor", { key: "actorBkg" });
1184
+ pair("border", "actorBorder", { key: "actorBkg" });
1185
+ if (features.humanActor) {
1186
+ pair("text", "actorTextColor", onPage, { suffix: " (human actor label)" });
1187
+ pair("border", "actorBorder", onPage, { suffix: " (human actor glyph)" });
1188
+ }
1189
+ pair("border", "actorLineColor", onPage);
1190
+ pair("text", "signalTextColor", onPage);
1191
+ pair("border", "signalColor", onPage);
1192
+ if (features.note)
1193
+ pair("text", "noteTextColor", { key: "noteBkgColor" });
1194
+ if (features.frame) {
1195
+ pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
1196
+ pair("text", "loopTextColor", onPage);
1197
+ pair("border", "labelBoxBorderColor", onPage, { suffix: " (loop frame)" });
1198
+ }
1199
+ if (features.activation)
1200
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1201
+ if (features.numbered)
1202
+ pair("text", "sequenceNumberColor", { key: "signalColor" });
1203
+ for (const [i, rect] of features.rects.entries()) {
1204
+ const fill = { colour: rect };
1205
+ const where = { suffix: " (rect)", at: rectLines[i] };
1206
+ pair("text", "signalTextColor", fill, where);
1207
+ pair("border", "signalColor", fill, where);
1208
+ if (features.frame)
1209
+ pair("text", "loopTextColor", fill, where);
1159
1210
  }
1211
+ };
1212
+ var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
1213
+ const out = { pairs: [], skipped: [] };
1214
+ const { lineOf, rectLines } = sequenceLines(source);
1215
+ const mermaid = await getMermaidCore();
1216
+ const parsed = await parseSequence(mermaid, source);
1217
+ if (!parsed)
1218
+ return out;
1219
+ const { config, db } = parsed;
1220
+ const block = (selector, reason) => {
1221
+ out.skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
1222
+ return out;
1223
+ };
1224
+ const problem = unmodelled(config);
1225
+ if (problem)
1226
+ return block(problem.selector, problem.reason);
1160
1227
  const features = sequenceFeatures(db);
1161
1228
  const host = profile === "mkdocs-material";
1162
- const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
1163
1229
  let variables;
1164
1230
  try {
1165
- variables = resolveVariables(mermaid, config, { light: theme("default"), dark: theme(host ? "default" : "dark") });
1231
+ variables = resolveVariables(mermaid, config, sequenceThemes(config.theme, host));
1166
1232
  } catch (err) {
1167
1233
  return block("themeVariables", `Mermaid could not resolve the theme: ${message(err)}`);
1168
1234
  }
1169
1235
  for (const t of THEMES) {
1170
1236
  const page = host ? MKDOCS_MATERIAL[t].bg : canvas[t];
1171
- const vars = variables[t];
1172
- const color = (key) => {
1173
- const value = vars[key];
1174
- if (typeof value !== "string")
1175
- throw new Error(`${key} is not a resolved colour`);
1176
- return value;
1177
- };
1178
- const pair = (kind, fgKey, bg, suffix = "", at) => {
1179
- const selector = fgKey + suffix;
1180
- const line = at ?? lineOf(fgKey);
1181
- try {
1182
- const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
1183
- pairs.push({ ...scoreHostPair(kind, themeVariable(selector, line), t, color(fgKey), box), advisory: host });
1184
- } catch (err) {
1185
- skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
1186
- }
1187
- };
1188
- const onPage = { colour: page };
1189
- if (host) {
1190
- pair("border", "actorLineColor", onPage);
1191
- pair("border", "signalColor", onPage, " (arrowhead)");
1192
- if (features.activation)
1193
- pair("border", "activationBorderColor", { key: "activationBkgColor" });
1194
- continue;
1195
- }
1196
- pair("text", "actorTextColor", { key: "actorBkg" });
1197
- pair("border", "actorBorder", { key: "actorBkg" });
1198
- if (features.humanActor) {
1199
- pair("text", "actorTextColor", onPage, " (human actor label)");
1200
- pair("border", "actorBorder", onPage, " (human actor glyph)");
1201
- }
1202
- pair("border", "actorLineColor", onPage);
1203
- pair("text", "signalTextColor", onPage);
1204
- pair("border", "signalColor", onPage);
1205
- if (features.note)
1206
- pair("text", "noteTextColor", { key: "noteBkgColor" });
1207
- if (features.frame) {
1208
- pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
1209
- pair("text", "loopTextColor", onPage);
1210
- pair("border", "labelBoxBorderColor", onPage, " (loop frame)");
1211
- }
1212
- if (features.activation)
1213
- pair("border", "activationBorderColor", { key: "activationBkgColor" });
1214
- if (features.numbered)
1215
- pair("text", "sequenceNumberColor", { key: "signalColor" });
1216
- for (const [i, rect] of features.rects.entries()) {
1217
- const fill = { colour: rect };
1218
- const at = rectLines[i];
1219
- pair("text", "signalTextColor", fill, " (rect)", at);
1220
- pair("border", "signalColor", fill, " (rect)", at);
1221
- if (features.frame)
1222
- pair("text", "loopTextColor", fill, " (rect)", at);
1223
- }
1237
+ const pair = sequencePair({ theme: t, page, vars: variables[t], host, lineOf, out });
1238
+ if (host)
1239
+ scoreHostSequence(pair, page, features);
1240
+ else
1241
+ scoreAuthorSequence(pair, page, features, rectLines);
1224
1242
  }
1225
- return { pairs, skipped };
1243
+ return out;
1226
1244
  };
1227
- var scoreDiagram = (source, directives, profile, keyword, canvas = GITHUB_CANVAS) => keyword === "sequenceDiagram" ? scoreSequence(source, profile, canvas) : Promise.resolve(scoreForProfile(directives, profile, keyword, canvas));
1245
+ var scoreDiagram = ({ source, directives, keyword }, profile, canvas = GITHUB_CANVAS) => keyword === "sequenceDiagram" ? scoreSequence(source, profile, canvas) : Promise.resolve(scoreForProfile(directives, profile, keyword, canvas));
1228
1246
  var detectProfile = (filePath) => {
1229
1247
  let dir = dirname(resolve(filePath));
1230
1248
  for (let i = 0;i < 20; i++) {
@@ -1316,7 +1334,7 @@ var canonicalPreset = (name) => {
1316
1334
  var configFromPreset = (name) => {
1317
1335
  const canonical = canonicalPreset(name);
1318
1336
  const preset = canonical ? PRESETS[canonical] : undefined;
1319
- if (!canonical || !preset) {
1337
+ if (!(canonical && preset)) {
1320
1338
  throw new Error(`Unknown preset '${name}'. Valid: ${PRESET_NAMES.join(", ")}`);
1321
1339
  }
1322
1340
  return {
@@ -1395,7 +1413,7 @@ var measure = async (diagram, config) => {
1395
1413
  return null;
1396
1414
  return { stats, metrics: calculateComplexity(stats, config) };
1397
1415
  };
1398
- var finding = (ctx, diagram, rule, message2, remediation) => ({
1416
+ var finding = (ctx, diagram, rule, { message: message2, remediation }) => ({
1399
1417
  file: ctx.file,
1400
1418
  line: diagram.startLine,
1401
1419
  endLine: diagram.endLine,
@@ -1446,7 +1464,7 @@ var contrastOf = (diagram, settings) => {
1446
1464
  }
1447
1465
  let result = bySettings.get(key);
1448
1466
  if (!result) {
1449
- result = scoreDiagram(diagram.source, diagram.directives, profile, diagram.keyword, canvas);
1467
+ result = scoreDiagram(diagram, profile, canvas);
1450
1468
  bySettings.set(key, result);
1451
1469
  }
1452
1470
  return result;
@@ -1468,7 +1486,7 @@ var complexityRule = (spec) => ({
1468
1486
  continue;
1469
1487
  const hit = spec.hit(measured, config);
1470
1488
  if (hit)
1471
- found.push(finding(ctx, diagram, spec.id, hit.message, hit.remediation));
1489
+ found.push(finding(ctx, diagram, spec.id, hit));
1472
1490
  }
1473
1491
  return found;
1474
1492
  }
@@ -1612,6 +1630,30 @@ var contrastDirectiveUnauditable = {
1612
1630
  var AA_NORMAL2 = 4.5;
1613
1631
  var CANDIDATES = ["#000000", "#ffffff"];
1614
1632
  var ratioOn = (fg, bg) => wcagRatio(compositeOver(fg, bg), bg);
1633
+ var remediationOf = (profile, kind) => {
1634
+ if (profile === "mkdocs-material") {
1635
+ return "The host theme forces the label text, so change the fill until the host text reads on it in both themes.";
1636
+ }
1637
+ if (kind === "themeVariables") {
1638
+ 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.";
1639
+ }
1640
+ if (kind === "linkStyle") {
1641
+ 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.";
1642
+ }
1643
+ return "Darken or lighten the fill, or change color:, until the ratio reaches 4.5:1 in every theme.";
1644
+ };
1645
+ var textFinding = (ctx, diagram, p, profile) => {
1646
+ const where = [p.theme, p.row].filter(Boolean).join("/");
1647
+ const line = fileLine(diagram, p.line);
1648
+ return {
1649
+ file: ctx.file,
1650
+ line,
1651
+ endLine: line,
1652
+ rule: RULE.TEXT,
1653
+ message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1654
+ remediation: remediationOf(profile, p.directive.kind)
1655
+ };
1656
+ };
1615
1657
  var contrastTextBelowAa = {
1616
1658
  id: RULE.TEXT,
1617
1659
  category: "contrast",
@@ -1622,18 +1664,8 @@ var contrastTextBelowAa = {
1622
1664
  const found = [];
1623
1665
  for (const diagram of ctx.doc.diagrams) {
1624
1666
  for (const p of (await contrastOf(diagram, settings)).pairs) {
1625
- if (p.kind !== "text" || p.passes)
1626
- continue;
1627
- const where = [p.theme, p.row].filter(Boolean).join("/");
1628
- const line = fileLine(diagram, p.line);
1629
- found.push({
1630
- file: ctx.file,
1631
- line,
1632
- endLine: line,
1633
- rule: RULE.TEXT,
1634
- message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1635
- 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."
1636
- });
1667
+ if (p.kind === "text" && !p.passes)
1668
+ found.push(textFinding(ctx, diagram, p, settings.profile));
1637
1669
  }
1638
1670
  }
1639
1671
  return found;
@@ -1692,7 +1724,7 @@ var parseDiagramUnparseable = {
1692
1724
  if (!parseFailed(diagram.source, stats))
1693
1725
  continue;
1694
1726
  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.";
1695
- found.push(finding(ctx, diagram, RULE.UNPARSEABLE, messageOf(stats), remediation));
1727
+ found.push(finding(ctx, diagram, RULE.UNPARSEABLE, { message: messageOf(stats), remediation }));
1696
1728
  }
1697
1729
  return found;
1698
1730
  }
@@ -1704,7 +1736,10 @@ var parseFenceUnterminated = {
1704
1736
  id: RULE.FENCE_UNTERMINATED,
1705
1737
  category: "parse",
1706
1738
  summary: "a mermaid fence that is never closed",
1707
- 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.")),
1739
+ check: (ctx) => ctx.doc.diagrams.filter((d) => !d.terminated).map((d) => finding(ctx, d, RULE.FENCE_UNTERMINATED, {
1740
+ message: `mermaid fence opened at line ${d.startLine} is never closed`,
1741
+ 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."
1742
+ })),
1708
1743
  fix: (ctx) => {
1709
1744
  const edits = [];
1710
1745
  const { src } = ctx.doc;
@@ -1801,20 +1836,20 @@ var checkCanvas = (where, rule, name, value) => {
1801
1836
  }
1802
1837
  }
1803
1838
  };
1804
- var checkOptions = (where, rule, options) => {
1805
- const accepted = Object.keys(rule.options ?? {});
1806
- for (const [name, value] of Object.entries(options)) {
1807
- const want = rule.options?.[name];
1808
- if (!want) {
1809
- const list = accepted.length > 0 ? `it accepts ${accepted.join(", ")}` : "it takes no options";
1810
- throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
1811
- }
1812
- if (typeOf(value) !== want) {
1813
- throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be ${describeType(want)}`);
1814
- }
1815
- if (want === "canvas")
1816
- checkCanvas(where, rule, name, value);
1839
+ var checkOption = (where, rule, name, value) => {
1840
+ const want = rule.options?.[name];
1841
+ if (!want) {
1842
+ const accepted = Object.keys(rule.options ?? {});
1843
+ const list = accepted.length > 0 ? `it accepts ${accepted.join(", ")}` : "it takes no options";
1844
+ throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
1817
1845
  }
1846
+ if (typeOf(value) !== want) {
1847
+ throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be ${describeType(want)}`);
1848
+ }
1849
+ if (want === "canvas")
1850
+ checkCanvas(where, rule, name, value);
1851
+ };
1852
+ var checkSharedOptions = (where, rule, options) => {
1818
1853
  if (options.profile === "mkdocs-material" && options.canvas !== undefined && rule.options?.canvas === "canvas") {
1819
1854
  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`);
1820
1855
  }
@@ -1825,10 +1860,12 @@ var checkOptions = (where, rule, options) => {
1825
1860
  throw new RuleOptionError(where, rule.id, "profile", `option "profile" of "${rule.id}" must be one of ${PROFILES.join(", ")}`);
1826
1861
  }
1827
1862
  };
1828
- var buildRegistry = (input) => {
1829
- const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
1830
- const selection = input.selection ?? {};
1831
- checkSelection(selection, categories);
1863
+ var checkOptions = (where, rule, options) => {
1864
+ for (const [name, value] of Object.entries(options))
1865
+ checkOption(where, rule, name, value);
1866
+ checkSharedOptions(where, rule, options);
1867
+ };
1868
+ var applyConfig = (rules, config, ignoreUnknownPluginIds) => {
1832
1869
  const off = new Set;
1833
1870
  const options = new Map;
1834
1871
  const ignored = [];
@@ -1847,6 +1884,13 @@ var buildRegistry = (input) => {
1847
1884
  if (setting.severity === "off")
1848
1885
  off.add(id);
1849
1886
  }
1887
+ return { off, options, ignored };
1888
+ };
1889
+ var buildRegistry = (input) => {
1890
+ const { rules, fixOrder, categories, sources = [], config, ignoreUnknownPluginIds = false } = input;
1891
+ const selection = input.selection ?? {};
1892
+ checkSelection(selection, categories);
1893
+ const { off, options, ignored } = applyConfig(rules, config, ignoreUnknownPluginIds);
1850
1894
  for (const rule of rules) {
1851
1895
  if (selection.enable?.includes(rule.category))
1852
1896
  off.delete(rule.id);
@@ -1899,6 +1943,17 @@ var checkRunFlags = (flags, prefix = "") => {
1899
1943
  var canvasOptions = (rule) => Object.entries(rule.options ?? {}).filter(([, type]) => type === "canvas").map(([name]) => name);
1900
1944
  var hasFlagCanvas = (flags) => THEMES.some((side) => flags.canvas?.[side] !== undefined);
1901
1945
  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";
1946
+ var mergeCanvas = (current, canvas) => {
1947
+ const merged = {
1948
+ ...typeof current === "object" && current !== null ? current : {}
1949
+ };
1950
+ for (const side of THEMES) {
1951
+ const value = canvas[side];
1952
+ if (value !== undefined)
1953
+ merged[side] = value;
1954
+ }
1955
+ return merged;
1956
+ };
1902
1957
  var optionsFor = (rule, configured, flags, file) => {
1903
1958
  const options = { ...configured };
1904
1959
  for (const name of ["preset", "profile"]) {
@@ -1910,18 +1965,8 @@ var optionsFor = (rule, configured, flags, file) => {
1910
1965
  options.preset = canonicalPreset(options.preset) ?? options.preset;
1911
1966
  const canvas = flags.canvas;
1912
1967
  if (canvas && hasFlagCanvas(flags)) {
1913
- for (const name of canvasOptions(rule)) {
1914
- const current = options[name];
1915
- const merged = {
1916
- ...typeof current === "object" && current !== null ? current : {}
1917
- };
1918
- for (const side of THEMES) {
1919
- const value = canvas[side];
1920
- if (value !== undefined)
1921
- merged[side] = value;
1922
- }
1923
- options[name] = merged;
1924
- }
1968
+ for (const name of canvasOptions(rule))
1969
+ options[name] = mergeCanvas(options[name], canvas);
1925
1970
  }
1926
1971
  if (file !== undefined && setsAside(rule, options, flags, file)) {
1927
1972
  for (const name of canvasOptions(rule))
@@ -1942,21 +1987,24 @@ var OMIT = new Set(["position", "data"]);
1942
1987
  var normalise = (node, opts = {}) => {
1943
1988
  const out = {};
1944
1989
  const isDiagram = node.type === "code" && node.lang === "mermaid";
1990
+ const blank = isDiagram && Boolean(opts.blankDiagrams);
1945
1991
  for (const [key, value] of Object.entries(node)) {
1946
1992
  if (OMIT.has(key))
1947
1993
  continue;
1948
- if (key === "children" && Array.isArray(value)) {
1949
- out.children = value.map((child) => normalise(child, opts));
1950
- } else if (key === "value" && typeof value === "string") {
1951
- if (isDiagram && opts.blankDiagrams)
1952
- continue;
1953
- out.value = node.type === "text" ? value.replace(/\s+/g, " ").trim() : value;
1954
- } else {
1955
- out[key] = value;
1956
- }
1994
+ if (blank && isStringValue(key, value))
1995
+ continue;
1996
+ out[key] = normaliseField(node, key, value, opts);
1957
1997
  }
1958
1998
  return out;
1959
1999
  };
2000
+ var isStringValue = (key, value) => key === "value" && typeof value === "string";
2001
+ var normaliseField = (node, key, value, opts) => {
2002
+ if (key === "children" && Array.isArray(value))
2003
+ return value.map((child) => normalise(child, opts));
2004
+ if (isStringValue(key, value))
2005
+ return node.type === "text" ? value.replace(/\s+/g, " ").trim() : value;
2006
+ return value;
2007
+ };
1960
2008
  var sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
1961
2009
  var sameTree = (a, b, opts) => sameJson(normalise(a, opts), normalise(b, opts));
1962
2010
  var sameDiagrams = async (before, after) => {
@@ -2021,63 +2069,68 @@ var overlaps = (edits) => {
2021
2069
  return sorted.some((e, i) => i > 0 && e.start < sorted[i - 1].end);
2022
2070
  };
2023
2071
  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";
2072
+ var keyOf = (run, e) => `${e.rule}\x00${run.current.slice(e.start, e.end)}\x00${e.text}`;
2073
+ var attempt = async (run, doc, edits) => {
2074
+ const next = splice(run.current, edits);
2075
+ const after = buildDocModel(next, run.kind);
2076
+ for (const e of edits)
2077
+ if (await verify(e, doc, after) !== "accept")
2078
+ return false;
2079
+ run.current = next;
2080
+ run.applied.push(...edits);
2081
+ return true;
2082
+ };
2083
+ var soundEdits = (run, proposed) => proposed.filter((e) => {
2084
+ if (wellFormed(e, run.current.length))
2085
+ return true;
2086
+ const key = `${e.rule}\x00malformed\x00${e.start}:${e.end}`;
2087
+ if (!run.refusedKeys.has(key)) {
2088
+ run.refusedKeys.add(key);
2089
+ run.refused.push(e);
2090
+ }
2091
+ return false;
2092
+ });
2093
+ var applyRule = async (run, doc, rule) => {
2094
+ const options = optionsFor(rule, run.registry.options.get(rule.id), run.flags, run.file);
2095
+ const proposed = await rule.fix?.({ doc, file: run.file, options, helpers: HELPERS }) ?? [];
2096
+ const edits = soundEdits(run, proposed).filter((e) => run.current.slice(e.start, e.end) !== e.text && !run.refusedKeys.has(keyOf(run, e)));
2097
+ if (edits.length === 0)
2098
+ return false;
2099
+ if (edits.length > 1 && !overlaps(edits) && await attempt(run, doc, edits))
2100
+ return true;
2101
+ for (const edit of edits) {
2102
+ if (await attempt(run, doc, [edit]))
2103
+ return true;
2104
+ run.refusedKeys.add(keyOf(run, edit));
2105
+ run.refused.push(edit);
2106
+ }
2107
+ return false;
2108
+ };
2109
+ var runPass = async (run) => {
2110
+ const doc = buildDocModel(run.current, run.kind);
2111
+ for (const rule of run.registry.fixOrder)
2112
+ if (await applyRule(run, doc, rule))
2113
+ return rule.id;
2114
+ return;
2115
+ };
2024
2116
  var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
2025
2117
  checkRunFlags(flags);
2026
- const kind = documentKind(file);
2027
- const applied = [];
2028
- const refused = [];
2029
- const refusedKeys = new Set;
2030
- let current = src;
2118
+ const run = {
2119
+ file,
2120
+ flags,
2121
+ registry,
2122
+ kind: documentKind(file),
2123
+ current: src,
2124
+ applied: [],
2125
+ refused: [],
2126
+ refusedKeys: new Set
2127
+ };
2031
2128
  const keptBy = [];
2032
2129
  for (let pass = 0;pass < MAX_PASSES; pass++) {
2033
- const doc = buildDocModel(current, kind);
2034
- const keyOf = (e) => `${e.rule}\x00${current.slice(e.start, e.end)}\x00${e.text}`;
2035
- const attempt = async (edits) => {
2036
- const next = splice(current, edits);
2037
- const after = buildDocModel(next, kind);
2038
- for (const e of edits)
2039
- if (await verify(e, doc, after) !== "accept")
2040
- return false;
2041
- current = next;
2042
- applied.push(...edits);
2043
- return true;
2044
- };
2045
- let progressed = false;
2046
- for (const rule of registry.fixOrder) {
2047
- const options = optionsFor(rule, registry.options.get(rule.id), flags, file);
2048
- const proposed = await rule.fix?.({ doc, file, options, helpers: HELPERS }) ?? [];
2049
- const sound = proposed.filter((e) => {
2050
- if (wellFormed(e, current.length))
2051
- return true;
2052
- const key = `${e.rule}\x00malformed\x00${e.start}:${e.end}`;
2053
- if (!refusedKeys.has(key)) {
2054
- refusedKeys.add(key);
2055
- refused.push(e);
2056
- }
2057
- return false;
2058
- });
2059
- const edits = sound.filter((e) => current.slice(e.start, e.end) !== e.text && !refusedKeys.has(keyOf(e)));
2060
- if (edits.length === 0)
2061
- continue;
2062
- if (edits.length > 1 && !overlaps(edits) && await attempt(edits)) {
2063
- progressed = true;
2064
- } else {
2065
- for (const edit of edits) {
2066
- if (await attempt([edit])) {
2067
- progressed = true;
2068
- break;
2069
- }
2070
- refusedKeys.add(keyOf(edit));
2071
- refused.push(edit);
2072
- }
2073
- }
2074
- if (progressed) {
2075
- keptBy.push(rule.id);
2076
- break;
2077
- }
2078
- }
2079
- if (!progressed)
2080
- return { output: current, applied, refused };
2130
+ const kept = await runPass(run);
2131
+ if (kept === undefined)
2132
+ return { output: run.current, applied: run.applied, refused: run.refused };
2133
+ keptBy.push(kept);
2081
2134
  }
2082
2135
  const lingering = [...new Set(keptBy.slice(-RECENT_PASSES))].sort();
2083
2136
  throw new FixNotConvergingError(file, MAX_PASSES, lingering);
@@ -2094,23 +2147,40 @@ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
2094
2147
  var message2 = (err) => err instanceof Error ? err.message : String(err);
2095
2148
  var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]", "canvas"]);
2096
2149
  var RESERVED_OPTIONS = ["preset", "profile"];
2097
- var checkRule = (spec, key, raw, own) => {
2150
+ var checkRuleCategory = (spec, key, category, own) => {
2098
2151
  const where = `rule "${key}"`;
2099
- if (!isRecord(raw))
2100
- throw new PluginContractError(spec, `${where} must be an object`);
2101
- if (!RULE_KEY.test(key)) {
2102
- throw new PluginContractError(spec, `${where} is not a valid key; use lower-case words joined by hyphens`);
2103
- }
2104
- const { category, summary, options, check, fix } = raw;
2105
2152
  if (typeof category !== "string" || !CATEGORY_WORD.test(category)) {
2106
2153
  throw new PluginContractError(spec, `${where} needs a category of one lower-case word`);
2107
2154
  }
2108
- if (!BUILTIN_CATEGORIES.has(category) && !own.has(category)) {
2155
+ if (!(BUILTIN_CATEGORIES.has(category) || own.has(category))) {
2109
2156
  throw new PluginContractError(spec, `${where} uses the category "${category}", which this plugin does not declare in meta.categories`);
2110
2157
  }
2111
2158
  if (!key.startsWith(`${category}-`)) {
2112
2159
  throw new PluginContractError(spec, `${where} must start with its category, as "${category}-short-name"`);
2113
2160
  }
2161
+ };
2162
+ var checkRuleOptions = (spec, where, options) => {
2163
+ if (options === undefined)
2164
+ return;
2165
+ const valid = isRecord(options) && Object.values(options).every((t) => OPTION_TYPES.has(t));
2166
+ if (!valid) {
2167
+ throw new PluginContractError(spec, `${where} options must map names to number, string, boolean, string[] or canvas`);
2168
+ }
2169
+ for (const name of RESERVED_OPTIONS) {
2170
+ if (name in options && options[name] !== "string") {
2171
+ 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`);
2172
+ }
2173
+ }
2174
+ };
2175
+ var checkRule = (spec, key, raw, own) => {
2176
+ const where = `rule "${key}"`;
2177
+ if (!isRecord(raw))
2178
+ throw new PluginContractError(spec, `${where} must be an object`);
2179
+ if (!RULE_KEY.test(key)) {
2180
+ throw new PluginContractError(spec, `${where} is not a valid key; use lower-case words joined by hyphens`);
2181
+ }
2182
+ const { category, summary, options, check, fix } = raw;
2183
+ checkRuleCategory(spec, key, category, own);
2114
2184
  if (typeof summary !== "string" || summary === "")
2115
2185
  throw new PluginContractError(spec, `${where} needs a summary`);
2116
2186
  if (typeof check !== "function")
@@ -2118,17 +2188,7 @@ var checkRule = (spec, key, raw, own) => {
2118
2188
  if (fix !== undefined && typeof fix !== "function") {
2119
2189
  throw new PluginContractError(spec, `${where} fix must be a function`);
2120
2190
  }
2121
- if (options !== undefined) {
2122
- const valid = isRecord(options) && Object.values(options).every((t) => OPTION_TYPES.has(t));
2123
- if (!valid) {
2124
- throw new PluginContractError(spec, `${where} options must map names to number, string, boolean, string[] or canvas`);
2125
- }
2126
- for (const name of RESERVED_OPTIONS) {
2127
- if (name in options && options[name] !== "string") {
2128
- 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`);
2129
- }
2130
- }
2131
- }
2191
+ checkRuleOptions(spec, where, options);
2132
2192
  return raw;
2133
2193
  };
2134
2194
  var checkCategories = (spec, raw) => {
@@ -2149,7 +2209,7 @@ var checkCategories = (spec, raw) => {
2149
2209
  return raw;
2150
2210
  };
2151
2211
  var parsePlugin = (spec, raw, reserved) => {
2152
- if (!isRecord(raw) || !isRecord(raw.meta) || !isRecord(raw.rules)) {
2212
+ if (!(isRecord(raw) && isRecord(raw.meta) && isRecord(raw.rules))) {
2153
2213
  throw new PluginContractError(spec, "the default export must be { meta, rules }");
2154
2214
  }
2155
2215
  const { name, namespace, apiVersion } = raw.meta;
@@ -2182,7 +2242,7 @@ var toRule = (namespace, key, rule) => {
2182
2242
  } catch (err) {
2183
2243
  return report(`threw: ${message2(err)}`);
2184
2244
  }
2185
- if (!Array.isArray(found) || !found.every(validFinding)) {
2245
+ if (!(Array.isArray(found) && found.every(validFinding))) {
2186
2246
  return report("returned something other than a list of { line, message } findings");
2187
2247
  }
2188
2248
  return found.map((f) => ({
@@ -2204,7 +2264,7 @@ var toRule = (namespace, key, rule) => {
2204
2264
  if (!Array.isArray(edits))
2205
2265
  throw new PluginFixerError(id, "fixer returned something other than a list of edits");
2206
2266
  return edits.map((e) => {
2207
- if (!isRecord(e) || !isRecord(e.expect) || !EXPECTATIONS.has(e.expect.kind)) {
2267
+ if (!(isRecord(e) && isRecord(e.expect) && EXPECTATIONS.has(e.expect.kind))) {
2208
2268
  throw new PluginFixerError(id, "fixer returned an edit with no valid expectation");
2209
2269
  }
2210
2270
  return { ...e, rule: id };