@rse/specbook 1.3.2 → 1.4.1

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.
Files changed (50) hide show
  1. package/README.md +9 -2
  2. package/dst/specbook-api.d.ts +5 -1
  3. package/dst/specbook-api.js +23 -9
  4. package/dst/specbook-cli.js +8 -5
  5. package/dst/specbook-cmd-export.js +2 -2
  6. package/dst/specbook-cmd-init.js +16 -2
  7. package/dst/specbook-coverage.js +1 -1
  8. package/dst/specbook-diagram.d.ts +1 -1
  9. package/dst/specbook-diagram.js +12 -10
  10. package/dst/specbook-export-ast.d.ts +1 -1
  11. package/dst/specbook-export-ast.js +21 -4
  12. package/dst/specbook-export-code.d.ts +14 -0
  13. package/dst/specbook-export-code.js +155 -0
  14. package/dst/specbook-export-common.d.ts +8 -2
  15. package/dst/specbook-export-common.js +126 -6
  16. package/dst/specbook-export-diagram.d.ts +15 -0
  17. package/dst/specbook-export-diagram.js +211 -0
  18. package/dst/specbook-export-html-fold.d.ts +20 -0
  19. package/dst/specbook-export-html-fold.js +458 -0
  20. package/dst/specbook-export-html-render.d.ts +51 -0
  21. package/dst/specbook-export-html-render.js +748 -0
  22. package/dst/specbook-export-html-search.js +6 -5
  23. package/dst/specbook-export-html-template.d.ts +21 -0
  24. package/dst/specbook-export-html-template.js +282 -0
  25. package/dst/specbook-export-html-widget.d.ts +12 -0
  26. package/dst/specbook-export-html-widget.js +605 -0
  27. package/dst/specbook-export-html.css +325 -50
  28. package/dst/specbook-export-html.js +62 -1931
  29. package/dst/specbook-export-image.d.ts +2 -0
  30. package/dst/specbook-export-image.js +161 -9
  31. package/dst/specbook-export-md.js +60 -12
  32. package/dst/specbook-format-schema.d.ts +1 -1
  33. package/dst/specbook-format-schema.js +1 -1
  34. package/dst/specbook-format-spec.d.ts +1 -0
  35. package/dst/specbook-format-spec.js +1 -0
  36. package/dst/specbook-format.md +173 -19
  37. package/dst/specbook-format.yaml +265 -24
  38. package/dst/specbook-ignore.js +2 -0
  39. package/dst/specbook-link.js +21 -5
  40. package/dst/specbook-mcp.js +9 -5
  41. package/dst/specbook-parse-common.d.ts +24 -0
  42. package/dst/specbook-parse-common.js +137 -10
  43. package/dst/specbook-parse-semantic.js +2 -0
  44. package/dst/specbook-parse-syntax.js +81 -20
  45. package/dst/specbook-parse.js +1 -1
  46. package/dst/specbook-project.d.ts +1 -0
  47. package/dst/specbook-project.js +10 -1
  48. package/dst/specbook-theme.d.ts +1 -0
  49. package/dst/specbook-theme.js +2 -0
  50. package/package.json +18 -12
@@ -4,8 +4,9 @@
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
6
  import fs from "node:fs";
7
- import { embeddingThemes } from "./specbook-parse-common.js";
7
+ import { embeddingThemes, darkMark } from "./specbook-parse-common.js";
8
8
  import { plainText } from "./specbook-link.js";
9
+ import { themeToneDefault } from "./specbook-theme.js";
9
10
  /* escape a text for embedding into template HTML (text and attributes) */
10
11
  export const escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
11
12
  .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -19,9 +20,13 @@ export const searchScript = () => fs.readFileSync(new URL("specbook-export-html-
19
20
  export const fallbackLogo = (theme) => "data:image/svg+xml;base64," +
20
21
  fs.readFileSync(new URL(`specbook-export-logo-${theme}.svg`, import.meta.url)).toString("base64");
21
22
  /* the content aspects an export can omit, each matching the set of a
22
- folding control of the HTML export: the diagrams of a type, the
23
- diagrams from an object tree nesting level on, and the long texts */
23
+ folding control of the HTML export: the diagrams of a type (the
24
+ derived Gradia ones, the embedded Mermaid/D2 ones as "code", the
25
+ embedded images and PDF pages as "image", and the code listings as
26
+ "listing"), the diagrams from an object tree nesting level on, and the
27
+ long texts */
24
28
  export const omitAspects = ["diagram:graph", "diagram:hub", "diagram:grid",
29
+ "diagram:code", "diagram:image", "diagram:listing",
25
30
  "diagram:1", "diagram:2", "diagram:3", "text:long"];
26
31
  /* parse the (comma-separated) aspects to omit, where the
27
32
  plain "diagram" is the alias of "diagram:1", hence of all diagrams */
@@ -107,6 +112,21 @@ export const documentPaperSize = (specification) => {
107
112
  `(expected ${paperSizes.join(", ")})`);
108
113
  return paper;
109
114
  };
115
+ /* the properties of the title object the exports interpret themselves,
116
+ each with the value "init" seeds it with (its rendering default, an
117
+ empty value counting as an absent property) */
118
+ export const titleProperties = [
119
+ { name: "LOGO", init: "" },
120
+ { name: "TITLE", init: "Specification" },
121
+ { name: "SUBTITLE", init: "" },
122
+ { name: "AUTHOR", init: "" },
123
+ { name: "VERSION", init: "" },
124
+ { name: "LANG", init: "en" },
125
+ { name: "CHARSET", init: "UTF-8" },
126
+ { name: "THEME-STYLE", init: "Light" },
127
+ { name: "THEME-TONE", init: themeToneDefault },
128
+ { name: "PAPER-SIZE", init: paperSizeDefault }
129
+ ];
110
130
  /* the vertical room (in rem) the introducing heading of a diagram
111
131
  claims above it on the same page: without this reserve a maximally
112
132
  sized diagram could not share its page with the heading, which
@@ -129,7 +149,7 @@ export const paperStylesheet = (paper) => {
129
149
  const avail = setup.height - setup.margin.top - setup.margin.bottom;
130
150
  return "@media print {\n" +
131
151
  "div.diagram { break-inside: avoid; }\n" +
132
- `div.diagram svg { max-height: calc(${paperLength(setup, avail)} - ${headingReserve}rem);` +
152
+ `div.diagram svg, div.diagram img { max-height: calc(${paperLength(setup, avail)} - ${headingReserve}rem);` +
133
153
  " width: auto; height: auto; }\n" +
134
154
  `nav.doc div.diagram svg { max-height: calc(${paperLength(setup, avail)} - ${docHeadingReserve}rem); }\n` +
135
155
  "}\n";
@@ -223,6 +243,105 @@ export const documentTitle = (specification) => {
223
243
  subtitle: subtitle !== undefined ? plainText(subtitle) : undefined
224
244
  };
225
245
  };
246
+ /* the luma (0-1, of the gamma-encoded channels) of a CSS color given as
247
+ "#rgb", "#rrggbb", "rgb(r, g, b)", "white", or "black" (undefined for
248
+ any other one) */
249
+ const luminance = (color) => {
250
+ const value = color.trim().toLowerCase();
251
+ const named = value === "white" ? "#ffffff" : value === "black" ? "#000000" : value;
252
+ const hex = named.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
253
+ const rgb = named.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
254
+ const parts = hex !== null ?
255
+ (hex[1].length === 3 ? hex[1].split("").map((c) => c + c) : hex[1].match(/../g) ?? [])
256
+ .map((c) => parseInt(c, 16)) :
257
+ rgb !== null ? rgb.slice(1, 4).map(Number) : undefined;
258
+ return parts !== undefined ? (0.2126 * parts[0] + 0.7152 * parts[1] + 0.0722 * parts[2]) / 255 : undefined;
259
+ };
260
+ /* whether an SVG image is most likely dark ink on a transparent or light
261
+ canvas and hence safe to invert on the dark theme ("dark=auto"): it
262
+ neither adapts itself (via "light-dark()" colors, "prefers-color-scheme"
263
+ rules, or a "color-scheme" declaration), nor embeds a raster image
264
+ (which would turn into a negative), nor paints a background of its own
265
+ which is not light (by the style of its root element or by the topmost
266
+ of the leading shapes covering the whole canvas -- rectangles or
267
+ rectangular paths, as a rendered PDF page paints its background over
268
+ the white paper of PDF.js --, an unknown color counting as not light) */
269
+ export const svgInvertible = (svg) => {
270
+ if (/light-dark\(|prefers-color-scheme|color-scheme|<image\b/.test(svg))
271
+ return false;
272
+ const root = svg.match(/<svg\b[^>]*>/)?.[0] ?? "";
273
+ const light = (color) => (luminance(color ?? "#000000") ?? 0) >= 0.5;
274
+ const back = root.match(/background(?:-color)?\s*:\s*([^;"]+)/)?.[1];
275
+ if (back !== undefined && !/^\s*(?:none|transparent)\s*$/.test(back) && !light(back))
276
+ return false;
277
+ /* the canvas size (in the user units of the viewBox, if any) */
278
+ const attr = (element, name) => element.match(new RegExp(`\\s${name}="([^"]*)"`))?.[1] ??
279
+ element.match(new RegExp(`[\\s;"]${name}\\s*:\\s*([^;"]+)`))?.[1];
280
+ const box = attr(root, "viewBox")?.trim().split(/[\s,]+/).map(Number) ?? [];
281
+ const width = box[2] || parseFloat(attr(root, "width") ?? "");
282
+ const height = box[3] || parseFloat(attr(root, "height") ?? "");
283
+ /* whether a shape covers the whole canvas: the corner points of a
284
+ rectangle or of a path of straight absolute segments only, mapped
285
+ through its "scale()" or "matrix()" transform (if any) */
286
+ const covers = (kind, element) => {
287
+ const num = (name) => parseFloat(attr(element, name) ?? "0") || 0;
288
+ let points;
289
+ if (kind === "rect") {
290
+ if (attr(element, "width") === "100%" && attr(element, "height") === "100%")
291
+ return true;
292
+ const [x, y] = [num("x"), num("y")];
293
+ points = [[x, y], [x + num("width"), y + num("height")]];
294
+ }
295
+ else {
296
+ const d = attr(element, "d") ?? "";
297
+ if (kind !== "path" || !/^[\sMLHVZ\d.,-]*$/.test(d))
298
+ return false;
299
+ points = [];
300
+ for (const [, cmd, args] of d.matchAll(/([MLHVZ])([^MLHVZ]*)/g)) {
301
+ const n = args.trim().split(/[\s,]+/).filter((a) => a !== "").map(Number);
302
+ const last = points.at(-1) ?? [0, 0];
303
+ if (cmd === "H")
304
+ points.push(...n.map((x) => [x, last[1]]));
305
+ else if (cmd === "V")
306
+ points.push(...n.map((y) => [last[0], y]));
307
+ else
308
+ for (let k = 0; k + 1 < n.length; k += 2)
309
+ points.push([n[k], n[k + 1]]);
310
+ }
311
+ }
312
+ const t = attr(element, "transform")?.trim() ?? "";
313
+ const f = t.match(/^(matrix|scale)\(([^)]*)\)$/);
314
+ const v = f?.[2].trim().split(/[\s,]+/).map(Number) ?? [];
315
+ const m = t === "" ? [1, 0, 0, 1, 0, 0] :
316
+ f?.[1] === "matrix" && v.length === 6 ? v :
317
+ f?.[1] === "scale" ? [v[0], 0, 0, v[1] ?? v[0], 0, 0] : undefined;
318
+ if (m === undefined || points.length === 0)
319
+ return false;
320
+ const xs = points.map(([x, y]) => m[0] * x + m[2] * y + m[4]);
321
+ const ys = points.map(([x, y]) => m[1] * x + m[3] * y + m[5]);
322
+ return Math.min(...xs) <= 1 && Math.min(...ys) <= 1
323
+ && Math.max(...xs) >= width - 1 && Math.max(...ys) >= height - 1;
324
+ };
325
+ /* the topmost leading canvas-covering shape (outside of any
326
+ definition) forms the background */
327
+ let fill;
328
+ const body = svg.slice(svg.indexOf(root) + root.length)
329
+ .replace(/<(clipPath|defs|mask|pattern|symbol)\b[\s\S]*?<\/\1>/g, "");
330
+ for (const shape of body.matchAll(/<(rect|path|circle|ellipse|line|polyline|polygon|text|use)\b[^>]*>/g)) {
331
+ if (!covers(shape[1], shape[0]))
332
+ break;
333
+ fill = attr(shape[0], "fill") ?? "#000000";
334
+ }
335
+ return fill === undefined || fill === "none" || fill === "transparent" || light(fill);
336
+ };
337
+ /* the treatment of an embedded image on the dark theme: its explicit
338
+ mark, else the "auto" detection (for a raster image or a PDF page the
339
+ judgement of "analyzeImages", none where absent) */
340
+ export const imageDark = (content, rasters) => {
341
+ const { content: plain, dark } = darkMark(content);
342
+ return dark ?? ((plain.startsWith("data:") ? rasters?.get(plain) === true : svgInvertible(plain)) ?
343
+ "invert" : "none");
344
+ };
226
345
  /* determine the document logo as a self-contained data: URL, taken from the
227
346
  first embedded image of the LOGO property of the title object -- the light
228
347
  variant of a "{theme}" reference -- and falling back onto the built-in
@@ -233,6 +352,7 @@ export const documentLogo = (specification) => {
233
352
  ?.properties.find((property) => property.key === "LOGO")?.embedding?.[0];
234
353
  if (content === undefined || content === "")
235
354
  return fallbackLogo("light");
236
- return content.startsWith("data:") ? content :
237
- `data:image/svg+xml;base64,${Buffer.from(content, "utf8").toString("base64")}`;
355
+ const plain = darkMark(content).content;
356
+ return plain.startsWith("data:") ? plain :
357
+ `data:image/svg+xml;base64,${Buffer.from(plain, "utf8").toString("base64")}`;
238
358
  };
@@ -0,0 +1,15 @@
1
+ import type { Spec, SpecObject } from "./specbook-format-spec.js";
2
+ import { type OmitAspect } from "./specbook-export-common.js";
3
+ import { type ThemeStyle } from "./specbook-theme.js";
4
+ import { type Verbose } from "./specbook-verbose.js";
5
+ export type DiagramLanguage = "mermaid" | "d2";
6
+ export type DiagramVariants = Record<ThemeStyle, string>;
7
+ export declare const fenceLanguage: (lang: string | undefined) => DiagramLanguage | undefined;
8
+ export declare const embeddedSource: (content: string) => {
9
+ language: DiagramLanguage;
10
+ source: string;
11
+ } | undefined;
12
+ export declare const diagramKey: (language: DiagramLanguage, source: string) => string;
13
+ export declare const omittedDiagram: (language: DiagramLanguage | "image" | "listing", level: number, omit: Set<OmitAspect>) => boolean;
14
+ export declare const objectLevels: (specification: Spec) => Map<SpecObject, number>;
15
+ export declare const renderEmbeddedDiagrams: (specification: Spec, verbose?: Verbose, omit?: Set<"diagram:graph" | "diagram:hub" | "diagram:grid" | "diagram:code" | "diagram:image" | "diagram:listing" | "diagram:1" | "diagram:2" | "diagram:3" | "text:long">) => Promise<Map<string, DiagramVariants>>;
@@ -0,0 +1,211 @@
1
+ /*
2
+ ** Specification Book (SpecBook)
3
+ ** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+ import { marked } from "marked";
7
+ import { documentThemeTone } from "./specbook-export-common.js";
8
+ import { themeColors, themeToneDefault } from "./specbook-theme.js";
9
+ import { plainText } from "./specbook-link.js";
10
+ import { literal } from "./specbook-verbose.js";
11
+ /* the fenced code block languages of the embedded diagrams */
12
+ const fenceLanguages = {
13
+ mermaid: "mermaid",
14
+ mmd: "mermaid",
15
+ d2: "d2"
16
+ };
17
+ /* map the language of a fenced code block onto its diagram language
18
+ (undefined for a regular code block) */
19
+ export const fenceLanguage = (lang) => fenceLanguages[(lang ?? "").trim().toLowerCase()];
20
+ /* decode an embedded diagram source file, which the parser loads as a
21
+ base64 data: URL of its diagram MIME type (undefined for an image) */
22
+ export const embeddedSource = (content) => {
23
+ const m = content.match(/^data:text\/vnd\.(mermaid|d2);base64,(.*)$/s);
24
+ return m !== null ?
25
+ { language: m[1], source: Buffer.from(m[2], "base64").toString("utf8") } :
26
+ undefined;
27
+ };
28
+ /* the key of an embedded diagram in the map of the rendered ones */
29
+ export const diagramKey = (language, source) => `${language}\u0000${source}`;
30
+ /* whether an embedded diagram of a language (a Mermaid/D2 one being of
31
+ the type "code"), an image, or a code listing, carried by an object of
32
+ a nesting level, is omitted (exactly like a Gradia diagram: by its
33
+ type, or from an object tree nesting level on) */
34
+ export const omittedDiagram = (language, level, omit) => omit.has(language === "image" ? "diagram:image" : language === "listing" ? "diagram:listing" : "diagram:code")
35
+ || [1, 2, 3].some((n) => n <= level && omit.has(`diagram:${n}`));
36
+ /* the object tree nesting levels of the objects of a specification
37
+ (the top-level objects of an artifact being level 1) */
38
+ export const objectLevels = (specification) => {
39
+ const levels = new Map();
40
+ const walk = (object, level) => {
41
+ levels.set(object, level);
42
+ for (const child of object.children)
43
+ walk(child, level + 1);
44
+ };
45
+ for (const artifact of specification.artifacts)
46
+ for (const object of artifact.objects)
47
+ walk(object, 1);
48
+ return levels;
49
+ };
50
+ /* collect the embedded diagrams of an object and its descendants: the
51
+ fenced code blocks of a diagram language inside its description
52
+ statement and elaboration and the diagram source files embedded into
53
+ its description and property values */
54
+ const collect = (object, level, diagrams) => {
55
+ for (const text of [object.description?.description ?? "", object.description?.elaboration ?? ""])
56
+ marked.walkTokens(marked.lexer(text), (token) => {
57
+ const language = token.type === "code" ? fenceLanguage(token.lang) : undefined;
58
+ if (language !== undefined)
59
+ diagrams.push({ language, source: token.text, object, level });
60
+ });
61
+ const contents = [...object.description?.embedding ?? [],
62
+ ...object.properties.flatMap((property) => property.embedding ?? [])];
63
+ for (const content of contents) {
64
+ const embedded = embeddedSource(content);
65
+ if (embedded !== undefined)
66
+ diagrams.push({ ...embedded, object, level });
67
+ }
68
+ for (const child of object.children)
69
+ collect(child, level + 1, diagrams);
70
+ };
71
+ /* the colors of the Mermaid diagrams per theme style, picked out of the
72
+ theme color spreads like the layer-2 mapping of the HTML rendering
73
+ (Mermaid derives all further colors of a diagram from these) */
74
+ const mermaidColors = (colors, style) => {
75
+ const [b, a] = [colors.base, colors.accent];
76
+ return style === "dark" ?
77
+ { bg: b[6], fg: b[28], line: b[21], accent: a[21], muted: b[21], surface: a[7], border: a[13] } :
78
+ { bg: b[31], fg: b[5], line: b[21], accent: a[11], muted: b[21], surface: a[30], border: a[23] };
79
+ };
80
+ /* the colors of the D2 diagrams per theme style, picked out of the
81
+ theme color spreads after the shape of the D2 themes "Neutral
82
+ Default" (light) and "Dark Mauve" (dark): the neutrals "N1" (text)
83
+ to "N7" (background), the base colors "B1" (strokes) to "B6"
84
+ (fills), and the alternative accents "AA*"/"AB*" */
85
+ const d2Colors = (colors, style) => {
86
+ const [b, a, s] = [colors.base, colors.accent, colors.signal];
87
+ return style === "dark" ? {
88
+ N1: b[28], N2: b[25], N3: b[22], N4: b[13], N5: b[10], N6: b[8], N7: b[6],
89
+ B1: a[21], B2: a[21], B3: a[13], B4: a[10], B5: a[8], B6: a[7],
90
+ AA2: s[19], AA4: s[10], AA5: s[8], AB4: s[10], AB5: s[8]
91
+ } : {
92
+ N1: b[3], N2: b[13], N3: b[19], N4: b[25], N5: b[27], N6: b[29], N7: b[31],
93
+ B1: a[7], B2: a[11], B3: a[27], B4: a[28], B5: a[29], B6: a[30],
94
+ AA2: s[13], AA4: s[28], AA5: s[30], AB4: s[28], AB5: s[30]
95
+ };
96
+ };
97
+ /* render a Mermaid diagram, stripping the web font import the SVG
98
+ carries, as an SVG embedded through an <img> never loads it and it
99
+ would be the only external reference of the exported document */
100
+ const renderMermaid = async (source, colors) => {
101
+ const { renderMermaidSVG } = await import("beautiful-mermaid");
102
+ return renderMermaidSVG(source, { ...colors, transparent: true, padding: 4 })
103
+ .replace(/^\s*@import url\([^)]*\);\s*$/m, "");
104
+ };
105
+ /* the D2 compiler, created on first use (as it spins up its WebAssembly
106
+ build in a worker thread), and the chain of its requests, as it
107
+ resolves just the latest pending one and hence has to be fed one
108
+ request at a time: the worker is referenced during a request only,
109
+ as it would otherwise keep the process alive forever */
110
+ let d2 = null;
111
+ let d2Chain = Promise.resolve();
112
+ /* turn a D2 compile failure (a JSON-encoded list of the errors, whose
113
+ source lines are shifted by the prepended theme line) into a message */
114
+ const d2Error = (err) => {
115
+ const message = err instanceof Error ? err.message : String(err);
116
+ try {
117
+ return JSON.parse(message)
118
+ .map((error) => error.errmsg.replace(/^index:(\d+):/, (_, line) => `line ${Number(line) - 1}:`))
119
+ .join("; ");
120
+ }
121
+ catch {
122
+ return message;
123
+ }
124
+ };
125
+ /* render a D2 diagram, with the theme colors prepended as one line of
126
+ theme overrides (on the theme of the style), so the overrides of the
127
+ diagram itself still win */
128
+ const renderD2 = (source, colors, style) => {
129
+ const overrides = Object.entries(colors).map(([key, color]) => `${key}: "${color}"`).join("; ");
130
+ const request = d2Chain.then(async () => {
131
+ d2 ??= import("@terrastruct/d2").then((module) => new module.D2())
132
+ .catch((err) => {
133
+ d2 = null;
134
+ throw err;
135
+ });
136
+ const compiler = await d2;
137
+ const worker = () => compiler.worker;
138
+ worker()?.ref();
139
+ try {
140
+ const result = await compiler.compile({
141
+ fs: { index: `vars: { d2-config: { theme-overrides: { ${overrides} } } }\n${source}` },
142
+ options: { themeID: style === "dark" ? 200 : 0, scale: 1, pad: 0, noXMLTag: true }
143
+ });
144
+ return await compiler.render(result.diagram, result.renderOptions);
145
+ }
146
+ catch (err) {
147
+ throw new Error(d2Error(err), { cause: err });
148
+ }
149
+ finally {
150
+ worker()?.unref();
151
+ }
152
+ });
153
+ d2Chain = request.catch(() => undefined);
154
+ return request;
155
+ };
156
+ /* the in-memory cache of the rendered embedded diagrams, keyed by theme
157
+ tone and diagram, and swept to the diagrams of the latest rendering,
158
+ exactly like the one of the Gradia diagrams */
159
+ let diagramCache = new Map();
160
+ /* render the embedded diagrams of a specification (the fenced code blocks
161
+ of a diagram language and the embedded diagram source files) into their
162
+ theme variants, drawn in the theme colors of the document and keyed by
163
+ "diagramKey" (a rendering failure omits the diagram and is surfaced as
164
+ a notice only, like the one of a Gradia diagram), served from the cache
165
+ where possible; the omitted diagrams are skipped */
166
+ export const renderEmbeddedDiagrams = async (specification, verbose, omit = new Set()) => {
167
+ const diagrams = new Array();
168
+ for (const artifact of specification.artifacts)
169
+ for (const object of artifact.objects)
170
+ collect(object, 1, diagrams);
171
+ const tone = documentThemeTone(specification) ?? themeToneDefault;
172
+ const colors = themeColors(tone);
173
+ const cache = new Map();
174
+ const rendered = new Map();
175
+ const failed = new Set();
176
+ let cached = 0;
177
+ for (const { language, source, object, level } of diagrams) {
178
+ const key = diagramKey(language, source);
179
+ if (rendered.has(key) || failed.has(key) || omittedDiagram(language, level, omit))
180
+ continue;
181
+ let variants = diagramCache.get(`${tone}\u0000${key}`) ?? cache.get(`${tone}\u0000${key}`);
182
+ if (variants !== undefined)
183
+ cached++;
184
+ else {
185
+ try {
186
+ variants = language === "mermaid" ? {
187
+ light: await renderMermaid(source, mermaidColors(colors, "light")),
188
+ dark: await renderMermaid(source, mermaidColors(colors, "dark"))
189
+ } : {
190
+ light: await renderD2(source, d2Colors(colors, "light"), "light"),
191
+ dark: await renderD2(source, d2Colors(colors, "dark"), "dark")
192
+ };
193
+ }
194
+ catch (err) {
195
+ verbose?.(`rendering ${language} diagram of ${object.kind} ` +
196
+ `"${literal(plainText(object.name))}" failed: ` +
197
+ (err instanceof Error ? err.message : String(err)));
198
+ failed.add(key);
199
+ continue;
200
+ }
201
+ }
202
+ cache.set(`${tone}\u0000${key}`, variants);
203
+ rendered.set(key, variants);
204
+ }
205
+ diagramCache = cache;
206
+ if (rendered.size > 0)
207
+ verbose?.(cached === rendered.size ?
208
+ `reusing ${literal(cached)} cached embedded diagram(s)` :
209
+ `rendering ${literal(rendered.size)} embedded diagram(s) (${literal(cached)} cached)`);
210
+ return rendered;
211
+ };
@@ -0,0 +1,20 @@
1
+ import type nunjucks from "nunjucks";
2
+ import type { OmitAspect } from "./specbook-export-common.js";
3
+ export declare const foldScript: (standins: boolean, textless: boolean) => string;
4
+ export declare const omitLong: (omit: Set<OmitAspect> | null, cells: nunjucks.runtime.SafeString[], shares: number[], percent?: number) => nunjucks.runtime.SafeString[];
5
+ export declare const omittedControls: (omit: Set<OmitAspect>) => {
6
+ omitted: {
7
+ graph: boolean;
8
+ hub: boolean;
9
+ grid: boolean;
10
+ code: boolean;
11
+ image: boolean;
12
+ listing: boolean;
13
+ level1: boolean;
14
+ level2: boolean;
15
+ level3: boolean;
16
+ text: boolean;
17
+ all: boolean;
18
+ };
19
+ css: string;
20
+ };