@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/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 GITHUB = { light: { bg: "#ffffff" }, dark: { bg: "#0d1117" } };
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, GITHUB[theme].bg)));
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, GITHUB[t].bg), t]) : [[fill, undefined]];
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, profile) => {
1268
- let byProfile = scored.get(diagram);
1269
- if (!byProfile) {
1270
- byProfile = new Map;
1271
- scored.set(diagram, byProfile);
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 = byProfile.get(profile);
1445
+ let result = bySettings.get(key);
1274
1446
  if (!result) {
1275
- result = scoreForProfile(diagram.directives, profile, diagram.keyword);
1276
- byProfile.set(profile, result);
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 profile = profileOf(ctx);
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, profile).pairs) {
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 profile = profileOf(ctx);
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, profile).skipped) {
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 or fix the colour so the pair can be scored."
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 profile = profileOf(ctx);
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, profile).pairs) {
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 profile = profileOf(ctx);
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, profile);
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 a ${want}`);
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 or string[]`);
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") {
@@ -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;
@@ -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;
@@ -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 declare const edgeLabelBackground: (theme: Theme) => string;
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: StyleDirective;
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 declare const contrastOf: (diagram: Diagram, profile: Profile) => ContrastResult;
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.4.2",
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": "^11.14.0",
61
+ "mermaid": "11.17.2",
62
62
  "micromark-extension-frontmatter": "^2.0.0",
63
63
  "micromark-extension-gfm": "^3.0.0"
64
64
  },