@jpeakai/mermaidjs-lint 0.4.1 → 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 +2 -2
- package/dist/bin.js +151 -15
- package/dist/index.js +151 -15
- package/dist/testing.js +148 -12
- package/dist/types/parse/contrast.d.ts +25 -1
- package/dist/types/parse/stats.d.ts +13 -0
- package/dist/types/plugins/runtime.d.ts +1 -1
- package/dist/types/rules/shared.d.ts +1 -1
- package/package.json +2 -2
- package/src/check.ts +3 -1
- package/src/parse/contrast.ts +230 -3
- package/src/parse/stats.ts +14 -0
- package/src/plugins/runtime.ts +3 -2
- package/src/rules/contrast-border-below-ui-minimum.ts +6 -3
- package/src/rules/contrast-directive-unauditable.ts +6 -4
- package/src/rules/contrast-text-below-aa.ts +9 -7
- package/src/rules/shared.ts +6 -4
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
|
|
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
|
|
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.
|
|
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: "
|
|
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 =
|
|
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
|
|
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;
|
|
@@ -1843,6 +1977,8 @@ var optionsFor = (rule, configured, flags) => {
|
|
|
1843
1977
|
if (value !== undefined && rule.options?.[name])
|
|
1844
1978
|
options[name] = value;
|
|
1845
1979
|
}
|
|
1980
|
+
if (typeof options.preset === "string")
|
|
1981
|
+
options.preset = canonicalPreset(options.preset) ?? options.preset;
|
|
1846
1982
|
return options;
|
|
1847
1983
|
};
|
|
1848
1984
|
var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
|
|
@@ -2267,7 +2403,7 @@ var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
|
2267
2403
|
|
|
2268
2404
|
// src/plugins/runtime.ts
|
|
2269
2405
|
var canImportTypeScript = (runtime = process) => typeof runtime.versions.bun === "string" || Boolean(runtime.features.typescript);
|
|
2270
|
-
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";
|
|
2271
2407
|
|
|
2272
2408
|
// src/plugins/load.ts
|
|
2273
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 =
|
|
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
|
|
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;
|
|
@@ -1765,6 +1899,8 @@ var optionsFor = (rule, configured, flags) => {
|
|
|
1765
1899
|
if (value !== undefined && rule.options?.[name])
|
|
1766
1900
|
options[name] = value;
|
|
1767
1901
|
}
|
|
1902
|
+
if (typeof options.preset === "string")
|
|
1903
|
+
options.preset = canonicalPreset(options.preset) ?? options.preset;
|
|
1768
1904
|
return options;
|
|
1769
1905
|
};
|
|
1770
1906
|
var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
|
|
@@ -1781,7 +1917,7 @@ import { parseArgs } from "node:util";
|
|
|
1781
1917
|
// package.json
|
|
1782
1918
|
var package_default = {
|
|
1783
1919
|
name: "@jpeakai/mermaidjs-lint",
|
|
1784
|
-
version: "0.
|
|
1920
|
+
version: "0.5.0",
|
|
1785
1921
|
type: "module",
|
|
1786
1922
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
1787
1923
|
license: "MIT",
|
|
@@ -1839,7 +1975,7 @@ var package_default = {
|
|
|
1839
1975
|
"mdast-util-from-markdown": "^2.0.3",
|
|
1840
1976
|
"mdast-util-frontmatter": "^2.0.1",
|
|
1841
1977
|
"mdast-util-gfm": "^3.1.0",
|
|
1842
|
-
mermaid: "
|
|
1978
|
+
mermaid: "11.17.2",
|
|
1843
1979
|
"micromark-extension-frontmatter": "^2.0.0",
|
|
1844
1980
|
"micromark-extension-gfm": "^3.0.0"
|
|
1845
1981
|
},
|
|
@@ -2267,7 +2403,7 @@ var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
|
2267
2403
|
|
|
2268
2404
|
// src/plugins/runtime.ts
|
|
2269
2405
|
var canImportTypeScript = (runtime = process) => typeof runtime.versions.bun === "string" || Boolean(runtime.features.typescript);
|
|
2270
|
-
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";
|
|
2271
2407
|
|
|
2272
2408
|
// src/plugins/load.ts
|
|
2273
2409
|
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|