@jpeakai/mermaidjs-lint 0.7.0 → 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/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 || !end || start.offset === undefined || end.offset === undefined) return [];
140
+ if (!(start && end) || start.offset === undefined || end.offset === undefined) return [];
141
141
  return [
142
142
  makeDiagram(src, lineStarts, {
143
143
  index,
@@ -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 || !value) continue;
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;
@@ -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(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t, canvas))));
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
- skipped.push({
171
- selector: d.selector,
172
- line: d.line,
173
- reason: `edge label text unparseable: ${message(err)}`,
174
- blocking: true,
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: ContrastPair[] = [];
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, pairs, skipped);
184
- if (d.kind !== "classDef" && d.kind !== "style") continue;
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 { pairs, skipped };
251
+ return out;
235
252
  };
236
253
 
237
- const scoreHostPair = (kind: PairKind, d: ContrastSource, theme: Theme, fgRaw: string, box: string): ContrastPair => {
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
- export const scoreMkdocs = (directives: StyleDirective[]): ContrastResult => {
256
- const pairs: ContrastPair[] = [];
257
- const skipped: SkippedDirective[] = [];
258
- for (const d of directives) {
259
- if (d.kind === "linkStyle" && d.properties.color) {
260
- skipped.push({
261
- selector: d.selector,
262
- line: d.line,
263
- reason: "color: ignored, because the host theme forces edge label text",
264
- });
265
- }
266
- if (d.kind !== "classDef" && d.kind !== "style") continue;
267
- const { fill, stroke, color } = d.properties;
268
- const note = (reason: string): void => {
269
- skipped.push({ selector: d.selector, line: d.line, reason });
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
- if (color) note("color: ignored, because the host theme forces label text");
276
- for (const theme of THEMES) {
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
- box = compositeOver(fill, MKDOCS_MATERIAL[theme].bg);
297
+ out.pairs.push(scoreHostPair("border", d, theme, { fg: stroke, bg: box }));
280
298
  } catch (err) {
281
- skipped.push({
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
- return { pairs, skipped };
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: ContrastPair[] = [];
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 && !color && !stroke) {
320
- skipped.push({ selector: d.selector, line: d.line, reason: "no color properties" });
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
- for (const theme of THEMES) {
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
- skipped.push({
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 { pairs, skipped };
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
- export const scoreSequence = async (
464
- source: string,
465
- profile: Profile,
466
- canvas: Canvas = GITHUB_CANVAS,
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
- const rectLines = lines.flatMap((l, i) => (/^\s*rect\s/.test(l) ? [i + 1] : []));
487
+ return { lineOf, rectLines: lines.flatMap((l, i) => (/^\s*rect\s/.test(l) ? [i + 1] : [])) };
488
+ };
478
489
 
479
- const mermaid = await getMermaidCore();
480
- let config: Variables;
481
- let db: SequenceDb;
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 = (await mermaid.parse(source)).config;
484
- db = (await mermaid.mermaidAPI.getDiagramFromText(source)).db as SequenceDb;
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
- // parse-diagram-unparseable reports it. As for complexity, a diagram the
487
- // parser cannot read has no colours to trust.
488
- return { pairs, skipped };
502
+ return null;
489
503
  }
504
+ };
490
505
 
491
- const block = (selector: string, reason: string): ContrastResult => {
492
- skipped.push({ selector, line: lineOf(selector), reason, blocking: true });
493
- return { pairs, skipped };
494
- };
495
- // Under mkdocs-material a diagram's themeCSS replaces Material's own, so
496
- // neither profile can say what it draws.
497
- if (config.themeCSS !== undefined)
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 block("theme", `the ${String(pinned)} theme is not modelled, only ${SEQUENCE_THEMES.join(", ")}`);
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, { light: theme("default"), dark: theme(host ? "default" : "dark") });
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 vars = variables[t];
518
- const color = (key: string): string => {
519
- const value = vars[key];
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 { pairs, skipped };
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: string,
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"