@jpeakai/mermaidjs-lint 0.4.2 → 0.5.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 CHANGED
@@ -28,7 +28,7 @@ Installed as a dev dependency, the command is `mermaidjs-lint`, and `--help` lis
28
28
 
29
29
  Three tenets are guarantees about every diagram the tool passes, and the fourth is a commitment about the tool.
30
30
 
31
- - **Readable in light and dark.** Declared fills, label colours and borders are scored as the host draws them, on nodes, erDiagram rows and edge labels, in both themes.
31
+ - **Readable in light and dark.** Declared fills, label colours and borders are scored as the host draws them, on nodes, erDiagram rows, edge labels and sequence diagrams, in both themes.
32
32
  - **Renders.** Every diagram parses with the mermaid parsers this package ships, and every mermaid fence is closed.
33
33
  - **Simple enough to follow.** Node count and visual complexity are scored against a density preset, so an oversized diagram is flagged to be split.
34
34
  - **Extensible.** A plugin adds rules of your own, which run through the same config, fix engine and verification as the built-ins.
@@ -54,7 +54,7 @@ It does not compare edge endpoints, edge labels or node text, so review the diff
54
54
  ## Requirements and compatibility
55
55
 
56
56
  - **Runtime:** Bun, or Node `>=20` as the `engines` field of `package.json` states.
57
- - **Mermaid:** the parse rules use the `mermaid` package, at `^11.14.0`, and `@mermaid-js/parser`, both installed with this package.
57
+ - **Mermaid:** the parse and contrast rules use the `mermaid` package at exactly `11.17.2`, and `@mermaid-js/parser`, both installed with this package. The pin holds sequence diagram colours, which change between mermaid releases.
58
58
  - **Your host's mermaid may differ.** GitHub and other hosts draw diagrams with their own mermaid version. A diagram type newer than the host's may parse here and still fail to draw there.
59
59
 
60
60
  `npm ls mermaid` in your project prints the mermaid version a run uses.
package/dist/bin.js CHANGED
@@ -9,7 +9,7 @@ import { parseArgs } from "node:util";
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "@jpeakai/mermaidjs-lint",
12
- version: "0.4.2",
12
+ version: "0.5.0",
13
13
  type: "module",
14
14
  description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
15
15
  license: "MIT",
@@ -67,7 +67,7 @@ var package_default = {
67
67
  "mdast-util-from-markdown": "^2.0.3",
68
68
  "mdast-util-frontmatter": "^2.0.1",
69
69
  "mdast-util-gfm": "^3.1.0",
70
- mermaid: "^11.14.0",
70
+ mermaid: "11.17.2",
71
71
  "micromark-extension-frontmatter": "^2.0.0",
72
72
  "micromark-extension-gfm": "^3.0.0"
73
73
  },
@@ -395,6 +395,7 @@ var getMermaid = async () => {
395
395
  mermaidCore = (await import("mermaid")).default;
396
396
  return mermaidCore;
397
397
  };
398
+ var getMermaidCore = async () => await getMermaid();
398
399
  var LANGIUM_TYPES = {
399
400
  "architecture-beta": "architecture",
400
401
  architecture: "architecture",
@@ -1250,6 +1251,139 @@ var scoreForProfile = (directives, profile, keyword) => {
1250
1251
  return scoreMkdocs(directives);
1251
1252
  return keyword === "erDiagram" ? scoreEr(directives) : scoreGithub(directives);
1252
1253
  };
1254
+ var SEQUENCE_THEMES = ["base", "default", "dark", "forest", "neutral"];
1255
+ var LINE = { NOTE: 2, ACTIVE_START: 17, RECT_START: 22, AUTONUMBER: 26 };
1256
+ var FRAME_STARTS = new Set([10, 12, 15, 19, 27, 30, 32]);
1257
+ var sequenceFeatures = (db) => {
1258
+ const messages = db.getMessages();
1259
+ const has = (type) => messages.some((m) => m.type === type);
1260
+ return {
1261
+ humanActor: [...db.getActors().values()].some((a) => a.type === "actor"),
1262
+ note: has(LINE.NOTE),
1263
+ frame: messages.some((m) => FRAME_STARTS.has(m.type)),
1264
+ activation: has(LINE.ACTIVE_START),
1265
+ numbered: messages.some((m) => m.type === LINE.AUTONUMBER && m.message?.visible !== false),
1266
+ rects: messages.filter((m) => m.type === LINE.RECT_START).map((m) => String(m.message))
1267
+ };
1268
+ };
1269
+ var resolveVariables = (mermaid, config, themes) => {
1270
+ const prior = mermaid.mermaidAPI.getSiteConfig();
1271
+ try {
1272
+ const out = {};
1273
+ for (const t of THEMES) {
1274
+ mermaid.initialize({ ...structuredClone(config), theme: themes[t] });
1275
+ out[t] = { ...mermaid.mermaidAPI.getSiteConfig().themeVariables };
1276
+ }
1277
+ return out;
1278
+ } finally {
1279
+ mermaid.initialize(prior);
1280
+ }
1281
+ };
1282
+ var themeVariable = (selector, line) => ({
1283
+ kind: "themeVariables",
1284
+ selector,
1285
+ properties: {},
1286
+ spans: {},
1287
+ line
1288
+ });
1289
+ var scoreSequence = async (source, profile) => {
1290
+ const pairs = [];
1291
+ const skipped = [];
1292
+ const lines = source.split(`
1293
+ `);
1294
+ const keywordLine = lines.findIndex((l) => /^\s*sequenceDiagram\b/.test(l)) + 1;
1295
+ const lineOf = (key) => {
1296
+ const found = lines.findLastIndex((l) => new RegExp(`\\b${key}["']?\\s*:`).test(l));
1297
+ return found < 0 ? keywordLine : found + 1;
1298
+ };
1299
+ const rectLines = lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []);
1300
+ const mermaid = await getMermaidCore();
1301
+ let config;
1302
+ let db;
1303
+ try {
1304
+ config = (await mermaid.parse(source)).config;
1305
+ db = (await mermaid.mermaidAPI.getDiagramFromText(source)).db;
1306
+ } catch {
1307
+ return { pairs, skipped };
1308
+ }
1309
+ const block = (selector, reason) => {
1310
+ skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
1311
+ return { pairs, skipped };
1312
+ };
1313
+ if (config.themeCSS !== undefined)
1314
+ return block("themeCSS", "themeCSS restyles the diagram with CSS the audit cannot model");
1315
+ const pinned = config.theme;
1316
+ if (pinned !== undefined && !SEQUENCE_THEMES.includes(String(pinned))) {
1317
+ return block("theme", `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`);
1318
+ }
1319
+ const features = sequenceFeatures(db);
1320
+ const host = profile === "mkdocs-material";
1321
+ const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
1322
+ let variables;
1323
+ try {
1324
+ variables = resolveVariables(mermaid, config, { light: theme("default"), dark: theme(host ? "default" : "dark") });
1325
+ } catch (err) {
1326
+ return block("themeVariables", `Mermaid could not resolve the theme: ${message(err)}`);
1327
+ }
1328
+ for (const t of THEMES) {
1329
+ const page = (host ? MKDOCS_MATERIAL : GITHUB)[t].bg;
1330
+ const vars = variables[t];
1331
+ const color = (key) => {
1332
+ const value = vars[key];
1333
+ if (typeof value !== "string")
1334
+ throw new Error(`${key} is not a resolved colour`);
1335
+ return value;
1336
+ };
1337
+ const pair = (kind, fgKey, bg, suffix = "", at) => {
1338
+ const selector = fgKey + suffix;
1339
+ const line = at ?? lineOf(fgKey);
1340
+ try {
1341
+ const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
1342
+ pairs.push({ ...scoreHostPair(kind, themeVariable(selector, line), t, color(fgKey), box), advisory: host });
1343
+ } catch (err) {
1344
+ skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
1345
+ }
1346
+ };
1347
+ const onPage = { colour: page };
1348
+ if (host) {
1349
+ pair("border", "actorLineColor", onPage);
1350
+ pair("border", "signalColor", onPage, " (arrowhead)");
1351
+ if (features.activation)
1352
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1353
+ continue;
1354
+ }
1355
+ pair("text", "actorTextColor", { key: "actorBkg" });
1356
+ pair("border", "actorBorder", { key: "actorBkg" });
1357
+ if (features.humanActor) {
1358
+ pair("text", "actorTextColor", onPage, " (human actor label)");
1359
+ pair("border", "actorBorder", onPage, " (human actor glyph)");
1360
+ }
1361
+ pair("border", "actorLineColor", onPage);
1362
+ pair("text", "signalTextColor", onPage);
1363
+ pair("border", "signalColor", onPage);
1364
+ if (features.note)
1365
+ pair("text", "noteTextColor", { key: "noteBkgColor" });
1366
+ if (features.frame) {
1367
+ pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
1368
+ pair("text", "loopTextColor", onPage);
1369
+ pair("border", "labelBoxBorderColor", onPage, " (loop frame)");
1370
+ }
1371
+ if (features.activation)
1372
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1373
+ if (features.numbered)
1374
+ pair("text", "sequenceNumberColor", { key: "signalColor" });
1375
+ for (const [i, rect] of features.rects.entries()) {
1376
+ const fill = { colour: rect };
1377
+ const at = rectLines[i];
1378
+ pair("text", "signalTextColor", fill, " (rect)", at);
1379
+ pair("border", "signalColor", fill, " (rect)", at);
1380
+ if (features.frame)
1381
+ pair("text", "loopTextColor", fill, " (rect)", at);
1382
+ }
1383
+ }
1384
+ return { pairs, skipped };
1385
+ };
1386
+ var scoreDiagram = (source, directives, profile, keyword) => keyword === "sequenceDiagram" ? scoreSequence(source, profile) : Promise.resolve(scoreForProfile(directives, profile, keyword));
1253
1387
  var detectProfile = (filePath) => {
1254
1388
  let dir = dirname(resolve(filePath));
1255
1389
  for (let i = 0;i < 20; i++) {
@@ -1452,7 +1586,7 @@ var contrastOf = (diagram, profile) => {
1452
1586
  }
1453
1587
  let result = byProfile.get(profile);
1454
1588
  if (!result) {
1455
- result = scoreForProfile(diagram.directives, profile, diagram.keyword);
1589
+ result = scoreDiagram(diagram.source, diagram.directives, profile, diagram.keyword);
1456
1590
  byProfile.set(profile, result);
1457
1591
  }
1458
1592
  return result;
@@ -1565,11 +1699,11 @@ var contrastBorderBelowUiMinimum = {
1565
1699
  category: "contrast",
1566
1700
  summary: "a border below 3:1 against its fill",
1567
1701
  options: CONTRAST_OPTIONS,
1568
- check: (ctx) => {
1702
+ check: async (ctx) => {
1569
1703
  const profile = profileOf(ctx);
1570
1704
  const found = [];
1571
1705
  for (const diagram of ctx.doc.diagrams) {
1572
- for (const p of contrastOf(diagram, profile).pairs) {
1706
+ for (const p of (await contrastOf(diagram, profile)).pairs) {
1573
1707
  if (p.kind !== "border" || p.passes || p.advisory)
1574
1708
  continue;
1575
1709
  const line = fileLine(diagram, p.line);
@@ -1579,7 +1713,7 @@ var contrastBorderBelowUiMinimum = {
1579
1713
  endLine: line,
1580
1714
  rule: RULE.BORDER,
1581
1715
  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}` : ""})`,
1582
- remediation: "Darken the stroke or lighten the fill until the ratio reaches 3:1."
1716
+ 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."
1583
1717
  });
1584
1718
  }
1585
1719
  }
@@ -1593,11 +1727,11 @@ var contrastDirectiveUnauditable = {
1593
1727
  category: "contrast",
1594
1728
  summary: "a style directive whose contrast cannot be audited",
1595
1729
  options: CONTRAST_OPTIONS,
1596
- check: (ctx) => {
1730
+ check: async (ctx) => {
1597
1731
  const profile = profileOf(ctx);
1598
1732
  const found = [];
1599
1733
  for (const diagram of ctx.doc.diagrams) {
1600
- for (const s of contrastOf(diagram, profile).skipped) {
1734
+ for (const s of (await contrastOf(diagram, profile)).skipped) {
1601
1735
  if (!s.blocking)
1602
1736
  continue;
1603
1737
  const line = fileLine(diagram, s.line);
@@ -1607,7 +1741,7 @@ var contrastDirectiveUnauditable = {
1607
1741
  endLine: line,
1608
1742
  rule: RULE.UNAUDITABLE,
1609
1743
  message: `${s.selector}: unaudited, ${s.reason}`,
1610
- remediation: "Declare the missing property or fix the colour so the pair can be scored."
1744
+ remediation: "Declare the missing property, fix the colour, or remove the config the audit cannot model, so the pair can be scored."
1611
1745
  });
1612
1746
  }
1613
1747
  }
@@ -1624,11 +1758,11 @@ var contrastTextBelowAa = {
1624
1758
  category: "contrast",
1625
1759
  summary: "label text below 4.5:1 against its fill",
1626
1760
  options: CONTRAST_OPTIONS,
1627
- check: (ctx) => {
1761
+ check: async (ctx) => {
1628
1762
  const profile = profileOf(ctx);
1629
1763
  const found = [];
1630
1764
  for (const diagram of ctx.doc.diagrams) {
1631
- for (const p of contrastOf(diagram, profile).pairs) {
1765
+ for (const p of (await contrastOf(diagram, profile)).pairs) {
1632
1766
  if (p.kind !== "text" || p.passes)
1633
1767
  continue;
1634
1768
  const where = [p.theme, p.row].filter(Boolean).join("/");
@@ -1639,19 +1773,19 @@ var contrastTextBelowAa = {
1639
1773
  endLine: line,
1640
1774
  rule: RULE.TEXT,
1641
1775
  message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
1642
- 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."
1776
+ 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 === "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."
1643
1777
  });
1644
1778
  }
1645
1779
  }
1646
1780
  return found;
1647
1781
  },
1648
- fix: (ctx) => {
1782
+ fix: async (ctx) => {
1649
1783
  const profile = profileOf(ctx);
1650
1784
  if (profile === "mkdocs-material")
1651
1785
  return [];
1652
1786
  const edits = [];
1653
1787
  for (const diagram of ctx.doc.diagrams) {
1654
- const { pairs } = contrastOf(diagram, profile);
1788
+ const { pairs } = await contrastOf(diagram, profile);
1655
1789
  const failing = new Set(pairs.filter((p) => p.kind === "text" && !p.passes).map((p) => p.directive));
1656
1790
  for (const directive of failing) {
1657
1791
  const span = directive.spans.color;
@@ -2269,7 +2403,7 @@ var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2269
2403
 
2270
2404
  // src/plugins/runtime.ts
2271
2405
  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";
2406
+ 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
2407
 
2274
2408
  // src/plugins/load.ts
2275
2409
  var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
package/dist/index.js CHANGED
@@ -317,6 +317,7 @@ var getMermaid = async () => {
317
317
  mermaidCore = (await import("mermaid")).default;
318
318
  return mermaidCore;
319
319
  };
320
+ var getMermaidCore = async () => await getMermaid();
320
321
  var LANGIUM_TYPES = {
321
322
  "architecture-beta": "architecture",
322
323
  architecture: "architecture",
@@ -1172,6 +1173,139 @@ var scoreForProfile = (directives, profile, keyword) => {
1172
1173
  return scoreMkdocs(directives);
1173
1174
  return keyword === "erDiagram" ? scoreEr(directives) : scoreGithub(directives);
1174
1175
  };
1176
+ var SEQUENCE_THEMES = ["base", "default", "dark", "forest", "neutral"];
1177
+ var LINE = { NOTE: 2, ACTIVE_START: 17, RECT_START: 22, AUTONUMBER: 26 };
1178
+ var FRAME_STARTS = new Set([10, 12, 15, 19, 27, 30, 32]);
1179
+ var sequenceFeatures = (db) => {
1180
+ const messages = db.getMessages();
1181
+ const has = (type) => messages.some((m) => m.type === type);
1182
+ return {
1183
+ humanActor: [...db.getActors().values()].some((a) => a.type === "actor"),
1184
+ note: has(LINE.NOTE),
1185
+ frame: messages.some((m) => FRAME_STARTS.has(m.type)),
1186
+ activation: has(LINE.ACTIVE_START),
1187
+ numbered: messages.some((m) => m.type === LINE.AUTONUMBER && m.message?.visible !== false),
1188
+ rects: messages.filter((m) => m.type === LINE.RECT_START).map((m) => String(m.message))
1189
+ };
1190
+ };
1191
+ var resolveVariables = (mermaid, config, themes) => {
1192
+ const prior = mermaid.mermaidAPI.getSiteConfig();
1193
+ try {
1194
+ const out = {};
1195
+ for (const t of THEMES) {
1196
+ mermaid.initialize({ ...structuredClone(config), theme: themes[t] });
1197
+ out[t] = { ...mermaid.mermaidAPI.getSiteConfig().themeVariables };
1198
+ }
1199
+ return out;
1200
+ } finally {
1201
+ mermaid.initialize(prior);
1202
+ }
1203
+ };
1204
+ var themeVariable = (selector, line) => ({
1205
+ kind: "themeVariables",
1206
+ selector,
1207
+ properties: {},
1208
+ spans: {},
1209
+ line
1210
+ });
1211
+ var scoreSequence = async (source, profile) => {
1212
+ const pairs = [];
1213
+ const skipped = [];
1214
+ const lines = source.split(`
1215
+ `);
1216
+ const keywordLine = lines.findIndex((l) => /^\s*sequenceDiagram\b/.test(l)) + 1;
1217
+ const lineOf = (key) => {
1218
+ const found = lines.findLastIndex((l) => new RegExp(`\\b${key}["']?\\s*:`).test(l));
1219
+ return found < 0 ? keywordLine : found + 1;
1220
+ };
1221
+ const rectLines = lines.flatMap((l, i) => /^\s*rect\s/.test(l) ? [i + 1] : []);
1222
+ const mermaid = await getMermaidCore();
1223
+ let config;
1224
+ let db;
1225
+ try {
1226
+ config = (await mermaid.parse(source)).config;
1227
+ db = (await mermaid.mermaidAPI.getDiagramFromText(source)).db;
1228
+ } catch {
1229
+ return { pairs, skipped };
1230
+ }
1231
+ const block = (selector, reason) => {
1232
+ skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
1233
+ return { pairs, skipped };
1234
+ };
1235
+ if (config.themeCSS !== undefined)
1236
+ return block("themeCSS", "themeCSS restyles the diagram with CSS the audit cannot model");
1237
+ const pinned = config.theme;
1238
+ if (pinned !== undefined && !SEQUENCE_THEMES.includes(String(pinned))) {
1239
+ return block("theme", `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`);
1240
+ }
1241
+ const features = sequenceFeatures(db);
1242
+ const host = profile === "mkdocs-material";
1243
+ const theme = (fallback) => pinned === undefined ? fallback : String(pinned);
1244
+ let variables;
1245
+ try {
1246
+ variables = resolveVariables(mermaid, config, { light: theme("default"), dark: theme(host ? "default" : "dark") });
1247
+ } catch (err) {
1248
+ return block("themeVariables", `Mermaid could not resolve the theme: ${message(err)}`);
1249
+ }
1250
+ for (const t of THEMES) {
1251
+ const page = (host ? MKDOCS_MATERIAL : GITHUB)[t].bg;
1252
+ const vars = variables[t];
1253
+ const color = (key) => {
1254
+ const value = vars[key];
1255
+ if (typeof value !== "string")
1256
+ throw new Error(`${key} is not a resolved colour`);
1257
+ return value;
1258
+ };
1259
+ const pair = (kind, fgKey, bg, suffix = "", at) => {
1260
+ const selector = fgKey + suffix;
1261
+ const line = at ?? lineOf(fgKey);
1262
+ try {
1263
+ const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
1264
+ pairs.push({ ...scoreHostPair(kind, themeVariable(selector, line), t, color(fgKey), box), advisory: host });
1265
+ } catch (err) {
1266
+ skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
1267
+ }
1268
+ };
1269
+ const onPage = { colour: page };
1270
+ if (host) {
1271
+ pair("border", "actorLineColor", onPage);
1272
+ pair("border", "signalColor", onPage, " (arrowhead)");
1273
+ if (features.activation)
1274
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1275
+ continue;
1276
+ }
1277
+ pair("text", "actorTextColor", { key: "actorBkg" });
1278
+ pair("border", "actorBorder", { key: "actorBkg" });
1279
+ if (features.humanActor) {
1280
+ pair("text", "actorTextColor", onPage, " (human actor label)");
1281
+ pair("border", "actorBorder", onPage, " (human actor glyph)");
1282
+ }
1283
+ pair("border", "actorLineColor", onPage);
1284
+ pair("text", "signalTextColor", onPage);
1285
+ pair("border", "signalColor", onPage);
1286
+ if (features.note)
1287
+ pair("text", "noteTextColor", { key: "noteBkgColor" });
1288
+ if (features.frame) {
1289
+ pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
1290
+ pair("text", "loopTextColor", onPage);
1291
+ pair("border", "labelBoxBorderColor", onPage, " (loop frame)");
1292
+ }
1293
+ if (features.activation)
1294
+ pair("border", "activationBorderColor", { key: "activationBkgColor" });
1295
+ if (features.numbered)
1296
+ pair("text", "sequenceNumberColor", { key: "signalColor" });
1297
+ for (const [i, rect] of features.rects.entries()) {
1298
+ const fill = { colour: rect };
1299
+ const at = rectLines[i];
1300
+ pair("text", "signalTextColor", fill, " (rect)", at);
1301
+ pair("border", "signalColor", fill, " (rect)", at);
1302
+ if (features.frame)
1303
+ pair("text", "loopTextColor", fill, " (rect)", at);
1304
+ }
1305
+ }
1306
+ return { pairs, skipped };
1307
+ };
1308
+ var scoreDiagram = (source, directives, profile, keyword) => keyword === "sequenceDiagram" ? scoreSequence(source, profile) : Promise.resolve(scoreForProfile(directives, profile, keyword));
1175
1309
  var detectProfile = (filePath) => {
1176
1310
  let dir = dirname(resolve(filePath));
1177
1311
  for (let i = 0;i < 20; i++) {
@@ -1374,7 +1508,7 @@ var contrastOf = (diagram, profile) => {
1374
1508
  }
1375
1509
  let result = byProfile.get(profile);
1376
1510
  if (!result) {
1377
- result = scoreForProfile(diagram.directives, profile, diagram.keyword);
1511
+ result = scoreDiagram(diagram.source, diagram.directives, profile, diagram.keyword);
1378
1512
  byProfile.set(profile, result);
1379
1513
  }
1380
1514
  return result;
@@ -1487,11 +1621,11 @@ var contrastBorderBelowUiMinimum = {
1487
1621
  category: "contrast",
1488
1622
  summary: "a border below 3:1 against its fill",
1489
1623
  options: CONTRAST_OPTIONS,
1490
- check: (ctx) => {
1624
+ check: async (ctx) => {
1491
1625
  const profile = profileOf(ctx);
1492
1626
  const found = [];
1493
1627
  for (const diagram of ctx.doc.diagrams) {
1494
- for (const p of contrastOf(diagram, profile).pairs) {
1628
+ for (const p of (await contrastOf(diagram, profile)).pairs) {
1495
1629
  if (p.kind !== "border" || p.passes || p.advisory)
1496
1630
  continue;
1497
1631
  const line = fileLine(diagram, p.line);
@@ -1501,7 +1635,7 @@ var contrastBorderBelowUiMinimum = {
1501
1635
  endLine: line,
1502
1636
  rule: RULE.BORDER,
1503
1637
  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."
1638
+ 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
1639
  });
1506
1640
  }
1507
1641
  }
@@ -1515,11 +1649,11 @@ var contrastDirectiveUnauditable = {
1515
1649
  category: "contrast",
1516
1650
  summary: "a style directive whose contrast cannot be audited",
1517
1651
  options: CONTRAST_OPTIONS,
1518
- check: (ctx) => {
1652
+ check: async (ctx) => {
1519
1653
  const profile = profileOf(ctx);
1520
1654
  const found = [];
1521
1655
  for (const diagram of ctx.doc.diagrams) {
1522
- for (const s of contrastOf(diagram, profile).skipped) {
1656
+ for (const s of (await contrastOf(diagram, profile)).skipped) {
1523
1657
  if (!s.blocking)
1524
1658
  continue;
1525
1659
  const line = fileLine(diagram, s.line);
@@ -1529,7 +1663,7 @@ var contrastDirectiveUnauditable = {
1529
1663
  endLine: line,
1530
1664
  rule: RULE.UNAUDITABLE,
1531
1665
  message: `${s.selector}: unaudited, ${s.reason}`,
1532
- remediation: "Declare the missing property or fix the colour so the pair can be scored."
1666
+ remediation: "Declare the missing property, fix the colour, or remove the config the audit cannot model, so the pair can be scored."
1533
1667
  });
1534
1668
  }
1535
1669
  }
@@ -1546,11 +1680,11 @@ var contrastTextBelowAa = {
1546
1680
  category: "contrast",
1547
1681
  summary: "label text below 4.5:1 against its fill",
1548
1682
  options: CONTRAST_OPTIONS,
1549
- check: (ctx) => {
1683
+ check: async (ctx) => {
1550
1684
  const profile = profileOf(ctx);
1551
1685
  const found = [];
1552
1686
  for (const diagram of ctx.doc.diagrams) {
1553
- for (const p of contrastOf(diagram, profile).pairs) {
1687
+ for (const p of (await contrastOf(diagram, profile)).pairs) {
1554
1688
  if (p.kind !== "text" || p.passes)
1555
1689
  continue;
1556
1690
  const where = [p.theme, p.row].filter(Boolean).join("/");
@@ -1561,19 +1695,19 @@ var contrastTextBelowAa = {
1561
1695
  endLine: line,
1562
1696
  rule: RULE.TEXT,
1563
1697
  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."
1698
+ 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 === "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
1699
  });
1566
1700
  }
1567
1701
  }
1568
1702
  return found;
1569
1703
  },
1570
- fix: (ctx) => {
1704
+ fix: async (ctx) => {
1571
1705
  const profile = profileOf(ctx);
1572
1706
  if (profile === "mkdocs-material")
1573
1707
  return [];
1574
1708
  const edits = [];
1575
1709
  for (const diagram of ctx.doc.diagrams) {
1576
- const { pairs } = contrastOf(diagram, profile);
1710
+ const { pairs } = await contrastOf(diagram, profile);
1577
1711
  const failing = new Set(pairs.filter((p) => p.kind === "text" && !p.passes).map((p) => p.directive));
1578
1712
  for (const directive of failing) {
1579
1713
  const span = directive.spans.color;
@@ -1783,7 +1917,7 @@ import { parseArgs } from "node:util";
1783
1917
  // package.json
1784
1918
  var package_default = {
1785
1919
  name: "@jpeakai/mermaidjs-lint",
1786
- version: "0.4.2",
1920
+ version: "0.5.0",
1787
1921
  type: "module",
1788
1922
  description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
1789
1923
  license: "MIT",
@@ -1841,7 +1975,7 @@ var package_default = {
1841
1975
  "mdast-util-from-markdown": "^2.0.3",
1842
1976
  "mdast-util-frontmatter": "^2.0.1",
1843
1977
  "mdast-util-gfm": "^3.1.0",
1844
- mermaid: "^11.14.0",
1978
+ mermaid: "11.17.2",
1845
1979
  "micromark-extension-frontmatter": "^2.0.0",
1846
1980
  "micromark-extension-gfm": "^3.0.0"
1847
1981
  },
@@ -2269,7 +2403,7 @@ var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2269
2403
 
2270
2404
  // src/plugins/runtime.ts
2271
2405
  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";
2406
+ 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
2407
 
2274
2408
  // src/plugins/load.ts
2275
2409
  var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);