@jpeakai/mermaidjs-lint 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/bin.js +283 -46
- package/dist/index.js +285 -46
- package/dist/testing.js +262 -39
- package/dist/types/check.d.ts +2 -0
- package/dist/types/errors.d.ts +4 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/parse/contrast.d.ts +34 -4
- package/dist/types/parse/stats.d.ts +13 -0
- package/dist/types/plugins/runtime.d.ts +1 -1
- package/dist/types/rules/shared.d.ts +7 -2
- package/dist/types/rules/types.d.ts +1 -1
- package/package.json +2 -2
- package/src/check.ts +40 -5
- package/src/cli.ts +19 -3
- package/src/errors.ts +15 -0
- package/src/index.ts +1 -0
- package/src/parse/contrast.ts +289 -20
- package/src/parse/stats.ts +14 -0
- package/src/plugins/load.ts +5 -2
- package/src/plugins/runtime.ts +3 -2
- package/src/rules/contrast-border-below-ui-minimum.ts +8 -5
- package/src/rules/contrast-directive-unauditable.ts +8 -6
- package/src/rules/contrast-text-below-aa.ts +16 -13
- package/src/rules/registry.ts +39 -2
- package/src/rules/shared.ts +51 -11
- package/src/rules/types.ts +4 -1
- package/src/testing.ts +2 -1
package/dist/testing.js
CHANGED
|
@@ -47,6 +47,14 @@ class RuleOptionError extends UsageError {
|
|
|
47
47
|
this.option = option;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
class CanvasProfileError extends UsageError {
|
|
52
|
+
file;
|
|
53
|
+
constructor(file) {
|
|
54
|
+
super(`${file}: a canvas applies under the github profile only, and this file is scored under mkdocs-material, whose page is Material's own; drop the canvas for it, or pass --profile github`, "canvas-profile");
|
|
55
|
+
this.file = file;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
50
58
|
class FixNotConvergingError extends MermaidLintError {
|
|
51
59
|
file;
|
|
52
60
|
rules;
|
|
@@ -215,6 +223,7 @@ var getMermaid = async () => {
|
|
|
215
223
|
mermaidCore = (await import("mermaid")).default;
|
|
216
224
|
return mermaidCore;
|
|
217
225
|
};
|
|
226
|
+
var getMermaidCore = async () => await getMermaid();
|
|
218
227
|
var LANGIUM_TYPES = {
|
|
219
228
|
"architecture-beta": "architecture",
|
|
220
229
|
architecture: "architecture",
|
|
@@ -860,15 +869,27 @@ var MKDOCS_MATERIAL = {
|
|
|
860
869
|
light: { bg: "#ffffff", text: "#36464e" },
|
|
861
870
|
dark: { bg: "#1e2129", text: "hsl(225 18% 86% / 0.82)" }
|
|
862
871
|
};
|
|
863
|
-
var
|
|
872
|
+
var GITHUB_CANVAS = { light: "#ffffff", dark: "#0d1117" };
|
|
864
873
|
var THEMES = ["light", "dark"];
|
|
874
|
+
var canvasColourProblem = (value) => {
|
|
875
|
+
if (typeof value !== "string")
|
|
876
|
+
return "is not a colour string";
|
|
877
|
+
let alpha;
|
|
878
|
+
try {
|
|
879
|
+
alpha = alphaOf(value);
|
|
880
|
+
} catch {
|
|
881
|
+
return "is not a CSS colour";
|
|
882
|
+
}
|
|
883
|
+
return alpha < 1 ? "is translucent, and a page canvas must be opaque" : null;
|
|
884
|
+
};
|
|
885
|
+
var resolveCanvas = (setting) => ({ ...GITHUB_CANVAS, ...setting });
|
|
865
886
|
var MERMAID_EDGE_LABEL = {
|
|
866
887
|
light: { box: "rgba(232,232,232,0.8)", backing: "rgba(232,232,232,0.5)" },
|
|
867
888
|
dark: { box: "#585858", backing: "rgba(88,88,88,0.5)" }
|
|
868
889
|
};
|
|
869
|
-
var edgeLabelBackground = (theme) => {
|
|
890
|
+
var edgeLabelBackground = (theme, canvas = GITHUB_CANVAS) => {
|
|
870
891
|
const { box, backing } = MERMAID_EDGE_LABEL[theme];
|
|
871
|
-
return compositeOver(box, compositeOver(box, compositeOver(backing,
|
|
892
|
+
return compositeOver(box, compositeOver(box, compositeOver(backing, canvas[theme])));
|
|
872
893
|
};
|
|
873
894
|
var MERMAID_ER = {
|
|
874
895
|
light: { odd: "#ffffff", even: "#f1f1ff", label: "#333333" },
|
|
@@ -890,12 +911,12 @@ var scorePair = (kind, d, fg, bg) => {
|
|
|
890
911
|
};
|
|
891
912
|
};
|
|
892
913
|
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
893
|
-
var scoreEdgeLabel = (d, pairs, skipped) => {
|
|
914
|
+
var scoreEdgeLabel = (d, canvas, pairs, skipped) => {
|
|
894
915
|
const { color } = d.properties;
|
|
895
916
|
if (!color)
|
|
896
917
|
return;
|
|
897
918
|
try {
|
|
898
|
-
pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t))));
|
|
919
|
+
pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t, canvas))));
|
|
899
920
|
} catch (err) {
|
|
900
921
|
skipped.push({
|
|
901
922
|
selector: d.selector,
|
|
@@ -905,12 +926,12 @@ var scoreEdgeLabel = (d, pairs, skipped) => {
|
|
|
905
926
|
});
|
|
906
927
|
}
|
|
907
928
|
};
|
|
908
|
-
var scoreGithub = (directives) => {
|
|
929
|
+
var scoreGithub = (directives, canvas = GITHUB_CANVAS) => {
|
|
909
930
|
const pairs = [];
|
|
910
931
|
const skipped = [];
|
|
911
932
|
for (const d of directives) {
|
|
912
933
|
if (d.kind === "linkStyle")
|
|
913
|
-
scoreEdgeLabel(d, pairs, skipped);
|
|
934
|
+
scoreEdgeLabel(d, canvas, pairs, skipped);
|
|
914
935
|
if (d.kind !== "classDef" && d.kind !== "style")
|
|
915
936
|
continue;
|
|
916
937
|
const { fill, color, stroke } = d.properties;
|
|
@@ -932,7 +953,7 @@ var scoreGithub = (directives) => {
|
|
|
932
953
|
note("fill is fully transparent, so there is no box to anchor contrast");
|
|
933
954
|
continue;
|
|
934
955
|
}
|
|
935
|
-
const boxes = alpha < 1 ? THEMES.map((t) => [compositeOver(fill,
|
|
956
|
+
const boxes = alpha < 1 ? THEMES.map((t) => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
|
|
936
957
|
if (!color) {
|
|
937
958
|
note("no color: declared beside fill:, and the github profile requires one " + "(if the host forces label text, use the mkdocs-material profile)", true);
|
|
938
959
|
}
|
|
@@ -1065,11 +1086,144 @@ var scoreEr = (directives) => {
|
|
|
1065
1086
|
}
|
|
1066
1087
|
return { pairs, skipped };
|
|
1067
1088
|
};
|
|
1068
|
-
var scoreForProfile = (directives, profile, keyword) => {
|
|
1089
|
+
var scoreForProfile = (directives, profile, keyword, canvas = GITHUB_CANVAS) => {
|
|
1069
1090
|
if (profile === "mkdocs-material")
|
|
1070
1091
|
return scoreMkdocs(directives);
|
|
1071
|
-
return keyword === "erDiagram" ? scoreEr(directives) : scoreGithub(directives);
|
|
1092
|
+
return keyword === "erDiagram" ? scoreEr(directives) : scoreGithub(directives, canvas);
|
|
1093
|
+
};
|
|
1094
|
+
var SEQUENCE_THEMES = ["base", "default", "dark", "forest", "neutral"];
|
|
1095
|
+
var LINE = { NOTE: 2, ACTIVE_START: 17, RECT_START: 22, AUTONUMBER: 26 };
|
|
1096
|
+
var FRAME_STARTS = new Set([10, 12, 15, 19, 27, 30, 32]);
|
|
1097
|
+
var sequenceFeatures = (db) => {
|
|
1098
|
+
const messages = db.getMessages();
|
|
1099
|
+
const has = (type) => messages.some((m) => m.type === type);
|
|
1100
|
+
return {
|
|
1101
|
+
humanActor: [...db.getActors().values()].some((a) => a.type === "actor"),
|
|
1102
|
+
note: has(LINE.NOTE),
|
|
1103
|
+
frame: messages.some((m) => FRAME_STARTS.has(m.type)),
|
|
1104
|
+
activation: has(LINE.ACTIVE_START),
|
|
1105
|
+
numbered: messages.some((m) => m.type === LINE.AUTONUMBER && m.message?.visible !== false),
|
|
1106
|
+
rects: messages.filter((m) => m.type === LINE.RECT_START).map((m) => String(m.message))
|
|
1107
|
+
};
|
|
1108
|
+
};
|
|
1109
|
+
var resolveVariables = (mermaid, config, themes) => {
|
|
1110
|
+
const prior = mermaid.mermaidAPI.getSiteConfig();
|
|
1111
|
+
try {
|
|
1112
|
+
const out = {};
|
|
1113
|
+
for (const t of THEMES) {
|
|
1114
|
+
mermaid.initialize({ ...structuredClone(config), theme: themes[t] });
|
|
1115
|
+
out[t] = { ...mermaid.mermaidAPI.getSiteConfig().themeVariables };
|
|
1116
|
+
}
|
|
1117
|
+
return out;
|
|
1118
|
+
} finally {
|
|
1119
|
+
mermaid.initialize(prior);
|
|
1120
|
+
}
|
|
1072
1121
|
};
|
|
1122
|
+
var themeVariable = (selector, line) => ({
|
|
1123
|
+
kind: "themeVariables",
|
|
1124
|
+
selector,
|
|
1125
|
+
properties: {},
|
|
1126
|
+
spans: {},
|
|
1127
|
+
line
|
|
1128
|
+
});
|
|
1129
|
+
var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
|
|
1130
|
+
const pairs = [];
|
|
1131
|
+
const skipped = [];
|
|
1132
|
+
const lines = source.split(`
|
|
1133
|
+
`);
|
|
1134
|
+
const keywordLine = lines.findIndex((l) => /^\s*sequenceDiagram\b/.test(l)) + 1;
|
|
1135
|
+
const lineOf = (key) => {
|
|
1136
|
+
const found = lines.findLastIndex((l) => new RegExp(`\\b${key}["']?\\s*:`).test(l));
|
|
1137
|
+
return found < 0 ? keywordLine : found + 1;
|
|
1138
|
+
};
|
|
1139
|
+
const rectLines = lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []);
|
|
1140
|
+
const mermaid = await getMermaidCore();
|
|
1141
|
+
let config;
|
|
1142
|
+
let db;
|
|
1143
|
+
try {
|
|
1144
|
+
config = (await mermaid.parse(source)).config;
|
|
1145
|
+
db = (await mermaid.mermaidAPI.getDiagramFromText(source)).db;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return { pairs, skipped };
|
|
1148
|
+
}
|
|
1149
|
+
const block = (selector, reason) => {
|
|
1150
|
+
skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
|
|
1151
|
+
return { pairs, skipped };
|
|
1152
|
+
};
|
|
1153
|
+
if (config.themeCSS !== undefined)
|
|
1154
|
+
return block("themeCSS", "themeCSS restyles the diagram with CSS the audit cannot model");
|
|
1155
|
+
const pinned = config.theme;
|
|
1156
|
+
if (pinned !== undefined && !SEQUENCE_THEMES.includes(String(pinned))) {
|
|
1157
|
+
return block("theme", `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`);
|
|
1158
|
+
}
|
|
1159
|
+
const features = sequenceFeatures(db);
|
|
1160
|
+
const host = profile === "mkdocs-material";
|
|
1161
|
+
const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
|
|
1162
|
+
let variables;
|
|
1163
|
+
try {
|
|
1164
|
+
variables = resolveVariables(mermaid, config, { light: theme("default"), dark: theme(host ? "default" : "dark") });
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
return block("themeVariables", `Mermaid could not resolve the theme: ${message(err)}`);
|
|
1167
|
+
}
|
|
1168
|
+
for (const t of THEMES) {
|
|
1169
|
+
const page = host ? MKDOCS_MATERIAL[t].bg : canvas[t];
|
|
1170
|
+
const vars = variables[t];
|
|
1171
|
+
const color = (key) => {
|
|
1172
|
+
const value = vars[key];
|
|
1173
|
+
if (typeof value !== "string")
|
|
1174
|
+
throw new Error(`${key} is not a resolved colour`);
|
|
1175
|
+
return value;
|
|
1176
|
+
};
|
|
1177
|
+
const pair = (kind, fgKey, bg, suffix = "", at) => {
|
|
1178
|
+
const selector = fgKey + suffix;
|
|
1179
|
+
const line = at ?? lineOf(fgKey);
|
|
1180
|
+
try {
|
|
1181
|
+
const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
|
|
1182
|
+
pairs.push({ ...scoreHostPair(kind, themeVariable(selector, line), t, color(fgKey), box), advisory: host });
|
|
1183
|
+
} catch (err) {
|
|
1184
|
+
skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
const onPage = { colour: page };
|
|
1188
|
+
if (host) {
|
|
1189
|
+
pair("border", "actorLineColor", onPage);
|
|
1190
|
+
pair("border", "signalColor", onPage, " (arrowhead)");
|
|
1191
|
+
if (features.activation)
|
|
1192
|
+
pair("border", "activationBorderColor", { key: "activationBkgColor" });
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
pair("text", "actorTextColor", { key: "actorBkg" });
|
|
1196
|
+
pair("border", "actorBorder", { key: "actorBkg" });
|
|
1197
|
+
if (features.humanActor) {
|
|
1198
|
+
pair("text", "actorTextColor", onPage, " (human actor label)");
|
|
1199
|
+
pair("border", "actorBorder", onPage, " (human actor glyph)");
|
|
1200
|
+
}
|
|
1201
|
+
pair("border", "actorLineColor", onPage);
|
|
1202
|
+
pair("text", "signalTextColor", onPage);
|
|
1203
|
+
pair("border", "signalColor", onPage);
|
|
1204
|
+
if (features.note)
|
|
1205
|
+
pair("text", "noteTextColor", { key: "noteBkgColor" });
|
|
1206
|
+
if (features.frame) {
|
|
1207
|
+
pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
|
|
1208
|
+
pair("text", "loopTextColor", onPage);
|
|
1209
|
+
pair("border", "labelBoxBorderColor", onPage, " (loop frame)");
|
|
1210
|
+
}
|
|
1211
|
+
if (features.activation)
|
|
1212
|
+
pair("border", "activationBorderColor", { key: "activationBkgColor" });
|
|
1213
|
+
if (features.numbered)
|
|
1214
|
+
pair("text", "sequenceNumberColor", { key: "signalColor" });
|
|
1215
|
+
for (const [i, rect] of features.rects.entries()) {
|
|
1216
|
+
const fill = { colour: rect };
|
|
1217
|
+
const at = rectLines[i];
|
|
1218
|
+
pair("text", "signalTextColor", fill, " (rect)", at);
|
|
1219
|
+
pair("border", "signalColor", fill, " (rect)", at);
|
|
1220
|
+
if (features.frame)
|
|
1221
|
+
pair("text", "loopTextColor", fill, " (rect)", at);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return { pairs, skipped };
|
|
1225
|
+
};
|
|
1226
|
+
var scoreDiagram = (source, directives, profile, keyword, canvas = GITHUB_CANVAS) => keyword === "sequenceDiagram" ? scoreSequence(source, profile, canvas) : Promise.resolve(scoreForProfile(directives, profile, keyword, canvas));
|
|
1073
1227
|
var detectProfile = (filePath) => {
|
|
1074
1228
|
let dir = dirname(resolve(filePath));
|
|
1075
1229
|
for (let i = 0;i < 20; i++) {
|
|
@@ -1233,7 +1387,7 @@ var COMPLEXITY_OPTIONS = {
|
|
|
1233
1387
|
subgraphWeight: "number",
|
|
1234
1388
|
depthWeight: "number"
|
|
1235
1389
|
};
|
|
1236
|
-
var CONTRAST_OPTIONS = { profile: "string" };
|
|
1390
|
+
var CONTRAST_OPTIONS = { profile: "string", canvas: "canvas" };
|
|
1237
1391
|
var measure = async (diagram, config) => {
|
|
1238
1392
|
const stats = await diagram.stats();
|
|
1239
1393
|
if (parseFailed(diagram.source, stats))
|
|
@@ -1263,17 +1417,35 @@ var profileOf = (ctx) => {
|
|
|
1263
1417
|
}
|
|
1264
1418
|
return wanted === "auto" ? detectProfile(ctx.file) : wanted;
|
|
1265
1419
|
};
|
|
1420
|
+
var settingsOf = (ctx) => {
|
|
1421
|
+
const profile = profileOf(ctx);
|
|
1422
|
+
const setting = ctx.options.canvas;
|
|
1423
|
+
if (setting === undefined)
|
|
1424
|
+
return { profile, canvas: GITHUB_CANVAS };
|
|
1425
|
+
for (const side of THEMES) {
|
|
1426
|
+
if (!(side in setting))
|
|
1427
|
+
continue;
|
|
1428
|
+
const problem = canvasColourProblem(setting[side]);
|
|
1429
|
+
if (problem)
|
|
1430
|
+
throw new Error(`canvas.${side} ${problem}: ${JSON.stringify(setting[side])}`);
|
|
1431
|
+
}
|
|
1432
|
+
if (profile === "mkdocs-material")
|
|
1433
|
+
throw new CanvasProfileError(ctx.file);
|
|
1434
|
+
return { profile, canvas: resolveCanvas(setting) };
|
|
1435
|
+
};
|
|
1266
1436
|
var scored = new WeakMap;
|
|
1267
|
-
var contrastOf = (diagram,
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1437
|
+
var contrastOf = (diagram, settings) => {
|
|
1438
|
+
const { profile, canvas } = settings;
|
|
1439
|
+
const key = `${profile}\x00${canvas.light}\x00${canvas.dark}`;
|
|
1440
|
+
let bySettings = scored.get(diagram);
|
|
1441
|
+
if (!bySettings) {
|
|
1442
|
+
bySettings = new Map;
|
|
1443
|
+
scored.set(diagram, bySettings);
|
|
1272
1444
|
}
|
|
1273
|
-
let result =
|
|
1445
|
+
let result = bySettings.get(key);
|
|
1274
1446
|
if (!result) {
|
|
1275
|
-
result =
|
|
1276
|
-
|
|
1447
|
+
result = scoreDiagram(diagram.source, diagram.directives, profile, diagram.keyword, canvas);
|
|
1448
|
+
bySettings.set(key, result);
|
|
1277
1449
|
}
|
|
1278
1450
|
return result;
|
|
1279
1451
|
};
|
|
@@ -1384,11 +1556,11 @@ var contrastBorderBelowUiMinimum = {
|
|
|
1384
1556
|
category: "contrast",
|
|
1385
1557
|
summary: "a border below 3:1 against its fill",
|
|
1386
1558
|
options: CONTRAST_OPTIONS,
|
|
1387
|
-
check: (ctx) => {
|
|
1388
|
-
const
|
|
1559
|
+
check: async (ctx) => {
|
|
1560
|
+
const settings = settingsOf(ctx);
|
|
1389
1561
|
const found = [];
|
|
1390
1562
|
for (const diagram of ctx.doc.diagrams) {
|
|
1391
|
-
for (const p of contrastOf(diagram,
|
|
1563
|
+
for (const p of (await contrastOf(diagram, settings)).pairs) {
|
|
1392
1564
|
if (p.kind !== "border" || p.passes || p.advisory)
|
|
1393
1565
|
continue;
|
|
1394
1566
|
const line = fileLine(diagram, p.line);
|
|
@@ -1398,7 +1570,7 @@ var contrastBorderBelowUiMinimum = {
|
|
|
1398
1570
|
endLine: line,
|
|
1399
1571
|
rule: RULE.BORDER,
|
|
1400
1572
|
message: `${p.directive.kind} ${p.selector}: border ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NON_TEXT2} ` + `(${p.foreground} on ${p.background}${p.theme ? `, ${p.theme}` : ""})`,
|
|
1401
|
-
remediation: "Darken the stroke or lighten the fill until the ratio reaches 3:1."
|
|
1573
|
+
remediation: p.directive.kind === "themeVariables" ? "Set the theme variable to a colour that reaches 3:1 on its background in both themes, or remove it and let the theme set it." : "Darken the stroke or lighten the fill until the ratio reaches 3:1."
|
|
1402
1574
|
});
|
|
1403
1575
|
}
|
|
1404
1576
|
}
|
|
@@ -1412,11 +1584,11 @@ var contrastDirectiveUnauditable = {
|
|
|
1412
1584
|
category: "contrast",
|
|
1413
1585
|
summary: "a style directive whose contrast cannot be audited",
|
|
1414
1586
|
options: CONTRAST_OPTIONS,
|
|
1415
|
-
check: (ctx) => {
|
|
1416
|
-
const
|
|
1587
|
+
check: async (ctx) => {
|
|
1588
|
+
const settings = settingsOf(ctx);
|
|
1417
1589
|
const found = [];
|
|
1418
1590
|
for (const diagram of ctx.doc.diagrams) {
|
|
1419
|
-
for (const s of contrastOf(diagram,
|
|
1591
|
+
for (const s of (await contrastOf(diagram, settings)).skipped) {
|
|
1420
1592
|
if (!s.blocking)
|
|
1421
1593
|
continue;
|
|
1422
1594
|
const line = fileLine(diagram, s.line);
|
|
@@ -1426,7 +1598,7 @@ var contrastDirectiveUnauditable = {
|
|
|
1426
1598
|
endLine: line,
|
|
1427
1599
|
rule: RULE.UNAUDITABLE,
|
|
1428
1600
|
message: `${s.selector}: unaudited, ${s.reason}`,
|
|
1429
|
-
remediation: "Declare the missing property
|
|
1601
|
+
remediation: "Declare the missing property, fix the colour, or remove the config the audit cannot model, so the pair can be scored."
|
|
1430
1602
|
});
|
|
1431
1603
|
}
|
|
1432
1604
|
}
|
|
@@ -1443,11 +1615,11 @@ var contrastTextBelowAa = {
|
|
|
1443
1615
|
category: "contrast",
|
|
1444
1616
|
summary: "label text below 4.5:1 against its fill",
|
|
1445
1617
|
options: CONTRAST_OPTIONS,
|
|
1446
|
-
check: (ctx) => {
|
|
1447
|
-
const
|
|
1618
|
+
check: async (ctx) => {
|
|
1619
|
+
const settings = settingsOf(ctx);
|
|
1448
1620
|
const found = [];
|
|
1449
1621
|
for (const diagram of ctx.doc.diagrams) {
|
|
1450
|
-
for (const p of contrastOf(diagram,
|
|
1622
|
+
for (const p of (await contrastOf(diagram, settings)).pairs) {
|
|
1451
1623
|
if (p.kind !== "text" || p.passes)
|
|
1452
1624
|
continue;
|
|
1453
1625
|
const where = [p.theme, p.row].filter(Boolean).join("/");
|
|
@@ -1458,19 +1630,19 @@ var contrastTextBelowAa = {
|
|
|
1458
1630
|
endLine: line,
|
|
1459
1631
|
rule: RULE.TEXT,
|
|
1460
1632
|
message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
|
|
1461
|
-
remediation: profile === "mkdocs-material" ? "The host theme forces the label text, so change the fill until the host text reads on it in both themes." : p.directive.kind === "linkStyle" ? "An edge label sits on Mermaid's own label box, light in one theme and grey in the other, and no one colour reads on both, so drop color: from the linkStyle and let the theme set the label text." : "Darken or lighten the fill, or change color:, until the ratio reaches 4.5:1 in every theme."
|
|
1633
|
+
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."
|
|
1462
1634
|
});
|
|
1463
1635
|
}
|
|
1464
1636
|
}
|
|
1465
1637
|
return found;
|
|
1466
1638
|
},
|
|
1467
|
-
fix: (ctx) => {
|
|
1468
|
-
const
|
|
1469
|
-
if (profile === "mkdocs-material")
|
|
1639
|
+
fix: async (ctx) => {
|
|
1640
|
+
const settings = settingsOf(ctx);
|
|
1641
|
+
if (settings.profile === "mkdocs-material")
|
|
1470
1642
|
return [];
|
|
1471
1643
|
const edits = [];
|
|
1472
1644
|
for (const diagram of ctx.doc.diagrams) {
|
|
1473
|
-
const { pairs } = contrastOf(diagram,
|
|
1645
|
+
const { pairs } = await contrastOf(diagram, settings);
|
|
1474
1646
|
const failing = new Set(pairs.filter((p) => p.kind === "text" && !p.passes).map((p) => p.directive));
|
|
1475
1647
|
for (const directive of failing) {
|
|
1476
1648
|
const span = directive.spans.color;
|
|
@@ -1590,8 +1762,27 @@ var typeOf = (value) => {
|
|
|
1590
1762
|
return "boolean";
|
|
1591
1763
|
if (Array.isArray(value) && value.every((v) => typeof v === "string"))
|
|
1592
1764
|
return "string[]";
|
|
1765
|
+
if (isCanvasShape(value))
|
|
1766
|
+
return "canvas";
|
|
1593
1767
|
return null;
|
|
1594
1768
|
};
|
|
1769
|
+
var isCanvasShape = (value) => {
|
|
1770
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1771
|
+
return false;
|
|
1772
|
+
const keys = Object.keys(value);
|
|
1773
|
+
return keys.length > 0 && keys.every((k) => THEMES.includes(k));
|
|
1774
|
+
};
|
|
1775
|
+
var describeType = (want) => want === "canvas" ? 'an object such as { "light": "#ffffff", "dark": "#0d1117" }' : `a ${want}`;
|
|
1776
|
+
var checkCanvas = (where, rule, name, value) => {
|
|
1777
|
+
for (const side of THEMES) {
|
|
1778
|
+
if (!(side in value))
|
|
1779
|
+
continue;
|
|
1780
|
+
const problem = canvasColourProblem(value[side]);
|
|
1781
|
+
if (problem) {
|
|
1782
|
+
throw new RuleOptionError(where, rule.id, name, `option "${name}.${side}" of "${rule.id}" ${problem}: ${JSON.stringify(value[side])}`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1595
1786
|
var checkOptions = (where, rule, options) => {
|
|
1596
1787
|
const accepted = Object.keys(rule.options ?? {});
|
|
1597
1788
|
for (const [name, value] of Object.entries(options)) {
|
|
@@ -1601,8 +1792,13 @@ var checkOptions = (where, rule, options) => {
|
|
|
1601
1792
|
throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
|
|
1602
1793
|
}
|
|
1603
1794
|
if (typeOf(value) !== want) {
|
|
1604
|
-
throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be
|
|
1795
|
+
throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be ${describeType(want)}`);
|
|
1605
1796
|
}
|
|
1797
|
+
if (want === "canvas")
|
|
1798
|
+
checkCanvas(where, rule, name, value);
|
|
1799
|
+
}
|
|
1800
|
+
if (options.profile === "mkdocs-material" && options.canvas !== undefined && rule.options?.canvas === "canvas") {
|
|
1801
|
+
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`);
|
|
1606
1802
|
}
|
|
1607
1803
|
if (typeof options.preset === "string" && canonicalPreset(options.preset) === null) {
|
|
1608
1804
|
throw new RuleOptionError(where, rule.id, "preset", `option "preset" of "${rule.id}" is not a known preset`);
|
|
@@ -1646,8 +1842,24 @@ var buildRegistry = (input) => {
|
|
|
1646
1842
|
var BUILTIN = buildRegistry({ rules: RULES, fixOrder: FIX_ORDER, categories: BUILTIN_CATEGORIES });
|
|
1647
1843
|
|
|
1648
1844
|
// src/check.ts
|
|
1845
|
+
var canvasName = (side, prefix) => prefix ? `${prefix}canvas-${side}` : `canvas.${side}`;
|
|
1649
1846
|
var checkRunFlags = (flags, prefix = "") => {
|
|
1650
|
-
const { preset, profile } = flags;
|
|
1847
|
+
const { preset, profile, canvas } = flags;
|
|
1848
|
+
for (const side of THEMES) {
|
|
1849
|
+
if (canvas?.[side] === undefined)
|
|
1850
|
+
continue;
|
|
1851
|
+
const problem = canvasColourProblem(canvas[side]);
|
|
1852
|
+
if (problem) {
|
|
1853
|
+
const name = canvasName(side, prefix);
|
|
1854
|
+
throw new FlagError(name, `${name} ${problem}: "${canvas[side]}"; give an opaque CSS colour, such as #ffffff`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
const sides = THEMES.filter((side) => canvas?.[side] !== undefined);
|
|
1858
|
+
const first = sides[0];
|
|
1859
|
+
if (profile === "mkdocs-material" && first !== undefined) {
|
|
1860
|
+
const name = canvasName(first, prefix);
|
|
1861
|
+
throw new FlagError(name, `${name} applies under the github profile only; mkdocs-material scores Material's own page colours`);
|
|
1862
|
+
}
|
|
1651
1863
|
if (preset !== undefined && canonicalPreset(preset) === null) {
|
|
1652
1864
|
throw new FlagError(`${prefix}preset`, `${prefix}preset must be one of ${PRESET_NAMES.join(", ")}, not "${preset}"`);
|
|
1653
1865
|
}
|
|
@@ -1664,6 +1876,17 @@ var optionsFor = (rule, configured, flags) => {
|
|
|
1664
1876
|
}
|
|
1665
1877
|
if (typeof options.preset === "string")
|
|
1666
1878
|
options.preset = canonicalPreset(options.preset) ?? options.preset;
|
|
1879
|
+
const canvas = flags.canvas;
|
|
1880
|
+
if (canvas && THEMES.some((side) => canvas[side] !== undefined) && rule.options?.canvas === "canvas") {
|
|
1881
|
+
const configured2 = typeof options.canvas === "object" && options.canvas !== null ? options.canvas : {};
|
|
1882
|
+
const merged = { ...configured2 };
|
|
1883
|
+
for (const side of THEMES) {
|
|
1884
|
+
const value = canvas[side];
|
|
1885
|
+
if (value !== undefined)
|
|
1886
|
+
merged[side] = value;
|
|
1887
|
+
}
|
|
1888
|
+
options.canvas = merged;
|
|
1889
|
+
}
|
|
1667
1890
|
return options;
|
|
1668
1891
|
};
|
|
1669
1892
|
var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
|
|
@@ -1829,7 +2052,7 @@ var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
|
1829
2052
|
// src/plugins/load.ts
|
|
1830
2053
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1831
2054
|
var message2 = (err) => err instanceof Error ? err.message : String(err);
|
|
1832
|
-
var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]"]);
|
|
2055
|
+
var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]", "canvas"]);
|
|
1833
2056
|
var RESERVED_OPTIONS = ["preset", "profile"];
|
|
1834
2057
|
var checkRule = (spec, key, raw, own) => {
|
|
1835
2058
|
const where = `rule "${key}"`;
|
|
@@ -1858,7 +2081,7 @@ var checkRule = (spec, key, raw, own) => {
|
|
|
1858
2081
|
if (options !== undefined) {
|
|
1859
2082
|
const valid = isRecord(options) && Object.values(options).every((t) => OPTION_TYPES.has(t));
|
|
1860
2083
|
if (!valid) {
|
|
1861
|
-
throw new PluginContractError(spec, `${where} options must map names to number, string, boolean
|
|
2084
|
+
throw new PluginContractError(spec, `${where} options must map names to number, string, boolean, string[] or canvas`);
|
|
1862
2085
|
}
|
|
1863
2086
|
for (const name of RESERVED_OPTIONS) {
|
|
1864
2087
|
if (name in options && options[name] !== "string") {
|
package/dist/types/check.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type DocModel } from "./model.ts";
|
|
2
|
+
import { type CanvasSetting } from "./parse/contrast.ts";
|
|
2
3
|
import { type Registry } from "./rules/registry.ts";
|
|
3
4
|
import type { Finding, Rule, RuleOptions } from "./rules/types.ts";
|
|
4
5
|
export interface RunFlags {
|
|
5
6
|
preset?: string;
|
|
6
7
|
profile?: string;
|
|
8
|
+
canvas?: CanvasSetting;
|
|
7
9
|
}
|
|
8
10
|
export declare const checkRunFlags: (flags: RunFlags, prefix?: string) => void;
|
|
9
11
|
export declare const optionsFor: (rule: Rule, configured: RuleOptions | undefined, flags: RunFlags) => RuleOptions;
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ export declare class RuleOptionError extends UsageError {
|
|
|
47
47
|
readonly option: string;
|
|
48
48
|
constructor(file: string, rule: string, option: string, message: string);
|
|
49
49
|
}
|
|
50
|
+
export declare class CanvasProfileError extends UsageError {
|
|
51
|
+
readonly file: string;
|
|
52
|
+
constructor(file: string);
|
|
53
|
+
}
|
|
50
54
|
export declare class FixOutsideRootError extends UsageError {
|
|
51
55
|
readonly file: string;
|
|
52
56
|
readonly target: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { type Collected, collectFiles } from "./files.ts";
|
|
|
6
6
|
export { type FixReport, fixSource, fixSourceReport } from "./fix/engine.ts";
|
|
7
7
|
export { buildDocModel, DIAGRAM_EXTS, type Diagram, type DocModel, documentKind } from "./model.ts";
|
|
8
8
|
export type { ContrastAssessment, StyleDirective, ValueSpan } from "./parse/color.ts";
|
|
9
|
+
export { type Canvas, type CanvasSetting, GITHUB_CANVAS } from "./parse/contrast.ts";
|
|
9
10
|
export { PRESETS, type Preset, type ThresholdConfig, thresholdsFrom } from "./parse/metrics.ts";
|
|
10
11
|
export { type DiagramStats, detectKeyword, extractStats } from "./parse/stats.ts";
|
|
11
12
|
export type { Plugin, PluginEdit, PluginFinding, PluginMeta, PluginRule } from "./plugins/contract.ts";
|
|
@@ -4,11 +4,20 @@ export type Theme = "light" | "dark";
|
|
|
4
4
|
export type PairKind = "text" | "border";
|
|
5
5
|
export type ErRow = "odd" | "even";
|
|
6
6
|
export declare const PROFILES: readonly ["auto", "github", "mkdocs-material"];
|
|
7
|
-
export
|
|
7
|
+
export type Canvas = Readonly<Record<Theme, string>>;
|
|
8
|
+
export type CanvasSetting = Readonly<Partial<Record<Theme, string>>>;
|
|
9
|
+
export declare const GITHUB_CANVAS: Canvas;
|
|
10
|
+
export declare const THEMES: readonly Theme[];
|
|
11
|
+
export declare const canvasColourProblem: (value: unknown) => string | null;
|
|
12
|
+
export declare const resolveCanvas: (setting: CanvasSetting | undefined) => Canvas;
|
|
13
|
+
export declare const edgeLabelBackground: (theme: Theme, canvas?: Canvas) => string;
|
|
14
|
+
export type ContrastSource = StyleDirective | (Omit<StyleDirective, "kind"> & {
|
|
15
|
+
kind: "themeVariables";
|
|
16
|
+
});
|
|
8
17
|
export interface ContrastPair {
|
|
9
18
|
kind: PairKind;
|
|
10
19
|
selector: string;
|
|
11
|
-
directive:
|
|
20
|
+
directive: ContrastSource;
|
|
12
21
|
foreground: string;
|
|
13
22
|
background: string;
|
|
14
23
|
line: number;
|
|
@@ -28,8 +37,29 @@ export interface ContrastResult {
|
|
|
28
37
|
pairs: ContrastPair[];
|
|
29
38
|
skipped: SkippedDirective[];
|
|
30
39
|
}
|
|
31
|
-
export declare const scoreGithub: (directives: StyleDirective[]) => ContrastResult;
|
|
40
|
+
export declare const scoreGithub: (directives: StyleDirective[], canvas?: Canvas) => ContrastResult;
|
|
32
41
|
export declare const scoreMkdocs: (directives: StyleDirective[]) => ContrastResult;
|
|
33
42
|
export declare const scoreEr: (directives: StyleDirective[]) => ContrastResult;
|
|
34
|
-
export declare const scoreForProfile: (directives: StyleDirective[], profile: Profile, keyword: string | null) => ContrastResult;
|
|
43
|
+
export declare const scoreForProfile: (directives: StyleDirective[], profile: Profile, keyword: string | null, canvas?: Canvas) => ContrastResult;
|
|
44
|
+
interface SequenceDb {
|
|
45
|
+
getActors: () => Map<string, {
|
|
46
|
+
type: string;
|
|
47
|
+
}>;
|
|
48
|
+
getMessages: () => Array<{
|
|
49
|
+
type: number;
|
|
50
|
+
message: unknown;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
export interface SequenceFeatures {
|
|
54
|
+
humanActor: boolean;
|
|
55
|
+
note: boolean;
|
|
56
|
+
frame: boolean;
|
|
57
|
+
activation: boolean;
|
|
58
|
+
numbered: boolean;
|
|
59
|
+
rects: string[];
|
|
60
|
+
}
|
|
61
|
+
export declare const sequenceFeatures: (db: SequenceDb) => SequenceFeatures;
|
|
62
|
+
export declare const scoreSequence: (source: string, profile: Profile, canvas?: Canvas) => Promise<ContrastResult>;
|
|
63
|
+
export declare const scoreDiagram: (source: string, directives: StyleDirective[], profile: Profile, keyword: string | null, canvas?: Canvas) => Promise<ContrastResult>;
|
|
35
64
|
export declare const detectProfile: (filePath: string) => Profile;
|
|
65
|
+
export {};
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
export interface MermaidCore {
|
|
2
|
+
parse: (text: string) => Promise<{
|
|
3
|
+
config: Record<string, unknown>;
|
|
4
|
+
}>;
|
|
5
|
+
initialize: (config: Record<string, unknown>) => void;
|
|
6
|
+
mermaidAPI: {
|
|
7
|
+
getSiteConfig: () => Record<string, unknown>;
|
|
8
|
+
getDiagramFromText: (text: string) => Promise<{
|
|
9
|
+
db: unknown;
|
|
10
|
+
}>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare const getMermaidCore: () => Promise<MermaidCore>;
|
|
1
14
|
export interface DiagramStats {
|
|
2
15
|
nodes: number;
|
|
3
16
|
edges: number;
|
|
@@ -3,4 +3,4 @@ export interface RuntimeInfo {
|
|
|
3
3
|
features: Record<string, unknown>;
|
|
4
4
|
}
|
|
5
5
|
export declare const canImportTypeScript: (runtime?: RuntimeInfo) => boolean;
|
|
6
|
-
export declare const TYPESCRIPT_HELP = "TypeScript cannot be imported by this runtime. Run with Bun (bunx --bun @jpeakai/mermaidjs-lint), use Node 22.18 or newer, or write a .js or .mjs file";
|
|
6
|
+
export declare const TYPESCRIPT_HELP = "TypeScript cannot be imported by this runtime. Run with Bun (bunx --bun @jpeakai/mermaidjs-lint), use Node 22.18 or newer (23.6 or newer on Node 23), or write a .js or .mjs file";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Diagram } from "../model.ts";
|
|
2
|
-
import { type ContrastResult, type Profile } from "../parse/contrast.ts";
|
|
2
|
+
import { type Canvas, type ContrastResult, type Profile } from "../parse/contrast.ts";
|
|
3
3
|
import { type ComplexityMetrics, type ThresholdConfig } from "../parse/metrics.ts";
|
|
4
4
|
import type { DiagramStats } from "../parse/stats.ts";
|
|
5
5
|
import type { Finding, OptionSpec, RuleContext, RuleId } from "./types.ts";
|
|
@@ -14,5 +14,10 @@ export declare const finding: (ctx: RuleContext, diagram: Diagram, rule: RuleId,
|
|
|
14
14
|
export declare const boundaryHint: (boundaries: string[]) => string;
|
|
15
15
|
export declare const splitTarget: (c: ThresholdConfig) => string;
|
|
16
16
|
export declare const profileOf: (ctx: RuleContext) => Profile;
|
|
17
|
-
export
|
|
17
|
+
export interface ContrastSettings {
|
|
18
|
+
profile: Profile;
|
|
19
|
+
canvas: Canvas;
|
|
20
|
+
}
|
|
21
|
+
export declare const settingsOf: (ctx: RuleContext) => ContrastSettings;
|
|
22
|
+
export declare const contrastOf: (diagram: Diagram, settings: ContrastSettings) => Promise<ContrastResult>;
|
|
18
23
|
export declare const fileLine: (diagram: Diagram, line: number) => number;
|
|
@@ -27,7 +27,7 @@ export interface Finding {
|
|
|
27
27
|
message: string;
|
|
28
28
|
remediation?: string;
|
|
29
29
|
}
|
|
30
|
-
export type OptionType = "number" | "string" | "boolean" | "string[]";
|
|
30
|
+
export type OptionType = "number" | "string" | "boolean" | "string[]" | "canvas";
|
|
31
31
|
export type OptionSpec = Readonly<Record<string, OptionType>>;
|
|
32
32
|
export type RuleOptions = Readonly<Record<string, unknown>>;
|
|
33
33
|
export interface Helpers {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jpeakai/mermaidjs-lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"mdast-util-from-markdown": "^2.0.3",
|
|
59
59
|
"mdast-util-frontmatter": "^2.0.1",
|
|
60
60
|
"mdast-util-gfm": "^3.1.0",
|
|
61
|
-
"mermaid": "
|
|
61
|
+
"mermaid": "11.17.2",
|
|
62
62
|
"micromark-extension-frontmatter": "^2.0.0",
|
|
63
63
|
"micromark-extension-gfm": "^3.0.0"
|
|
64
64
|
},
|