@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/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 GITHUB = { light: { bg: "#ffffff" }, dark: { bg: "#0d1117" } };
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, GITHUB[theme].bg)));
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, GITHUB[t].bg), t]) : [[fill, undefined]];
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 scored = new WeakMap;
1369
- var contrastOf = (diagram, profile) => {
1370
- let byProfile = scored.get(diagram);
1371
- if (!byProfile) {
1372
- byProfile = new Map;
1373
- scored.set(diagram, byProfile);
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
- let result = byProfile.get(profile);
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 = scoreForProfile(diagram.directives, profile, diagram.keyword);
1378
- byProfile.set(profile, result);
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 profile = profileOf(ctx);
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, profile).pairs) {
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 profile = profileOf(ctx);
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, profile).skipped) {
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 or fix the colour so the pair can be scored."
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 profile = profileOf(ctx);
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, profile).pairs) {
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 profile = profileOf(ctx);
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, profile);
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 a ${want}`);
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.4.2",
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: "^11.14.0",
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 or string[]`);
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
- checkRunFlags({ preset: values.preset, profile: values.profile }, "--");
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,