@jpeakai/mermaidjs-lint 0.7.1 → 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/README.md +20 -21
- package/dist/bin.js +691 -578
- package/dist/index.js +691 -578
- package/dist/testing.js +471 -411
- package/dist/types/parse/contrast.d.ts +6 -1
- package/dist/types/parse/graph.d.ts +16 -0
- package/dist/types/rules/shared.d.ts +5 -1
- package/package.json +1 -1
- package/src/check.ts +13 -11
- package/src/cli.ts +153 -90
- package/src/files.ts +23 -18
- package/src/fix/engine.ts +82 -57
- package/src/fix/verify.ts +13 -8
- package/src/model.ts +1 -1
- package/src/parse/color.ts +1 -1
- package/src/parse/contrast.ts +308 -228
- package/src/parse/graph.ts +98 -0
- package/src/parse/metrics.ts +1 -1
- package/src/parse/stats.ts +111 -82
- package/src/plugins/discover.ts +13 -15
- package/src/plugins/index.ts +66 -37
- package/src/plugins/load.ts +94 -61
- package/src/rules/complexity.ts +1 -1
- package/src/rules/contrast-text-below-aa.ts +34 -21
- package/src/rules/parse-diagram-unparseable.ts +1 -1
- package/src/rules/parse-fence-unterminated.ts +5 -7
- package/src/rules/registry.ts +38 -16
- package/src/rules/shared.ts +8 -3
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
|
|
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())
|
|
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
|
|
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
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
|
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,
|
|
936
|
-
if (d.kind
|
|
937
|
-
|
|
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
|
|
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
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1020
|
+
out.pairs.push(scoreHostPair("border", d, theme, { fg: stroke, bg: box }));
|
|
1023
1021
|
} catch (err) {
|
|
1024
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1057
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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 =
|
|
1146
|
-
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
|
|
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
|
|
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,
|
|
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
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
|
1243
|
+
return out;
|
|
1226
1244
|
};
|
|
1227
|
-
var scoreDiagram = (source, directives,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1626
|
-
|
|
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,
|
|
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
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
const
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
|
1829
|
-
const
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
-
|
|
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 (
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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 };
|