@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/index.js
CHANGED
|
@@ -101,6 +101,14 @@ class RuleOptionError extends UsageError {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
class CanvasProfileError extends UsageError {
|
|
105
|
+
file;
|
|
106
|
+
constructor(file) {
|
|
107
|
+
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");
|
|
108
|
+
this.file = file;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
104
112
|
class FixOutsideRootError extends UsageError {
|
|
105
113
|
file;
|
|
106
114
|
target;
|
|
@@ -317,6 +325,7 @@ var getMermaid = async () => {
|
|
|
317
325
|
mermaidCore = (await import("mermaid")).default;
|
|
318
326
|
return mermaidCore;
|
|
319
327
|
};
|
|
328
|
+
var getMermaidCore = async () => await getMermaid();
|
|
320
329
|
var LANGIUM_TYPES = {
|
|
321
330
|
"architecture-beta": "architecture",
|
|
322
331
|
architecture: "architecture",
|
|
@@ -962,15 +971,27 @@ var MKDOCS_MATERIAL = {
|
|
|
962
971
|
light: { bg: "#ffffff", text: "#36464e" },
|
|
963
972
|
dark: { bg: "#1e2129", text: "hsl(225 18% 86% / 0.82)" }
|
|
964
973
|
};
|
|
965
|
-
var
|
|
974
|
+
var GITHUB_CANVAS = { light: "#ffffff", dark: "#0d1117" };
|
|
966
975
|
var THEMES = ["light", "dark"];
|
|
976
|
+
var canvasColourProblem = (value) => {
|
|
977
|
+
if (typeof value !== "string")
|
|
978
|
+
return "is not a colour string";
|
|
979
|
+
let alpha;
|
|
980
|
+
try {
|
|
981
|
+
alpha = alphaOf(value);
|
|
982
|
+
} catch {
|
|
983
|
+
return "is not a CSS colour";
|
|
984
|
+
}
|
|
985
|
+
return alpha < 1 ? "is translucent, and a page canvas must be opaque" : null;
|
|
986
|
+
};
|
|
987
|
+
var resolveCanvas = (setting) => ({ ...GITHUB_CANVAS, ...setting });
|
|
967
988
|
var MERMAID_EDGE_LABEL = {
|
|
968
989
|
light: { box: "rgba(232,232,232,0.8)", backing: "rgba(232,232,232,0.5)" },
|
|
969
990
|
dark: { box: "#585858", backing: "rgba(88,88,88,0.5)" }
|
|
970
991
|
};
|
|
971
|
-
var edgeLabelBackground = (theme) => {
|
|
992
|
+
var edgeLabelBackground = (theme, canvas = GITHUB_CANVAS) => {
|
|
972
993
|
const { box, backing } = MERMAID_EDGE_LABEL[theme];
|
|
973
|
-
return compositeOver(box, compositeOver(box, compositeOver(backing,
|
|
994
|
+
return compositeOver(box, compositeOver(box, compositeOver(backing, canvas[theme])));
|
|
974
995
|
};
|
|
975
996
|
var MERMAID_ER = {
|
|
976
997
|
light: { odd: "#ffffff", even: "#f1f1ff", label: "#333333" },
|
|
@@ -992,12 +1013,12 @@ var scorePair = (kind, d, fg, bg) => {
|
|
|
992
1013
|
};
|
|
993
1014
|
};
|
|
994
1015
|
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
995
|
-
var scoreEdgeLabel = (d, pairs, skipped) => {
|
|
1016
|
+
var scoreEdgeLabel = (d, canvas, pairs, skipped) => {
|
|
996
1017
|
const { color } = d.properties;
|
|
997
1018
|
if (!color)
|
|
998
1019
|
return;
|
|
999
1020
|
try {
|
|
1000
|
-
pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t))));
|
|
1021
|
+
pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t, canvas))));
|
|
1001
1022
|
} catch (err) {
|
|
1002
1023
|
skipped.push({
|
|
1003
1024
|
selector: d.selector,
|
|
@@ -1007,12 +1028,12 @@ var scoreEdgeLabel = (d, pairs, skipped) => {
|
|
|
1007
1028
|
});
|
|
1008
1029
|
}
|
|
1009
1030
|
};
|
|
1010
|
-
var scoreGithub = (directives) => {
|
|
1031
|
+
var scoreGithub = (directives, canvas = GITHUB_CANVAS) => {
|
|
1011
1032
|
const pairs = [];
|
|
1012
1033
|
const skipped = [];
|
|
1013
1034
|
for (const d of directives) {
|
|
1014
1035
|
if (d.kind === "linkStyle")
|
|
1015
|
-
scoreEdgeLabel(d, pairs, skipped);
|
|
1036
|
+
scoreEdgeLabel(d, canvas, pairs, skipped);
|
|
1016
1037
|
if (d.kind !== "classDef" && d.kind !== "style")
|
|
1017
1038
|
continue;
|
|
1018
1039
|
const { fill, color, stroke } = d.properties;
|
|
@@ -1034,7 +1055,7 @@ var scoreGithub = (directives) => {
|
|
|
1034
1055
|
note("fill is fully transparent, so there is no box to anchor contrast");
|
|
1035
1056
|
continue;
|
|
1036
1057
|
}
|
|
1037
|
-
const boxes = alpha < 1 ? THEMES.map((t) => [compositeOver(fill,
|
|
1058
|
+
const boxes = alpha < 1 ? THEMES.map((t) => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
|
|
1038
1059
|
if (!color) {
|
|
1039
1060
|
note("no color: declared beside fill:, and the github profile requires one " + "(if the host forces label text, use the mkdocs-material profile)", true);
|
|
1040
1061
|
}
|
|
@@ -1167,11 +1188,144 @@ var scoreEr = (directives) => {
|
|
|
1167
1188
|
}
|
|
1168
1189
|
return { pairs, skipped };
|
|
1169
1190
|
};
|
|
1170
|
-
var scoreForProfile = (directives, profile, keyword) => {
|
|
1191
|
+
var scoreForProfile = (directives, profile, keyword, canvas = GITHUB_CANVAS) => {
|
|
1171
1192
|
if (profile === "mkdocs-material")
|
|
1172
1193
|
return scoreMkdocs(directives);
|
|
1173
|
-
return keyword === "erDiagram" ? scoreEr(directives) : scoreGithub(directives);
|
|
1194
|
+
return keyword === "erDiagram" ? scoreEr(directives) : scoreGithub(directives, canvas);
|
|
1195
|
+
};
|
|
1196
|
+
var SEQUENCE_THEMES = ["base", "default", "dark", "forest", "neutral"];
|
|
1197
|
+
var LINE = { NOTE: 2, ACTIVE_START: 17, RECT_START: 22, AUTONUMBER: 26 };
|
|
1198
|
+
var FRAME_STARTS = new Set([10, 12, 15, 19, 27, 30, 32]);
|
|
1199
|
+
var sequenceFeatures = (db) => {
|
|
1200
|
+
const messages = db.getMessages();
|
|
1201
|
+
const has = (type) => messages.some((m) => m.type === type);
|
|
1202
|
+
return {
|
|
1203
|
+
humanActor: [...db.getActors().values()].some((a) => a.type === "actor"),
|
|
1204
|
+
note: has(LINE.NOTE),
|
|
1205
|
+
frame: messages.some((m) => FRAME_STARTS.has(m.type)),
|
|
1206
|
+
activation: has(LINE.ACTIVE_START),
|
|
1207
|
+
numbered: messages.some((m) => m.type === LINE.AUTONUMBER && m.message?.visible !== false),
|
|
1208
|
+
rects: messages.filter((m) => m.type === LINE.RECT_START).map((m) => String(m.message))
|
|
1209
|
+
};
|
|
1174
1210
|
};
|
|
1211
|
+
var resolveVariables = (mermaid, config, themes) => {
|
|
1212
|
+
const prior = mermaid.mermaidAPI.getSiteConfig();
|
|
1213
|
+
try {
|
|
1214
|
+
const out = {};
|
|
1215
|
+
for (const t of THEMES) {
|
|
1216
|
+
mermaid.initialize({ ...structuredClone(config), theme: themes[t] });
|
|
1217
|
+
out[t] = { ...mermaid.mermaidAPI.getSiteConfig().themeVariables };
|
|
1218
|
+
}
|
|
1219
|
+
return out;
|
|
1220
|
+
} finally {
|
|
1221
|
+
mermaid.initialize(prior);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
var themeVariable = (selector, line) => ({
|
|
1225
|
+
kind: "themeVariables",
|
|
1226
|
+
selector,
|
|
1227
|
+
properties: {},
|
|
1228
|
+
spans: {},
|
|
1229
|
+
line
|
|
1230
|
+
});
|
|
1231
|
+
var scoreSequence = async (source, profile, canvas = GITHUB_CANVAS) => {
|
|
1232
|
+
const pairs = [];
|
|
1233
|
+
const skipped = [];
|
|
1234
|
+
const lines = source.split(`
|
|
1235
|
+
`);
|
|
1236
|
+
const keywordLine = lines.findIndex((l) => /^\s*sequenceDiagram\b/.test(l)) + 1;
|
|
1237
|
+
const lineOf = (key) => {
|
|
1238
|
+
const found = lines.findLastIndex((l) => new RegExp(`\\b${key}["']?\\s*:`).test(l));
|
|
1239
|
+
return found < 0 ? keywordLine : found + 1;
|
|
1240
|
+
};
|
|
1241
|
+
const rectLines = lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []);
|
|
1242
|
+
const mermaid = await getMermaidCore();
|
|
1243
|
+
let config;
|
|
1244
|
+
let db;
|
|
1245
|
+
try {
|
|
1246
|
+
config = (await mermaid.parse(source)).config;
|
|
1247
|
+
db = (await mermaid.mermaidAPI.getDiagramFromText(source)).db;
|
|
1248
|
+
} catch {
|
|
1249
|
+
return { pairs, skipped };
|
|
1250
|
+
}
|
|
1251
|
+
const block = (selector, reason) => {
|
|
1252
|
+
skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
|
|
1253
|
+
return { pairs, skipped };
|
|
1254
|
+
};
|
|
1255
|
+
if (config.themeCSS !== undefined)
|
|
1256
|
+
return block("themeCSS", "themeCSS restyles the diagram with CSS the audit cannot model");
|
|
1257
|
+
const pinned = config.theme;
|
|
1258
|
+
if (pinned !== undefined && !SEQUENCE_THEMES.includes(String(pinned))) {
|
|
1259
|
+
return block("theme", `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`);
|
|
1260
|
+
}
|
|
1261
|
+
const features = sequenceFeatures(db);
|
|
1262
|
+
const host = profile === "mkdocs-material";
|
|
1263
|
+
const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
|
|
1264
|
+
let variables;
|
|
1265
|
+
try {
|
|
1266
|
+
variables = resolveVariables(mermaid, config, { light: theme("default"), dark: theme(host ? "default" : "dark") });
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
return block("themeVariables", `Mermaid could not resolve the theme: ${message(err)}`);
|
|
1269
|
+
}
|
|
1270
|
+
for (const t of THEMES) {
|
|
1271
|
+
const page = host ? MKDOCS_MATERIAL[t].bg : canvas[t];
|
|
1272
|
+
const vars = variables[t];
|
|
1273
|
+
const color = (key) => {
|
|
1274
|
+
const value = vars[key];
|
|
1275
|
+
if (typeof value !== "string")
|
|
1276
|
+
throw new Error(`${key} is not a resolved colour`);
|
|
1277
|
+
return value;
|
|
1278
|
+
};
|
|
1279
|
+
const pair = (kind, fgKey, bg, suffix = "", at) => {
|
|
1280
|
+
const selector = fgKey + suffix;
|
|
1281
|
+
const line = at ?? lineOf(fgKey);
|
|
1282
|
+
try {
|
|
1283
|
+
const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
|
|
1284
|
+
pairs.push({ ...scoreHostPair(kind, themeVariable(selector, line), t, color(fgKey), box), advisory: host });
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const onPage = { colour: page };
|
|
1290
|
+
if (host) {
|
|
1291
|
+
pair("border", "actorLineColor", onPage);
|
|
1292
|
+
pair("border", "signalColor", onPage, " (arrowhead)");
|
|
1293
|
+
if (features.activation)
|
|
1294
|
+
pair("border", "activationBorderColor", { key: "activationBkgColor" });
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
pair("text", "actorTextColor", { key: "actorBkg" });
|
|
1298
|
+
pair("border", "actorBorder", { key: "actorBkg" });
|
|
1299
|
+
if (features.humanActor) {
|
|
1300
|
+
pair("text", "actorTextColor", onPage, " (human actor label)");
|
|
1301
|
+
pair("border", "actorBorder", onPage, " (human actor glyph)");
|
|
1302
|
+
}
|
|
1303
|
+
pair("border", "actorLineColor", onPage);
|
|
1304
|
+
pair("text", "signalTextColor", onPage);
|
|
1305
|
+
pair("border", "signalColor", onPage);
|
|
1306
|
+
if (features.note)
|
|
1307
|
+
pair("text", "noteTextColor", { key: "noteBkgColor" });
|
|
1308
|
+
if (features.frame) {
|
|
1309
|
+
pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
|
|
1310
|
+
pair("text", "loopTextColor", onPage);
|
|
1311
|
+
pair("border", "labelBoxBorderColor", onPage, " (loop frame)");
|
|
1312
|
+
}
|
|
1313
|
+
if (features.activation)
|
|
1314
|
+
pair("border", "activationBorderColor", { key: "activationBkgColor" });
|
|
1315
|
+
if (features.numbered)
|
|
1316
|
+
pair("text", "sequenceNumberColor", { key: "signalColor" });
|
|
1317
|
+
for (const [i, rect] of features.rects.entries()) {
|
|
1318
|
+
const fill = { colour: rect };
|
|
1319
|
+
const at = rectLines[i];
|
|
1320
|
+
pair("text", "signalTextColor", fill, " (rect)", at);
|
|
1321
|
+
pair("border", "signalColor", fill, " (rect)", at);
|
|
1322
|
+
if (features.frame)
|
|
1323
|
+
pair("text", "loopTextColor", fill, " (rect)", at);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return { pairs, skipped };
|
|
1327
|
+
};
|
|
1328
|
+
var scoreDiagram = (source, directives, profile, keyword, canvas = GITHUB_CANVAS) => keyword === "sequenceDiagram" ? scoreSequence(source, profile, canvas) : Promise.resolve(scoreForProfile(directives, profile, keyword, canvas));
|
|
1175
1329
|
var detectProfile = (filePath) => {
|
|
1176
1330
|
let dir = dirname(resolve(filePath));
|
|
1177
1331
|
for (let i = 0;i < 20; i++) {
|
|
@@ -1335,7 +1489,7 @@ var COMPLEXITY_OPTIONS = {
|
|
|
1335
1489
|
subgraphWeight: "number",
|
|
1336
1490
|
depthWeight: "number"
|
|
1337
1491
|
};
|
|
1338
|
-
var CONTRAST_OPTIONS = { profile: "string" };
|
|
1492
|
+
var CONTRAST_OPTIONS = { profile: "string", canvas: "canvas" };
|
|
1339
1493
|
var measure = async (diagram, config) => {
|
|
1340
1494
|
const stats = await diagram.stats();
|
|
1341
1495
|
if (parseFailed(diagram.source, stats))
|
|
@@ -1365,17 +1519,35 @@ var profileOf = (ctx) => {
|
|
|
1365
1519
|
}
|
|
1366
1520
|
return wanted === "auto" ? detectProfile(ctx.file) : wanted;
|
|
1367
1521
|
};
|
|
1368
|
-
var
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
if (
|
|
1372
|
-
|
|
1373
|
-
|
|
1522
|
+
var settingsOf = (ctx) => {
|
|
1523
|
+
const profile = profileOf(ctx);
|
|
1524
|
+
const setting = ctx.options.canvas;
|
|
1525
|
+
if (setting === undefined)
|
|
1526
|
+
return { profile, canvas: GITHUB_CANVAS };
|
|
1527
|
+
for (const side of THEMES) {
|
|
1528
|
+
if (!(side in setting))
|
|
1529
|
+
continue;
|
|
1530
|
+
const problem = canvasColourProblem(setting[side]);
|
|
1531
|
+
if (problem)
|
|
1532
|
+
throw new Error(`canvas.${side} ${problem}: ${JSON.stringify(setting[side])}`);
|
|
1374
1533
|
}
|
|
1375
|
-
|
|
1534
|
+
if (profile === "mkdocs-material")
|
|
1535
|
+
throw new CanvasProfileError(ctx.file);
|
|
1536
|
+
return { profile, canvas: resolveCanvas(setting) };
|
|
1537
|
+
};
|
|
1538
|
+
var scored = new WeakMap;
|
|
1539
|
+
var contrastOf = (diagram, settings) => {
|
|
1540
|
+
const { profile, canvas } = settings;
|
|
1541
|
+
const key = `${profile}\x00${canvas.light}\x00${canvas.dark}`;
|
|
1542
|
+
let bySettings = scored.get(diagram);
|
|
1543
|
+
if (!bySettings) {
|
|
1544
|
+
bySettings = new Map;
|
|
1545
|
+
scored.set(diagram, bySettings);
|
|
1546
|
+
}
|
|
1547
|
+
let result = bySettings.get(key);
|
|
1376
1548
|
if (!result) {
|
|
1377
|
-
result =
|
|
1378
|
-
|
|
1549
|
+
result = scoreDiagram(diagram.source, diagram.directives, profile, diagram.keyword, canvas);
|
|
1550
|
+
bySettings.set(key, result);
|
|
1379
1551
|
}
|
|
1380
1552
|
return result;
|
|
1381
1553
|
};
|
|
@@ -1487,11 +1659,11 @@ var contrastBorderBelowUiMinimum = {
|
|
|
1487
1659
|
category: "contrast",
|
|
1488
1660
|
summary: "a border below 3:1 against its fill",
|
|
1489
1661
|
options: CONTRAST_OPTIONS,
|
|
1490
|
-
check: (ctx) => {
|
|
1491
|
-
const
|
|
1662
|
+
check: async (ctx) => {
|
|
1663
|
+
const settings = settingsOf(ctx);
|
|
1492
1664
|
const found = [];
|
|
1493
1665
|
for (const diagram of ctx.doc.diagrams) {
|
|
1494
|
-
for (const p of contrastOf(diagram,
|
|
1666
|
+
for (const p of (await contrastOf(diagram, settings)).pairs) {
|
|
1495
1667
|
if (p.kind !== "border" || p.passes || p.advisory)
|
|
1496
1668
|
continue;
|
|
1497
1669
|
const line = fileLine(diagram, p.line);
|
|
@@ -1501,7 +1673,7 @@ var contrastBorderBelowUiMinimum = {
|
|
|
1501
1673
|
endLine: line,
|
|
1502
1674
|
rule: RULE.BORDER,
|
|
1503
1675
|
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}` : ""})`,
|
|
1504
|
-
remediation: "Darken the stroke or lighten the fill until the ratio reaches 3:1."
|
|
1676
|
+
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."
|
|
1505
1677
|
});
|
|
1506
1678
|
}
|
|
1507
1679
|
}
|
|
@@ -1515,11 +1687,11 @@ var contrastDirectiveUnauditable = {
|
|
|
1515
1687
|
category: "contrast",
|
|
1516
1688
|
summary: "a style directive whose contrast cannot be audited",
|
|
1517
1689
|
options: CONTRAST_OPTIONS,
|
|
1518
|
-
check: (ctx) => {
|
|
1519
|
-
const
|
|
1690
|
+
check: async (ctx) => {
|
|
1691
|
+
const settings = settingsOf(ctx);
|
|
1520
1692
|
const found = [];
|
|
1521
1693
|
for (const diagram of ctx.doc.diagrams) {
|
|
1522
|
-
for (const s of contrastOf(diagram,
|
|
1694
|
+
for (const s of (await contrastOf(diagram, settings)).skipped) {
|
|
1523
1695
|
if (!s.blocking)
|
|
1524
1696
|
continue;
|
|
1525
1697
|
const line = fileLine(diagram, s.line);
|
|
@@ -1529,7 +1701,7 @@ var contrastDirectiveUnauditable = {
|
|
|
1529
1701
|
endLine: line,
|
|
1530
1702
|
rule: RULE.UNAUDITABLE,
|
|
1531
1703
|
message: `${s.selector}: unaudited, ${s.reason}`,
|
|
1532
|
-
remediation: "Declare the missing property
|
|
1704
|
+
remediation: "Declare the missing property, fix the colour, or remove the config the audit cannot model, so the pair can be scored."
|
|
1533
1705
|
});
|
|
1534
1706
|
}
|
|
1535
1707
|
}
|
|
@@ -1546,11 +1718,11 @@ var contrastTextBelowAa = {
|
|
|
1546
1718
|
category: "contrast",
|
|
1547
1719
|
summary: "label text below 4.5:1 against its fill",
|
|
1548
1720
|
options: CONTRAST_OPTIONS,
|
|
1549
|
-
check: (ctx) => {
|
|
1550
|
-
const
|
|
1721
|
+
check: async (ctx) => {
|
|
1722
|
+
const settings = settingsOf(ctx);
|
|
1551
1723
|
const found = [];
|
|
1552
1724
|
for (const diagram of ctx.doc.diagrams) {
|
|
1553
|
-
for (const p of contrastOf(diagram,
|
|
1725
|
+
for (const p of (await contrastOf(diagram, settings)).pairs) {
|
|
1554
1726
|
if (p.kind !== "text" || p.passes)
|
|
1555
1727
|
continue;
|
|
1556
1728
|
const where = [p.theme, p.row].filter(Boolean).join("/");
|
|
@@ -1561,19 +1733,19 @@ var contrastTextBelowAa = {
|
|
|
1561
1733
|
endLine: line,
|
|
1562
1734
|
rule: RULE.TEXT,
|
|
1563
1735
|
message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
|
|
1564
|
-
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."
|
|
1736
|
+
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."
|
|
1565
1737
|
});
|
|
1566
1738
|
}
|
|
1567
1739
|
}
|
|
1568
1740
|
return found;
|
|
1569
1741
|
},
|
|
1570
|
-
fix: (ctx) => {
|
|
1571
|
-
const
|
|
1572
|
-
if (profile === "mkdocs-material")
|
|
1742
|
+
fix: async (ctx) => {
|
|
1743
|
+
const settings = settingsOf(ctx);
|
|
1744
|
+
if (settings.profile === "mkdocs-material")
|
|
1573
1745
|
return [];
|
|
1574
1746
|
const edits = [];
|
|
1575
1747
|
for (const diagram of ctx.doc.diagrams) {
|
|
1576
|
-
const { pairs } = contrastOf(diagram,
|
|
1748
|
+
const { pairs } = await contrastOf(diagram, settings);
|
|
1577
1749
|
const failing = new Set(pairs.filter((p) => p.kind === "text" && !p.passes).map((p) => p.directive));
|
|
1578
1750
|
for (const directive of failing) {
|
|
1579
1751
|
const span = directive.spans.color;
|
|
@@ -1693,8 +1865,27 @@ var typeOf = (value) => {
|
|
|
1693
1865
|
return "boolean";
|
|
1694
1866
|
if (Array.isArray(value) && value.every((v) => typeof v === "string"))
|
|
1695
1867
|
return "string[]";
|
|
1868
|
+
if (isCanvasShape(value))
|
|
1869
|
+
return "canvas";
|
|
1696
1870
|
return null;
|
|
1697
1871
|
};
|
|
1872
|
+
var isCanvasShape = (value) => {
|
|
1873
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1874
|
+
return false;
|
|
1875
|
+
const keys = Object.keys(value);
|
|
1876
|
+
return keys.length > 0 && keys.every((k) => THEMES.includes(k));
|
|
1877
|
+
};
|
|
1878
|
+
var describeType = (want) => want === "canvas" ? 'an object such as { "light": "#ffffff", "dark": "#0d1117" }' : `a ${want}`;
|
|
1879
|
+
var checkCanvas = (where, rule, name, value) => {
|
|
1880
|
+
for (const side of THEMES) {
|
|
1881
|
+
if (!(side in value))
|
|
1882
|
+
continue;
|
|
1883
|
+
const problem = canvasColourProblem(value[side]);
|
|
1884
|
+
if (problem) {
|
|
1885
|
+
throw new RuleOptionError(where, rule.id, name, `option "${name}.${side}" of "${rule.id}" ${problem}: ${JSON.stringify(value[side])}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1698
1889
|
var checkOptions = (where, rule, options) => {
|
|
1699
1890
|
const accepted = Object.keys(rule.options ?? {});
|
|
1700
1891
|
for (const [name, value] of Object.entries(options)) {
|
|
@@ -1704,8 +1895,13 @@ var checkOptions = (where, rule, options) => {
|
|
|
1704
1895
|
throw new RuleOptionError(where, rule.id, name, `rule "${rule.id}" has no option "${name}"; ${list}`);
|
|
1705
1896
|
}
|
|
1706
1897
|
if (typeOf(value) !== want) {
|
|
1707
|
-
throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be
|
|
1898
|
+
throw new RuleOptionError(where, rule.id, name, `option "${name}" of "${rule.id}" must be ${describeType(want)}`);
|
|
1708
1899
|
}
|
|
1900
|
+
if (want === "canvas")
|
|
1901
|
+
checkCanvas(where, rule, name, value);
|
|
1902
|
+
}
|
|
1903
|
+
if (options.profile === "mkdocs-material" && options.canvas !== undefined && rule.options?.canvas === "canvas") {
|
|
1904
|
+
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`);
|
|
1709
1905
|
}
|
|
1710
1906
|
if (typeof options.preset === "string" && canonicalPreset(options.preset) === null) {
|
|
1711
1907
|
throw new RuleOptionError(where, rule.id, "preset", `option "preset" of "${rule.id}" is not a known preset`);
|
|
@@ -1749,8 +1945,24 @@ var buildRegistry = (input) => {
|
|
|
1749
1945
|
var BUILTIN = buildRegistry({ rules: RULES, fixOrder: FIX_ORDER, categories: BUILTIN_CATEGORIES });
|
|
1750
1946
|
|
|
1751
1947
|
// src/check.ts
|
|
1948
|
+
var canvasName = (side, prefix) => prefix ? `${prefix}canvas-${side}` : `canvas.${side}`;
|
|
1752
1949
|
var checkRunFlags = (flags, prefix = "") => {
|
|
1753
|
-
const { preset, profile } = flags;
|
|
1950
|
+
const { preset, profile, canvas } = flags;
|
|
1951
|
+
for (const side of THEMES) {
|
|
1952
|
+
if (canvas?.[side] === undefined)
|
|
1953
|
+
continue;
|
|
1954
|
+
const problem = canvasColourProblem(canvas[side]);
|
|
1955
|
+
if (problem) {
|
|
1956
|
+
const name = canvasName(side, prefix);
|
|
1957
|
+
throw new FlagError(name, `${name} ${problem}: "${canvas[side]}"; give an opaque CSS colour, such as #ffffff`);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
const sides = THEMES.filter((side) => canvas?.[side] !== undefined);
|
|
1961
|
+
const first = sides[0];
|
|
1962
|
+
if (profile === "mkdocs-material" && first !== undefined) {
|
|
1963
|
+
const name = canvasName(first, prefix);
|
|
1964
|
+
throw new FlagError(name, `${name} applies under the github profile only; mkdocs-material scores Material's own page colours`);
|
|
1965
|
+
}
|
|
1754
1966
|
if (preset !== undefined && canonicalPreset(preset) === null) {
|
|
1755
1967
|
throw new FlagError(`${prefix}preset`, `${prefix}preset must be one of ${PRESET_NAMES.join(", ")}, not "${preset}"`);
|
|
1756
1968
|
}
|
|
@@ -1767,6 +1979,17 @@ var optionsFor = (rule, configured, flags) => {
|
|
|
1767
1979
|
}
|
|
1768
1980
|
if (typeof options.preset === "string")
|
|
1769
1981
|
options.preset = canonicalPreset(options.preset) ?? options.preset;
|
|
1982
|
+
const canvas = flags.canvas;
|
|
1983
|
+
if (canvas && THEMES.some((side) => canvas[side] !== undefined) && rule.options?.canvas === "canvas") {
|
|
1984
|
+
const configured2 = typeof options.canvas === "object" && options.canvas !== null ? options.canvas : {};
|
|
1985
|
+
const merged = { ...configured2 };
|
|
1986
|
+
for (const side of THEMES) {
|
|
1987
|
+
const value = canvas[side];
|
|
1988
|
+
if (value !== undefined)
|
|
1989
|
+
merged[side] = value;
|
|
1990
|
+
}
|
|
1991
|
+
options.canvas = merged;
|
|
1992
|
+
}
|
|
1770
1993
|
return options;
|
|
1771
1994
|
};
|
|
1772
1995
|
var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
|
|
@@ -1783,7 +2006,7 @@ import { parseArgs } from "node:util";
|
|
|
1783
2006
|
// package.json
|
|
1784
2007
|
var package_default = {
|
|
1785
2008
|
name: "@jpeakai/mermaidjs-lint",
|
|
1786
|
-
version: "0.
|
|
2009
|
+
version: "0.6.0",
|
|
1787
2010
|
type: "module",
|
|
1788
2011
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
1789
2012
|
license: "MIT",
|
|
@@ -1841,7 +2064,7 @@ var package_default = {
|
|
|
1841
2064
|
"mdast-util-from-markdown": "^2.0.3",
|
|
1842
2065
|
"mdast-util-frontmatter": "^2.0.1",
|
|
1843
2066
|
"mdast-util-gfm": "^3.1.0",
|
|
1844
|
-
mermaid: "
|
|
2067
|
+
mermaid: "11.17.2",
|
|
1845
2068
|
"micromark-extension-frontmatter": "^2.0.0",
|
|
1846
2069
|
"micromark-extension-gfm": "^3.0.0"
|
|
1847
2070
|
},
|
|
@@ -2269,7 +2492,7 @@ var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
|
2269
2492
|
|
|
2270
2493
|
// src/plugins/runtime.ts
|
|
2271
2494
|
var canImportTypeScript = (runtime = process) => typeof runtime.versions.bun === "string" || Boolean(runtime.features.typescript);
|
|
2272
|
-
var 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";
|
|
2495
|
+
var 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";
|
|
2273
2496
|
|
|
2274
2497
|
// src/plugins/load.ts
|
|
2275
2498
|
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -2291,7 +2514,7 @@ var importModule = async (source, runtime) => {
|
|
|
2291
2514
|
throw new PluginImportError(source.spec, message2(err));
|
|
2292
2515
|
}
|
|
2293
2516
|
};
|
|
2294
|
-
var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]"]);
|
|
2517
|
+
var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]", "canvas"]);
|
|
2295
2518
|
var RESERVED_OPTIONS = ["preset", "profile"];
|
|
2296
2519
|
var checkRule = (spec, key, raw, own) => {
|
|
2297
2520
|
const where = `rule "${key}"`;
|
|
@@ -2320,7 +2543,7 @@ var checkRule = (spec, key, raw, own) => {
|
|
|
2320
2543
|
if (options !== undefined) {
|
|
2321
2544
|
const valid = isRecord3(options) && Object.values(options).every((t) => OPTION_TYPES.has(t));
|
|
2322
2545
|
if (!valid) {
|
|
2323
|
-
throw new PluginContractError(spec, `${where} options must map names to number, string, boolean
|
|
2546
|
+
throw new PluginContractError(spec, `${where} options must map names to number, string, boolean, string[] or canvas`);
|
|
2324
2547
|
}
|
|
2325
2548
|
for (const name of RESERVED_OPTIONS) {
|
|
2326
2549
|
if (name in options && options[name] !== "string") {
|
|
@@ -2587,7 +2810,7 @@ var setUp = async ({
|
|
|
2587
2810
|
|
|
2588
2811
|
// src/cli.ts
|
|
2589
2812
|
var SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
|
|
2590
|
-
[--no-plugins | --lenient-plugins]
|
|
2813
|
+
[--canvas-light COLOUR] [--canvas-dark COLOUR] [--no-plugins | --lenient-plugins]
|
|
2591
2814
|
mermaidjs-lint --list-rules
|
|
2592
2815
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
2593
2816
|
mermaidjs-lint --version | -h, --help`;
|
|
@@ -2613,6 +2836,10 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
|
|
|
2613
2836
|
numeric threshold set in the config overrides both.
|
|
2614
2837
|
--profile NAME sets how contrast is read: ${PROFILES.join(", ")}. It overrides a profile set in the config.
|
|
2615
2838
|
auto reads mkdocs-material when any parent folder holds mkdocs.yml or mkdocs.yaml, else github.
|
|
2839
|
+
--canvas-light COLOUR and --canvas-dark COLOUR set the page a host draws in each theme, under the github
|
|
2840
|
+
profile only. A translucent fill, an edge label box and a sequence diagram's page-backed text are scored
|
|
2841
|
+
on it. Each is an opaque CSS colour such as #faf8f9, and a side not given keeps the config's, or GitHub's
|
|
2842
|
+
(#ffffff light, #0d1117 dark). A file scored under mkdocs-material refuses a canvas with a usage error.
|
|
2616
2843
|
--json prints one JSON object instead of text lines.
|
|
2617
2844
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
2618
2845
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
@@ -2636,6 +2863,8 @@ var parse2 = (argv) => parseArgs({
|
|
|
2636
2863
|
help: { type: "boolean", short: "h", default: false },
|
|
2637
2864
|
preset: { type: "string", short: "p" },
|
|
2638
2865
|
profile: { type: "string" },
|
|
2866
|
+
"canvas-light": { type: "string" },
|
|
2867
|
+
"canvas-dark": { type: "string" },
|
|
2639
2868
|
config: { type: "string" },
|
|
2640
2869
|
"no-plugins": { type: "boolean", default: false },
|
|
2641
2870
|
"lenient-plugins": { type: "boolean", default: false },
|
|
@@ -2741,7 +2970,16 @@ var run = async (argv, cwd) => {
|
|
|
2741
2970
|
}
|
|
2742
2971
|
const listing = values["list-rules"];
|
|
2743
2972
|
const validating = values["validate-plugins"];
|
|
2744
|
-
|
|
2973
|
+
const canvas = {
|
|
2974
|
+
...values["canvas-light"] !== undefined && { light: values["canvas-light"] },
|
|
2975
|
+
...values["canvas-dark"] !== undefined && { dark: values["canvas-dark"] }
|
|
2976
|
+
};
|
|
2977
|
+
const flags = {
|
|
2978
|
+
preset: values.preset,
|
|
2979
|
+
profile: values.profile,
|
|
2980
|
+
...Object.keys(canvas).length > 0 && { canvas }
|
|
2981
|
+
};
|
|
2982
|
+
checkRunFlags(flags, "--");
|
|
2745
2983
|
if (values["no-plugins"] && values["lenient-plugins"]) {
|
|
2746
2984
|
throw new FlagError("--lenient-plugins", "--no-plugins and --lenient-plugins contradict each other");
|
|
2747
2985
|
}
|
|
@@ -2787,7 +3025,6 @@ var run = async (argv, cwd) => {
|
|
|
2787
3025
|
throw new FlagError("files", "no matching files");
|
|
2788
3026
|
if (values.fix)
|
|
2789
3027
|
await refuseOutsideLinks(files, root, cwd);
|
|
2790
|
-
const flags = { preset: values.preset, profile: values.profile };
|
|
2791
3028
|
const perFile = await Promise.all(files.map(async (file) => {
|
|
2792
3029
|
let src = await readFile3(file, "utf8");
|
|
2793
3030
|
if (values.fix) {
|
|
@@ -2863,6 +3100,7 @@ export {
|
|
|
2863
3100
|
PackageJsonError,
|
|
2864
3101
|
PRESETS,
|
|
2865
3102
|
MermaidLintError,
|
|
3103
|
+
GITHUB_CANVAS,
|
|
2866
3104
|
FlagError,
|
|
2867
3105
|
FixOutsideRootError,
|
|
2868
3106
|
FixNotConvergingError,
|
|
@@ -2872,6 +3110,7 @@ export {
|
|
|
2872
3110
|
ConfigNotFoundError,
|
|
2873
3111
|
ConfigFileError,
|
|
2874
3112
|
ConfigCodeError,
|
|
3113
|
+
CanvasProfileError,
|
|
2875
3114
|
CATEGORIES,
|
|
2876
3115
|
BUILTIN_CATEGORIES,
|
|
2877
3116
|
BUILTIN,
|