@jpeakai/mermaidjs-lint 0.7.1 → 0.7.2
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 +20 -21
- package/dist/bin.js +691 -578
- package/dist/index.js +691 -578
- package/dist/testing.js +471 -411
- package/dist/types/parse/contrast.d.ts +6 -1
- package/dist/types/parse/graph.d.ts +16 -0
- package/dist/types/rules/shared.d.ts +5 -1
- package/package.json +1 -1
- package/src/check.ts +13 -11
- package/src/cli.ts +153 -90
- package/src/files.ts +23 -18
- package/src/fix/engine.ts +82 -57
- package/src/fix/verify.ts +13 -8
- package/src/model.ts +1 -1
- package/src/parse/color.ts +1 -1
- package/src/parse/contrast.ts +308 -228
- package/src/parse/graph.ts +98 -0
- package/src/parse/metrics.ts +1 -1
- package/src/parse/stats.ts +111 -82
- package/src/plugins/discover.ts +13 -15
- package/src/plugins/index.ts +66 -37
- package/src/plugins/load.ts +94 -61
- package/src/rules/complexity.ts +1 -1
- package/src/rules/contrast-text-below-aa.ts +34 -21
- package/src/rules/parse-diagram-unparseable.ts +1 -1
- package/src/rules/parse-fence-unterminated.ts +5 -7
- package/src/rules/registry.ts +38 -16
- package/src/rules/shared.ts +8 -3
package/src/model.ts
CHANGED
|
@@ -137,7 +137,7 @@ export const buildDocModel = (src: string, kind: "markdown" | "diagram" = "markd
|
|
|
137
137
|
collectMermaid(tree, fences);
|
|
138
138
|
const diagrams = fences.flatMap((node, index) => {
|
|
139
139
|
const { start, end } = node.position ?? {};
|
|
140
|
-
if (!start
|
|
140
|
+
if (!(start && end) || start.offset === undefined || end.offset === undefined) return [];
|
|
141
141
|
return [
|
|
142
142
|
makeDiagram(src, lineStarts, {
|
|
143
143
|
index,
|
package/src/parse/color.ts
CHANGED
|
@@ -106,7 +106,7 @@ const parseProperties = (
|
|
|
106
106
|
const key = pair.slice(0, colon).trim().toLowerCase();
|
|
107
107
|
const valueRaw = pair.slice(colon + 1);
|
|
108
108
|
const value = valueRaw.trim();
|
|
109
|
-
if (!key
|
|
109
|
+
if (!(key && value)) continue;
|
|
110
110
|
properties[key] = value;
|
|
111
111
|
const lead = valueRaw.length - valueRaw.trimStart().length;
|
|
112
112
|
const start = rawStart + pairStart + colon + 1 + lead;
|
package/src/parse/contrast.ts
CHANGED
|
@@ -154,87 +154,109 @@ const scorePair = (kind: PairKind, d: ContrastSource, fg: string, bg: string): C
|
|
|
154
154
|
|
|
155
155
|
const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
156
156
|
|
|
157
|
+
// Records a directive that was not scored, and why.
|
|
158
|
+
const skip = (out: ContrastResult, d: ContrastSource, reason: string, blocking?: boolean): void => {
|
|
159
|
+
out.skipped.push({ selector: d.selector, line: d.line, reason, ...(blocking && { blocking }) });
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// A foreground colour and the background it is drawn on.
|
|
163
|
+
interface Colours {
|
|
164
|
+
fg: string;
|
|
165
|
+
bg: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
157
168
|
// A linkStyle color: is edge label text, scored on the label box in both themes.
|
|
158
169
|
// A linkStyle without one styles only the line, which has no label pair.
|
|
159
|
-
const scoreEdgeLabel = (
|
|
160
|
-
d: StyleDirective,
|
|
161
|
-
canvas: Canvas,
|
|
162
|
-
pairs: ContrastPair[],
|
|
163
|
-
skipped: SkippedDirective[],
|
|
164
|
-
): void => {
|
|
170
|
+
const scoreEdgeLabel = (d: StyleDirective, canvas: Canvas, out: ContrastResult): void => {
|
|
165
171
|
const { color } = d.properties;
|
|
166
172
|
if (!color) return;
|
|
167
173
|
try {
|
|
168
|
-
pairs.push(
|
|
174
|
+
out.pairs.push(
|
|
175
|
+
...THEMES.map((t) => scoreHostPair("text", d, t, { fg: color, bg: edgeLabelBackground(t, canvas) })),
|
|
176
|
+
);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
skip(out, d, `edge label text unparseable: ${message(err)}`, true);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// The boxes a github fill is drawn as. A translucent fill is scored on both
|
|
183
|
+
// canvases and the worse result gates. An opaque fill resolves the same on both
|
|
184
|
+
// and keeps one pair.
|
|
185
|
+
const githubBoxes = (fill: string, alpha: number, canvas: Canvas): Array<[string, Theme | undefined]> =>
|
|
186
|
+
alpha < 1 ? THEMES.map((t): [string, Theme] => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
|
|
187
|
+
|
|
188
|
+
// Scores one pair on one github box, or records the colour that will not parse.
|
|
189
|
+
const scoreGithubPair = (
|
|
190
|
+
out: ContrastResult,
|
|
191
|
+
d: StyleDirective,
|
|
192
|
+
kind: PairKind,
|
|
193
|
+
{ fg, bg, theme }: Colours & { theme: Theme | undefined },
|
|
194
|
+
): void => {
|
|
195
|
+
try {
|
|
196
|
+
out.pairs.push({ ...scorePair(kind, d, fg, bg), ...(theme && { theme }) });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
skip(out, d, `${kind} pair unparseable: ${message(err)}`, true);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// The alpha of a fill, or null after recording that it will not parse.
|
|
203
|
+
const fillAlpha = (out: ContrastResult, d: StyleDirective, fill: string): number | null => {
|
|
204
|
+
try {
|
|
205
|
+
return alphaOf(fill);
|
|
169
206
|
} catch (err) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
207
|
+
skip(out, d, `fill unparseable: ${message(err)}`, true);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// A classDef or style under github: its text and border pairs on every box.
|
|
213
|
+
const scoreGithubStyle = (d: StyleDirective, canvas: Canvas, out: ContrastResult): void => {
|
|
214
|
+
const { fill, color, stroke } = d.properties;
|
|
215
|
+
if (!fill) {
|
|
216
|
+
const reason =
|
|
217
|
+
color || stroke ? "no fill declared, so text and border contrast have no anchor" : "no color properties";
|
|
218
|
+
skip(out, d, reason);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const alpha = fillAlpha(out, d, fill);
|
|
222
|
+
if (alpha === null) return;
|
|
223
|
+
// A fully transparent fill declares "no box": nothing to anchor contrast to,
|
|
224
|
+
// and the renderer's own theme text already tracks light and dark.
|
|
225
|
+
if (alpha === 0) {
|
|
226
|
+
skip(out, d, "fill is fully transparent, so there is no box to anchor contrast");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const boxes = githubBoxes(fill, alpha, canvas);
|
|
230
|
+
if (!color) {
|
|
231
|
+
skip(
|
|
232
|
+
out,
|
|
233
|
+
d,
|
|
234
|
+
"no color: declared beside fill:, and the github profile requires one " +
|
|
235
|
+
"(if the host forces label text, use the mkdocs-material profile)",
|
|
236
|
+
true,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
for (const [bg, theme] of boxes) {
|
|
240
|
+
if (color) scoreGithubPair(out, d, "text", { fg: color, bg, theme });
|
|
241
|
+
if (stroke) scoreGithubPair(out, d, "border", { fg: stroke, bg, theme });
|
|
176
242
|
}
|
|
177
243
|
};
|
|
178
244
|
|
|
179
245
|
export const scoreGithub = (directives: StyleDirective[], canvas: Canvas = GITHUB_CANVAS): ContrastResult => {
|
|
180
|
-
const pairs:
|
|
181
|
-
const skipped: SkippedDirective[] = [];
|
|
246
|
+
const out: ContrastResult = { pairs: [], skipped: [] };
|
|
182
247
|
for (const d of directives) {
|
|
183
|
-
if (d.kind === "linkStyle") scoreEdgeLabel(d, canvas,
|
|
184
|
-
if (d.kind
|
|
185
|
-
const { fill, color, stroke } = d.properties;
|
|
186
|
-
const note = (reason: string, blocking?: boolean): void => {
|
|
187
|
-
skipped.push({ selector: d.selector, line: d.line, reason, ...(blocking && { blocking }) });
|
|
188
|
-
};
|
|
189
|
-
if (!fill) {
|
|
190
|
-
note(color || stroke ? "no fill declared, so text and border contrast have no anchor" : "no color properties");
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
let alpha: number;
|
|
194
|
-
try {
|
|
195
|
-
alpha = alphaOf(fill);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
note(`fill unparseable: ${message(err)}`, true);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
// A fully transparent fill declares "no box": nothing to anchor contrast to,
|
|
201
|
-
// and the renderer's own theme text already tracks light and dark.
|
|
202
|
-
if (alpha === 0) {
|
|
203
|
-
note("fill is fully transparent, so there is no box to anchor contrast");
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
// A translucent fill is scored on both canvases and the worse result gates.
|
|
207
|
-
// An opaque fill resolves the same on both and keeps one pair.
|
|
208
|
-
const boxes: Array<[string, Theme | undefined]> =
|
|
209
|
-
alpha < 1 ? THEMES.map((t): [string, Theme] => [compositeOver(fill, canvas[t]), t]) : [[fill, undefined]];
|
|
210
|
-
if (!color) {
|
|
211
|
-
note(
|
|
212
|
-
"no color: declared beside fill:, and the github profile requires one " +
|
|
213
|
-
"(if the host forces label text, use the mkdocs-material profile)",
|
|
214
|
-
true,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
for (const [box, theme] of boxes) {
|
|
218
|
-
if (color) {
|
|
219
|
-
try {
|
|
220
|
-
pairs.push({ ...scorePair("text", d, color, box), ...(theme && { theme }) });
|
|
221
|
-
} catch (err) {
|
|
222
|
-
note(`text pair unparseable: ${message(err)}`, true);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (stroke) {
|
|
226
|
-
try {
|
|
227
|
-
pairs.push({ ...scorePair("border", d, stroke, box), ...(theme && { theme }) });
|
|
228
|
-
} catch (err) {
|
|
229
|
-
note(`border pair unparseable: ${message(err)}`, true);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
248
|
+
if (d.kind === "linkStyle") scoreEdgeLabel(d, canvas, out);
|
|
249
|
+
if (d.kind === "classDef" || d.kind === "style") scoreGithubStyle(d, canvas, out);
|
|
233
250
|
}
|
|
234
|
-
return
|
|
251
|
+
return out;
|
|
235
252
|
};
|
|
236
253
|
|
|
237
|
-
const scoreHostPair = (
|
|
254
|
+
const scoreHostPair = (
|
|
255
|
+
kind: PairKind,
|
|
256
|
+
d: ContrastSource,
|
|
257
|
+
theme: Theme,
|
|
258
|
+
{ fg: fgRaw, bg: box }: Colours,
|
|
259
|
+
): ContrastPair => {
|
|
238
260
|
// The foreground may itself be translucent, so it composites onto the box.
|
|
239
261
|
const fg = compositeOver(fgRaw, box);
|
|
240
262
|
const assessment = wcagAssess(fg, box);
|
|
@@ -252,56 +274,43 @@ const scoreHostPair = (kind: PairKind, d: ContrastSource, theme: Theme, fgRaw: s
|
|
|
252
274
|
};
|
|
253
275
|
};
|
|
254
276
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (!fill) {
|
|
272
|
-
note("no fill declared, and the host theme controls the text");
|
|
273
|
-
continue;
|
|
277
|
+
// A classDef or style under mkdocs-material: the host text on the fill, and
|
|
278
|
+
// the border, in each theme.
|
|
279
|
+
const scoreMkdocsStyle = (d: StyleDirective, out: ContrastResult): void => {
|
|
280
|
+
const { fill, stroke, color } = d.properties;
|
|
281
|
+
if (!fill) {
|
|
282
|
+
skip(out, d, "no fill declared, and the host theme controls the text");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (color) skip(out, d, "color: ignored, because the host theme forces label text");
|
|
286
|
+
for (const theme of THEMES) {
|
|
287
|
+
let box: string;
|
|
288
|
+
try {
|
|
289
|
+
box = compositeOver(fill, MKDOCS_MATERIAL[theme].bg);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
skip(out, d, `fill unparseable: ${message(err)}`, true);
|
|
292
|
+
break;
|
|
274
293
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
let box: string;
|
|
294
|
+
out.pairs.push(scoreHostPair("text", d, theme, { fg: MKDOCS_MATERIAL[theme].text, bg: box }));
|
|
295
|
+
if (stroke) {
|
|
278
296
|
try {
|
|
279
|
-
|
|
297
|
+
out.pairs.push(scoreHostPair("border", d, theme, { fg: stroke, bg: box }));
|
|
280
298
|
} catch (err) {
|
|
281
|
-
|
|
282
|
-
selector: d.selector,
|
|
283
|
-
line: d.line,
|
|
284
|
-
reason: `fill unparseable: ${message(err)}`,
|
|
285
|
-
blocking: true,
|
|
286
|
-
});
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
pairs.push(scoreHostPair("text", d, theme, MKDOCS_MATERIAL[theme].text, box));
|
|
290
|
-
if (stroke) {
|
|
291
|
-
try {
|
|
292
|
-
pairs.push(scoreHostPair("border", d, theme, stroke, box));
|
|
293
|
-
} catch (err) {
|
|
294
|
-
skipped.push({
|
|
295
|
-
selector: d.selector,
|
|
296
|
-
line: d.line,
|
|
297
|
-
reason: `border pair unparseable: ${message(err)}`,
|
|
298
|
-
blocking: true,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
299
|
+
skip(out, d, `border pair unparseable: ${message(err)}`, true);
|
|
301
300
|
}
|
|
302
301
|
}
|
|
303
302
|
}
|
|
304
|
-
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export const scoreMkdocs = (directives: StyleDirective[]): ContrastResult => {
|
|
306
|
+
const out: ContrastResult = { pairs: [], skipped: [] };
|
|
307
|
+
for (const d of directives) {
|
|
308
|
+
if (d.kind === "linkStyle" && d.properties.color) {
|
|
309
|
+
skip(out, d, "color: ignored, because the host theme forces edge label text");
|
|
310
|
+
}
|
|
311
|
+
if (d.kind === "classDef" || d.kind === "style") scoreMkdocsStyle(d, out);
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
305
314
|
};
|
|
306
315
|
|
|
307
316
|
// An erDiagram under the github profile. The flowchart model, where the
|
|
@@ -310,43 +319,48 @@ export const scoreMkdocs = (directives: StyleDirective[]): ContrastResult => {
|
|
|
310
319
|
// label is scored against both rows in both Mermaid themes. A missing color is
|
|
311
320
|
// not a blocking gap here, because dropping it for a translucent fill is the
|
|
312
321
|
// recipe that reads in both themes.
|
|
322
|
+
// The two erDiagram rows in one theme, each with the surface its labels sit on.
|
|
323
|
+
const erRows = (theme: Theme, fill: string | undefined): Array<[ErRow, string]> => {
|
|
324
|
+
const anchor = MERMAID_ER[theme];
|
|
325
|
+
return [
|
|
326
|
+
["odd", anchor.odd],
|
|
327
|
+
["even", fill ? compositeOver(fill, anchor.even) : anchor.even],
|
|
328
|
+
];
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Every pair one erDiagram directive draws, on both rows in both themes. It
|
|
332
|
+
// throws on a colour that will not parse, so the directive scores all or none.
|
|
333
|
+
const erPairs = (d: StyleDirective): ContrastPair[] => {
|
|
334
|
+
const { fill, color, stroke } = d.properties;
|
|
335
|
+
const scored: ContrastPair[] = [];
|
|
336
|
+
const score = (kind: PairKind, theme: Theme, row: ErRow, colours: Colours): void => {
|
|
337
|
+
scored.push({ ...scoreHostPair(kind, d, theme, colours), row, advisory: kind === "border" });
|
|
338
|
+
};
|
|
339
|
+
for (const theme of THEMES) {
|
|
340
|
+
for (const [row, bg] of erRows(theme, fill)) {
|
|
341
|
+
if (color || (row === "even" && fill)) score("text", theme, row, { fg: color ?? MERMAID_ER[theme].label, bg });
|
|
342
|
+
if (stroke) score("border", theme, row, { fg: stroke, bg });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return scored;
|
|
346
|
+
};
|
|
347
|
+
|
|
313
348
|
export const scoreEr = (directives: StyleDirective[]): ContrastResult => {
|
|
314
|
-
const pairs:
|
|
315
|
-
const skipped: SkippedDirective[] = [];
|
|
349
|
+
const out: ContrastResult = { pairs: [], skipped: [] };
|
|
316
350
|
for (const d of directives) {
|
|
317
351
|
if (d.kind !== "classDef" && d.kind !== "style") continue;
|
|
318
352
|
const { fill, color, stroke } = d.properties;
|
|
319
|
-
if (!fill
|
|
320
|
-
|
|
353
|
+
if (!(fill || color || stroke)) {
|
|
354
|
+
skip(out, d, "no color properties");
|
|
321
355
|
continue;
|
|
322
356
|
}
|
|
323
|
-
const scored: ContrastPair[] = [];
|
|
324
|
-
const score = (kind: PairKind, theme: Theme, row: ErRow, fg: string, bg: string): void => {
|
|
325
|
-
scored.push({ ...scoreHostPair(kind, d, theme, fg, bg), row, advisory: kind === "border" });
|
|
326
|
-
};
|
|
327
357
|
try {
|
|
328
|
-
|
|
329
|
-
const anchor = MERMAID_ER[theme];
|
|
330
|
-
const rows: Array<[ErRow, string]> = [
|
|
331
|
-
["odd", anchor.odd],
|
|
332
|
-
["even", fill ? compositeOver(fill, anchor.even) : anchor.even],
|
|
333
|
-
];
|
|
334
|
-
for (const [row, bg] of rows) {
|
|
335
|
-
if (color || (row === "even" && fill)) score("text", theme, row, color ?? anchor.label, bg);
|
|
336
|
-
if (stroke) score("border", theme, row, stroke, bg);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
pairs.push(...scored);
|
|
358
|
+
out.pairs.push(...erPairs(d));
|
|
340
359
|
} catch (err) {
|
|
341
|
-
|
|
342
|
-
selector: d.selector,
|
|
343
|
-
line: d.line,
|
|
344
|
-
reason: `erDiagram pair unparseable: ${message(err)}`,
|
|
345
|
-
blocking: true,
|
|
346
|
-
});
|
|
360
|
+
skip(out, d, `erDiagram pair unparseable: ${message(err)}`, true);
|
|
347
361
|
}
|
|
348
362
|
}
|
|
349
|
-
return
|
|
363
|
+
return out;
|
|
350
364
|
};
|
|
351
365
|
|
|
352
366
|
// The canvas applies under github only. Material draws its own page, so the
|
|
@@ -460,53 +474,169 @@ const themeVariable = (selector: string, line: number): ContrastSource => ({
|
|
|
460
474
|
line,
|
|
461
475
|
});
|
|
462
476
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
): Promise<ContrastResult> => {
|
|
468
|
-
const pairs: ContrastPair[] = [];
|
|
469
|
-
const skipped: SkippedDirective[] = [];
|
|
477
|
+
// Where a sequence variable is set in the source. lineOf gives the line that
|
|
478
|
+
// sets a variable, or the keyword line when it is inherited, and rectLines the
|
|
479
|
+
// line of each rect in order.
|
|
480
|
+
const sequenceLines = (source: string): { lineOf: (key: string) => number; rectLines: number[] } => {
|
|
470
481
|
const lines = source.split("\n");
|
|
471
482
|
const keywordLine = lines.findIndex((l) => /^\s*sequenceDiagram\b/.test(l)) + 1;
|
|
472
|
-
// The line that sets a variable, or the keyword line when it is inherited.
|
|
473
483
|
const lineOf = (key: string): number => {
|
|
474
484
|
const found = lines.findLastIndex((l) => new RegExp(`\\b${key}["']?\\s*:`).test(l));
|
|
475
485
|
return found < 0 ? keywordLine : found + 1;
|
|
476
486
|
};
|
|
477
|
-
|
|
487
|
+
return { lineOf, rectLines: lines.flatMap((l, i) => (/^\s*rect\s/.test(l) ? [i + 1] : [])) };
|
|
488
|
+
};
|
|
478
489
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
490
|
+
// The merged config and sequence DB, or null when the parser cannot read the
|
|
491
|
+
// diagram. parse-diagram-unparseable reports it. As for complexity, a diagram
|
|
492
|
+
// the parser cannot read has no colours to trust.
|
|
493
|
+
const parseSequence = async (
|
|
494
|
+
mermaid: MermaidCore,
|
|
495
|
+
source: string,
|
|
496
|
+
): Promise<{ config: Variables; db: SequenceDb } | null> => {
|
|
482
497
|
try {
|
|
483
|
-
config =
|
|
484
|
-
db =
|
|
498
|
+
const { config } = await mermaid.parse(source);
|
|
499
|
+
const { db } = await mermaid.mermaidAPI.getDiagramFromText(source);
|
|
500
|
+
return { config, db: db as SequenceDb };
|
|
485
501
|
} catch {
|
|
486
|
-
|
|
487
|
-
// parser cannot read has no colours to trust.
|
|
488
|
-
return { pairs, skipped };
|
|
502
|
+
return null;
|
|
489
503
|
}
|
|
504
|
+
};
|
|
490
505
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return block("themeCSS", "themeCSS restyles the diagram with CSS the audit cannot model");
|
|
506
|
+
// Why a sequence config cannot be modelled, as the key to blame and the
|
|
507
|
+
// reason, or null when it can. Under mkdocs-material a diagram's themeCSS
|
|
508
|
+
// replaces Material's own, so neither profile can say what it draws.
|
|
509
|
+
const unmodelled = (config: Variables): { selector: string; reason: string } | null => {
|
|
510
|
+
if (config.themeCSS !== undefined) {
|
|
511
|
+
return { selector: "themeCSS", reason: "themeCSS restyles the diagram with CSS the audit cannot model" };
|
|
512
|
+
}
|
|
499
513
|
const pinned = config.theme;
|
|
500
514
|
if (pinned !== undefined && !SEQUENCE_THEMES.includes(String(pinned))) {
|
|
501
|
-
return
|
|
515
|
+
return {
|
|
516
|
+
selector: "theme",
|
|
517
|
+
reason: `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`,
|
|
518
|
+
};
|
|
502
519
|
}
|
|
520
|
+
return null;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// The Mermaid theme drawn in each reader theme: a pinned theme in both, or
|
|
524
|
+
// else what the host draws.
|
|
525
|
+
const sequenceThemes = (pinned: unknown, host: boolean): Record<Theme, string> => {
|
|
526
|
+
const theme = (fallback: string): string => (pinned === undefined ? fallback : String(pinned));
|
|
527
|
+
return { light: theme("default"), dark: theme(host ? "default" : "dark") };
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// What one reader theme's sequence pairs are scored with.
|
|
531
|
+
interface SequenceTheme {
|
|
532
|
+
theme: Theme;
|
|
533
|
+
page: string;
|
|
534
|
+
vars: Variables;
|
|
535
|
+
host: boolean;
|
|
536
|
+
lineOf: (key: string) => number;
|
|
537
|
+
out: ContrastResult;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
type SequenceBackground = { key: string } | { colour: string };
|
|
541
|
+
|
|
542
|
+
// Scores fgKey drawn on a background: a variable, or a colour such as the page.
|
|
543
|
+
type SequencePair = (
|
|
544
|
+
kind: PairKind,
|
|
545
|
+
fgKey: string,
|
|
546
|
+
bg: SequenceBackground,
|
|
547
|
+
where?: { suffix?: string; at?: number | undefined },
|
|
548
|
+
) => void;
|
|
549
|
+
|
|
550
|
+
const sequencePair =
|
|
551
|
+
({ theme: t, page, vars, host, lineOf, out }: SequenceTheme): SequencePair =>
|
|
552
|
+
(kind, fgKey, bg, { suffix = "", at } = {}) => {
|
|
553
|
+
const color = (key: string): string => {
|
|
554
|
+
const value = vars[key];
|
|
555
|
+
if (typeof value !== "string") throw new Error(`${key} is not a resolved colour`);
|
|
556
|
+
return value;
|
|
557
|
+
};
|
|
558
|
+
const selector = fgKey + suffix;
|
|
559
|
+
const line = at ?? lineOf(fgKey);
|
|
560
|
+
try {
|
|
561
|
+
const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
|
|
562
|
+
out.pairs.push({
|
|
563
|
+
...scoreHostPair(kind, themeVariable(selector, line), t, { fg: color(fgKey), bg: box }),
|
|
564
|
+
advisory: host,
|
|
565
|
+
});
|
|
566
|
+
} catch (err) {
|
|
567
|
+
out.skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// The surfaces that escape Material's themeCSS: lifelines, arrowheads and
|
|
572
|
+
// activation bars.
|
|
573
|
+
const scoreHostSequence = (pair: SequencePair, page: string, features: SequenceFeatures): void => {
|
|
574
|
+
const onPage = { colour: page };
|
|
575
|
+
pair("border", "actorLineColor", onPage);
|
|
576
|
+
pair("border", "signalColor", onPage, { suffix: " (arrowhead)" });
|
|
577
|
+
if (features.activation) pair("border", "activationBorderColor", { key: "activationBkgColor" });
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// Every pair a sequence diagram draws when the author controls its colours.
|
|
581
|
+
const scoreAuthorSequence = (
|
|
582
|
+
pair: SequencePair,
|
|
583
|
+
page: string,
|
|
584
|
+
features: SequenceFeatures,
|
|
585
|
+
rectLines: number[],
|
|
586
|
+
): void => {
|
|
587
|
+
const onPage = { colour: page };
|
|
588
|
+
pair("text", "actorTextColor", { key: "actorBkg" });
|
|
589
|
+
pair("border", "actorBorder", { key: "actorBkg" });
|
|
590
|
+
// A human actor's name and stick figure sit outside the box fill.
|
|
591
|
+
if (features.humanActor) {
|
|
592
|
+
pair("text", "actorTextColor", onPage, { suffix: " (human actor label)" });
|
|
593
|
+
pair("border", "actorBorder", onPage, { suffix: " (human actor glyph)" });
|
|
594
|
+
}
|
|
595
|
+
pair("border", "actorLineColor", onPage);
|
|
596
|
+
pair("text", "signalTextColor", onPage);
|
|
597
|
+
pair("border", "signalColor", onPage);
|
|
598
|
+
if (features.note) pair("text", "noteTextColor", { key: "noteBkgColor" });
|
|
599
|
+
if (features.frame) {
|
|
600
|
+
pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
|
|
601
|
+
pair("text", "loopTextColor", onPage);
|
|
602
|
+
pair("border", "labelBoxBorderColor", onPage, { suffix: " (loop frame)" });
|
|
603
|
+
}
|
|
604
|
+
if (features.activation) pair("border", "activationBorderColor", { key: "activationBkgColor" });
|
|
605
|
+
if (features.numbered) pair("text", "sequenceNumberColor", { key: "signalColor" });
|
|
606
|
+
// Messages and frame text inside a rect sit on its colour, not the page.
|
|
607
|
+
for (const [i, rect] of features.rects.entries()) {
|
|
608
|
+
const fill = { colour: rect };
|
|
609
|
+
const where = { suffix: " (rect)", at: rectLines[i] };
|
|
610
|
+
pair("text", "signalTextColor", fill, where);
|
|
611
|
+
pair("border", "signalColor", fill, where);
|
|
612
|
+
if (features.frame) pair("text", "loopTextColor", fill, where);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
export const scoreSequence = async (
|
|
617
|
+
source: string,
|
|
618
|
+
profile: Profile,
|
|
619
|
+
canvas: Canvas = GITHUB_CANVAS,
|
|
620
|
+
): Promise<ContrastResult> => {
|
|
621
|
+
const out: ContrastResult = { pairs: [], skipped: [] };
|
|
622
|
+
const { lineOf, rectLines } = sequenceLines(source);
|
|
623
|
+
const mermaid = await getMermaidCore();
|
|
624
|
+
const parsed = await parseSequence(mermaid, source);
|
|
625
|
+
if (!parsed) return out;
|
|
626
|
+
const { config, db } = parsed;
|
|
627
|
+
|
|
628
|
+
const block = (selector: string, reason: string): ContrastResult => {
|
|
629
|
+
out.skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
|
|
630
|
+
return out;
|
|
631
|
+
};
|
|
632
|
+
const problem = unmodelled(config);
|
|
633
|
+
if (problem) return block(problem.selector, problem.reason);
|
|
503
634
|
|
|
504
635
|
const features = sequenceFeatures(db);
|
|
505
636
|
const host = profile === "mkdocs-material";
|
|
506
|
-
const theme = (fallback: string): string => (pinned === undefined ? fallback : String(pinned));
|
|
507
637
|
let variables: Record<Theme, Variables>;
|
|
508
638
|
try {
|
|
509
|
-
variables = resolveVariables(mermaid, config,
|
|
639
|
+
variables = resolveVariables(mermaid, config, sequenceThemes(config.theme, host));
|
|
510
640
|
} catch (err) {
|
|
511
641
|
// A colour the theme derives others from, such as actorBorder, fails here.
|
|
512
642
|
return block("themeVariables", `Mermaid could not resolve the theme: ${message(err)}`);
|
|
@@ -514,75 +644,25 @@ export const scoreSequence = async (
|
|
|
514
644
|
|
|
515
645
|
for (const t of THEMES) {
|
|
516
646
|
const page = host ? MKDOCS_MATERIAL[t].bg : canvas[t];
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (typeof value !== "string") throw new Error(`${key} is not a resolved colour`);
|
|
521
|
-
return value;
|
|
522
|
-
};
|
|
523
|
-
// fgKey drawn on a background: a variable, or a colour such as the page.
|
|
524
|
-
const pair = (
|
|
525
|
-
kind: PairKind,
|
|
526
|
-
fgKey: string,
|
|
527
|
-
bg: { key: string } | { colour: string },
|
|
528
|
-
suffix = "",
|
|
529
|
-
at?: number,
|
|
530
|
-
) => {
|
|
531
|
-
const selector = fgKey + suffix;
|
|
532
|
-
const line = at ?? lineOf(fgKey);
|
|
533
|
-
try {
|
|
534
|
-
const box = compositeOver("key" in bg ? color(bg.key) : bg.colour, page);
|
|
535
|
-
pairs.push({ ...scoreHostPair(kind, themeVariable(selector, line), t, color(fgKey), box), advisory: host });
|
|
536
|
-
} catch (err) {
|
|
537
|
-
skipped.push({ selector, line, reason: `${t} sequence pair unparseable: ${message(err)}`, blocking: true });
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
const onPage = { colour: page };
|
|
541
|
-
|
|
542
|
-
if (host) {
|
|
543
|
-
pair("border", "actorLineColor", onPage);
|
|
544
|
-
pair("border", "signalColor", onPage, " (arrowhead)");
|
|
545
|
-
if (features.activation) pair("border", "activationBorderColor", { key: "activationBkgColor" });
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
pair("text", "actorTextColor", { key: "actorBkg" });
|
|
550
|
-
pair("border", "actorBorder", { key: "actorBkg" });
|
|
551
|
-
// A human actor's name and stick figure sit outside the box fill.
|
|
552
|
-
if (features.humanActor) {
|
|
553
|
-
pair("text", "actorTextColor", onPage, " (human actor label)");
|
|
554
|
-
pair("border", "actorBorder", onPage, " (human actor glyph)");
|
|
555
|
-
}
|
|
556
|
-
pair("border", "actorLineColor", onPage);
|
|
557
|
-
pair("text", "signalTextColor", onPage);
|
|
558
|
-
pair("border", "signalColor", onPage);
|
|
559
|
-
if (features.note) pair("text", "noteTextColor", { key: "noteBkgColor" });
|
|
560
|
-
if (features.frame) {
|
|
561
|
-
pair("text", "labelTextColor", { key: "labelBoxBkgColor" });
|
|
562
|
-
pair("text", "loopTextColor", onPage);
|
|
563
|
-
pair("border", "labelBoxBorderColor", onPage, " (loop frame)");
|
|
564
|
-
}
|
|
565
|
-
if (features.activation) pair("border", "activationBorderColor", { key: "activationBkgColor" });
|
|
566
|
-
if (features.numbered) pair("text", "sequenceNumberColor", { key: "signalColor" });
|
|
567
|
-
// Messages and frame text inside a rect sit on its colour, not the page.
|
|
568
|
-
for (const [i, rect] of features.rects.entries()) {
|
|
569
|
-
const fill = { colour: rect };
|
|
570
|
-
const at = rectLines[i];
|
|
571
|
-
pair("text", "signalTextColor", fill, " (rect)", at);
|
|
572
|
-
pair("border", "signalColor", fill, " (rect)", at);
|
|
573
|
-
if (features.frame) pair("text", "loopTextColor", fill, " (rect)", at);
|
|
574
|
-
}
|
|
647
|
+
const pair = sequencePair({ theme: t, page, vars: variables[t], host, lineOf, out });
|
|
648
|
+
if (host) scoreHostSequence(pair, page, features);
|
|
649
|
+
else scoreAuthorSequence(pair, page, features, rectLines);
|
|
575
650
|
}
|
|
576
|
-
return
|
|
651
|
+
return out;
|
|
577
652
|
};
|
|
578
653
|
|
|
654
|
+
// What scoreDiagram reads off a diagram.
|
|
655
|
+
export interface ScoredDiagram {
|
|
656
|
+
source: string;
|
|
657
|
+
directives: StyleDirective[];
|
|
658
|
+
keyword: string | null;
|
|
659
|
+
}
|
|
660
|
+
|
|
579
661
|
// Scores a diagram by its keyword: a sequence diagram by its theme variables,
|
|
580
662
|
// any other by its style directives.
|
|
581
663
|
export const scoreDiagram = (
|
|
582
|
-
source:
|
|
583
|
-
directives: StyleDirective[],
|
|
664
|
+
{ source, directives, keyword }: ScoredDiagram,
|
|
584
665
|
profile: Profile,
|
|
585
|
-
keyword: string | null,
|
|
586
666
|
canvas: Canvas = GITHUB_CANVAS,
|
|
587
667
|
): Promise<ContrastResult> =>
|
|
588
668
|
keyword === "sequenceDiagram"
|