@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.
- package/README.md +9 -2
- package/dst/specbook-api.d.ts +5 -1
- package/dst/specbook-api.js +23 -9
- package/dst/specbook-cli.js +8 -5
- package/dst/specbook-cmd-export.js +2 -2
- package/dst/specbook-cmd-init.js +16 -2
- package/dst/specbook-coverage.js +1 -1
- package/dst/specbook-diagram.d.ts +1 -1
- package/dst/specbook-diagram.js +12 -10
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +21 -4
- package/dst/specbook-export-code.d.ts +14 -0
- package/dst/specbook-export-code.js +155 -0
- package/dst/specbook-export-common.d.ts +8 -2
- package/dst/specbook-export-common.js +126 -6
- package/dst/specbook-export-diagram.d.ts +15 -0
- package/dst/specbook-export-diagram.js +211 -0
- package/dst/specbook-export-html-fold.d.ts +20 -0
- package/dst/specbook-export-html-fold.js +458 -0
- package/dst/specbook-export-html-render.d.ts +51 -0
- package/dst/specbook-export-html-render.js +748 -0
- package/dst/specbook-export-html-search.js +6 -5
- package/dst/specbook-export-html-template.d.ts +21 -0
- package/dst/specbook-export-html-template.js +282 -0
- package/dst/specbook-export-html-widget.d.ts +12 -0
- package/dst/specbook-export-html-widget.js +605 -0
- package/dst/specbook-export-html.css +325 -50
- package/dst/specbook-export-html.js +62 -1931
- package/dst/specbook-export-image.d.ts +2 -0
- package/dst/specbook-export-image.js +161 -9
- package/dst/specbook-export-md.js +60 -12
- package/dst/specbook-format-schema.d.ts +1 -1
- package/dst/specbook-format-schema.js +1 -1
- package/dst/specbook-format-spec.d.ts +1 -0
- package/dst/specbook-format-spec.js +1 -0
- package/dst/specbook-format.md +173 -19
- package/dst/specbook-format.yaml +265 -24
- package/dst/specbook-ignore.js +2 -0
- package/dst/specbook-link.js +21 -5
- package/dst/specbook-mcp.js +9 -5
- package/dst/specbook-parse-common.d.ts +24 -0
- package/dst/specbook-parse-common.js +137 -10
- package/dst/specbook-parse-semantic.js +2 -0
- package/dst/specbook-parse-syntax.js +81 -20
- package/dst/specbook-parse.js +1 -1
- package/dst/specbook-project.d.ts +1 -0
- package/dst/specbook-project.js +10 -1
- package/dst/specbook-theme.d.ts +1 -0
- package/dst/specbook-theme.js +2 -0
- package/package.json +18 -12
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { Spec } from "./specbook-format-spec.js";
|
|
2
2
|
import { type Verbose } from "./specbook-verbose.js";
|
|
3
|
+
export declare const isDocument: (content: string) => boolean;
|
|
3
4
|
export declare const optimizeImages: (specification: Spec, print: boolean, verbose?: Verbose) => Promise<Map<string, string>>;
|
|
5
|
+
export declare const analyzeImages: (specification: Spec, verbose?: Verbose) => Promise<Map<string, boolean>>;
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { embeddedSource } from "./specbook-export-diagram.js";
|
|
9
|
+
import { embeddedListing } from "./specbook-export-code.js";
|
|
10
|
+
import { svgInvertible } from "./specbook-export-common.js";
|
|
11
|
+
import { darkMark, markDark, pdfType } from "./specbook-parse-common.js";
|
|
12
|
+
import { plainText } from "./specbook-link.js";
|
|
6
13
|
import { literal } from "./specbook-verbose.js";
|
|
7
14
|
/* the pixel width the raster images are capped to: the 60rem content
|
|
8
15
|
width of the document (at 16px per rem) at a 2x device pixel ratio */
|
|
@@ -19,6 +26,73 @@ const imageCaches = {
|
|
|
19
26
|
screen: new Map(),
|
|
20
27
|
print: new Map()
|
|
21
28
|
};
|
|
29
|
+
/* whether an embedded content is a page of a PDF document, which is
|
|
30
|
+
converted into an SVG image and then treated exactly like one */
|
|
31
|
+
export const isDocument = (content) => darkMark(content).content.startsWith(`data:${pdfType};`);
|
|
32
|
+
/* render the selected page of a PDF document (given as its embedding
|
|
33
|
+
data: URL carrying the 1-based "page" parameter, the first by default)
|
|
34
|
+
with PDF.js onto an SVG canvas of @napi-rs/canvas, i.e. as vector
|
|
35
|
+
graphics with the glyphs drawn as outlines, where PDF.js takes the
|
|
36
|
+
standard fonts, CMaps, ICC profiles, and WebAssembly decoders of
|
|
37
|
+
non-self-contained documents from its own package */
|
|
38
|
+
const renderPdf = async (content) => {
|
|
39
|
+
const page = Number(content.match(/^data:[^,]*?;page=(\d+)[;,]/)?.[1] ?? "1");
|
|
40
|
+
const data = content.slice(content.indexOf(",") + 1);
|
|
41
|
+
const { getDocument } = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
42
|
+
const { createCanvas, SvgExportFlag } = await import("@napi-rs/canvas");
|
|
43
|
+
const dir = path.dirname(createRequire(import.meta.url).resolve("pdfjs-dist/package.json"));
|
|
44
|
+
const task = getDocument({ data: new Uint8Array(Buffer.from(data, "base64")), verbosity: 0,
|
|
45
|
+
standardFontDataUrl: `${dir}/standard_fonts/`, cMapUrl: `${dir}/cmaps/`,
|
|
46
|
+
iccUrl: `${dir}/iccs/`, wasmUrl: `${dir}/wasm/` });
|
|
47
|
+
try {
|
|
48
|
+
const doc = await task.promise;
|
|
49
|
+
if (page < 1 || page > doc.numPages)
|
|
50
|
+
throw new Error(`document has no page ${page}`);
|
|
51
|
+
const sheet = await doc.getPage(page);
|
|
52
|
+
const viewport = sheet.getViewport({ scale: 1 });
|
|
53
|
+
const canvas = createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height), SvgExportFlag.NoPrettyXML);
|
|
54
|
+
await sheet.render({ viewport,
|
|
55
|
+
canvas: canvas,
|
|
56
|
+
canvasContext: canvas.getContext("2d") }).promise;
|
|
57
|
+
return canvas.getContent().toString("utf8");
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await task.destroy();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/* the in-memory cache of the SVG renderings of the embedded PDF pages
|
|
64
|
+
(null for a failed one), keyed by their plain embedded content and
|
|
65
|
+
swept to the pages of the latest conversion, like the optimized images */
|
|
66
|
+
let documentCache = new Map();
|
|
67
|
+
/* convert the embedded PDF pages of a specification into SVG images,
|
|
68
|
+
served from the cache where possible, where a rendering failure (e.g.
|
|
69
|
+
an absent page) is surfaced as a notice only and omits the page */
|
|
70
|
+
const convertDocuments = async (specification, verbose) => {
|
|
71
|
+
const cache = new Map();
|
|
72
|
+
const walk = async (object) => {
|
|
73
|
+
const contents = [...object.description?.embedding ?? [],
|
|
74
|
+
...object.properties.flatMap((property) => property.embedding ?? [])];
|
|
75
|
+
for (const plain of contents.filter(isDocument).map((content) => darkMark(content).content)) {
|
|
76
|
+
if (cache.has(plain))
|
|
77
|
+
continue;
|
|
78
|
+
let svg = documentCache.get(plain);
|
|
79
|
+
if (svg === undefined)
|
|
80
|
+
svg = await renderPdf(plain).catch((err) => {
|
|
81
|
+
verbose?.(`rendering PDF page of ${object.kind} "${literal(plainText(object.name))}" failed: ` +
|
|
82
|
+
(err instanceof Error ? err.message : String(err)));
|
|
83
|
+
return null;
|
|
84
|
+
});
|
|
85
|
+
cache.set(plain, svg);
|
|
86
|
+
}
|
|
87
|
+
for (const child of object.children)
|
|
88
|
+
await walk(child);
|
|
89
|
+
};
|
|
90
|
+
for (const artifact of specification.artifacts)
|
|
91
|
+
for (const object of artifact.objects)
|
|
92
|
+
await walk(object);
|
|
93
|
+
documentCache = cache;
|
|
94
|
+
return cache;
|
|
95
|
+
};
|
|
22
96
|
/* optimize a single SVG image with SVGO (the identifiers and classes
|
|
23
97
|
are free to be minified, as the image is rendered in isolation
|
|
24
98
|
through an <img> element), after stripping the "content" attribute
|
|
@@ -63,19 +137,23 @@ const optimizeRaster = async (content, print) => {
|
|
|
63
137
|
return `data:${type};base64,${data.toString("base64")}`;
|
|
64
138
|
};
|
|
65
139
|
/* collect the embedded image contents of an object and its descendants
|
|
66
|
-
(the empty entries of the unreadable files
|
|
140
|
+
(the empty entries of the unreadable files, the embedded diagram
|
|
141
|
+
sources, which are rendered instead, and the embedded source code
|
|
142
|
+
files, which are listed instead, left out) */
|
|
143
|
+
const isImage = (content) => content !== "" && embeddedSource(content) === undefined && embeddedListing(content) === undefined;
|
|
67
144
|
const collect = (object, contents) => {
|
|
68
145
|
for (const content of object.description?.embedding ?? [])
|
|
69
|
-
if (content
|
|
146
|
+
if (isImage(content))
|
|
70
147
|
contents.add(content);
|
|
71
148
|
for (const property of object.properties)
|
|
72
149
|
for (const content of property.embedding ?? [])
|
|
73
|
-
if (content
|
|
150
|
+
if (isImage(content))
|
|
74
151
|
contents.add(content);
|
|
75
152
|
for (const child of object.children)
|
|
76
153
|
collect(child, contents);
|
|
77
154
|
};
|
|
78
|
-
/* optimize the embedded images of a specification
|
|
155
|
+
/* optimize the embedded images of a specification (the PDF pages as
|
|
156
|
+
their converted SVGs, a failed one left out) for the HTML export
|
|
79
157
|
(the screen) or the PDF export (print), yielding the map from the
|
|
80
158
|
embedded image contents onto their optimized ones, served from the
|
|
81
159
|
cache where possible: an optimization which fails (an image the
|
|
@@ -83,7 +161,9 @@ const collect = (object, contents) => {
|
|
|
83
161
|
for) or which does not shrink the image keeps the original, so the
|
|
84
162
|
export never depends on it */
|
|
85
163
|
export const optimizeImages = async (specification, print, verbose) => {
|
|
86
|
-
/* collect the distinct embedded image contents of the specification
|
|
164
|
+
/* collect the distinct embedded image contents of the specification
|
|
165
|
+
and convert its PDF pages */
|
|
166
|
+
const documents = await convertDocuments(specification, verbose);
|
|
87
167
|
const medium = print ? "print" : "screen";
|
|
88
168
|
const contents = new Set();
|
|
89
169
|
for (const artifact of specification.artifacts)
|
|
@@ -97,20 +177,29 @@ export const optimizeImages = async (specification, print, verbose) => {
|
|
|
97
177
|
let before = 0;
|
|
98
178
|
let after = 0;
|
|
99
179
|
for (const content of contents) {
|
|
180
|
+
const plain = darkMark(content);
|
|
181
|
+
const document = isDocument(content) ? documents.get(plain.content) ?? null : undefined;
|
|
182
|
+
if (document === null)
|
|
183
|
+
continue;
|
|
100
184
|
let optimized = imageCaches[medium].get(content);
|
|
101
185
|
if (optimized !== undefined)
|
|
102
186
|
cached++;
|
|
103
187
|
else {
|
|
104
188
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
189
|
+
/* an image marked with its treatment on the dark theme is
|
|
190
|
+
optimized plain and marked again, a PDF page as its SVG */
|
|
191
|
+
optimized = document !== undefined ? await optimizeSvg(document) :
|
|
192
|
+
plain.content.startsWith("data:") ?
|
|
193
|
+
await optimizeRaster(plain.content, print) : await optimizeSvg(plain.content);
|
|
194
|
+
if (plain.dark !== undefined)
|
|
195
|
+
optimized = markDark(optimized, plain.dark);
|
|
107
196
|
}
|
|
108
197
|
catch (err) {
|
|
109
198
|
verbose?.("optimizing image failed (keeping the original): " +
|
|
110
199
|
(err instanceof Error ? err.message : String(err)));
|
|
111
|
-
optimized = content;
|
|
200
|
+
optimized = document !== undefined ? document : content;
|
|
112
201
|
}
|
|
113
|
-
if (optimized.length >= content.length)
|
|
202
|
+
if (document === undefined && optimized.length >= content.length)
|
|
114
203
|
optimized = content;
|
|
115
204
|
}
|
|
116
205
|
cache.set(content, optimized);
|
|
@@ -127,3 +216,66 @@ export const optimizeImages = async (specification, print, verbose) => {
|
|
|
127
216
|
`${literal(Math.round(before / 1024))} KB -> ${literal(Math.round(after / 1024))} KB`);
|
|
128
217
|
return cache;
|
|
129
218
|
};
|
|
219
|
+
/* the in-memory cache of the "auto" judgements of the raster images,
|
|
220
|
+
keyed by the embedded image content and swept to the images of the
|
|
221
|
+
latest analysis, exactly like the optimized images */
|
|
222
|
+
let rasterCache = new Map();
|
|
223
|
+
/* whether a raster image (given as a base64 data: URL) is most likely
|
|
224
|
+
dark ink on a transparent or light canvas and hence safe to invert on
|
|
225
|
+
the dark theme ("dark=auto"), judged on a downscaled copy of its
|
|
226
|
+
pixels: at least 60% of them are transparent or near-white (the
|
|
227
|
+
canvas), the others (the ink) exist, are dark on average, and consist
|
|
228
|
+
of a few colors only (the quantized colors holding at least 2% of the
|
|
229
|
+
ink cover at least 85% of it), unlike a photo or a colorful screenshot */
|
|
230
|
+
const rasterInvertible = async (content) => {
|
|
231
|
+
const m = content.match(/^data:image\/(?:png|jpeg|webp);base64,(.*)$/s);
|
|
232
|
+
if (m === null)
|
|
233
|
+
return false;
|
|
234
|
+
const { default: sharp } = await import("sharp");
|
|
235
|
+
const { data, info } = await sharp(Buffer.from(m[1], "base64"))
|
|
236
|
+
.resize({ width: 128, height: 128, fit: "inside", withoutEnlargement: true })
|
|
237
|
+
.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
238
|
+
let canvas = 0;
|
|
239
|
+
let ink = 0;
|
|
240
|
+
let luma = 0;
|
|
241
|
+
const bins = new Map();
|
|
242
|
+
for (let p = 0; p < data.length; p += info.channels) {
|
|
243
|
+
const [r, g, b, a] = [data[p], data[p + 1], data[p + 2], data[p + 3]];
|
|
244
|
+
if (a < 32 || Math.min(r, g, b) >= 235)
|
|
245
|
+
canvas++;
|
|
246
|
+
else {
|
|
247
|
+
ink++;
|
|
248
|
+
luma += (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
|
249
|
+
const bin = ((r >> 5) << 6) | ((g >> 5) << 3) | (b >> 5);
|
|
250
|
+
bins.set(bin, (bins.get(bin) ?? 0) + 1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const few = Array.from(bins.values()).filter((n) => n >= ink * 0.02).reduce((sum, n) => sum + n, 0);
|
|
254
|
+
return ink > 0 && canvas / (canvas + ink) >= 0.6 && luma / ink < 0.6 && few >= ink * 0.85;
|
|
255
|
+
};
|
|
256
|
+
/* judge the raster images and PDF pages of a specification without an
|
|
257
|
+
explicit treatment on the dark theme for the "auto" one, yielding the
|
|
258
|
+
map from their embedded contents onto whether they are invertible,
|
|
259
|
+
served from the cache where possible (an image Sharp cannot process
|
|
260
|
+
and a failed PDF page count as not invertible), where a PDF page is
|
|
261
|
+
judged on its converted SVG like an SVG image */
|
|
262
|
+
export const analyzeImages = async (specification, verbose) => {
|
|
263
|
+
const documents = await convertDocuments(specification, verbose);
|
|
264
|
+
const contents = new Set();
|
|
265
|
+
for (const artifact of specification.artifacts)
|
|
266
|
+
for (const object of artifact.objects)
|
|
267
|
+
collect(object, contents);
|
|
268
|
+
const cache = new Map();
|
|
269
|
+
for (const content of contents) {
|
|
270
|
+
if (!content.startsWith("data:") || darkMark(content).dark !== undefined)
|
|
271
|
+
continue;
|
|
272
|
+
if (isDocument(content)) {
|
|
273
|
+
const document = documents.get(content);
|
|
274
|
+
cache.set(content, typeof document === "string" && svgInvertible(document));
|
|
275
|
+
}
|
|
276
|
+
else
|
|
277
|
+
cache.set(content, rasterCache.get(content) ?? await rasterInvertible(content).catch(() => false));
|
|
278
|
+
}
|
|
279
|
+
rasterCache = new Map(Array.from(cache).filter(([content]) => !isDocument(content)));
|
|
280
|
+
return cache;
|
|
281
|
+
};
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { specDiagrams } from "./specbook-diagram.js";
|
|
8
|
-
import { becauseRegex, embeddingRegex, embeddingCount } from "./specbook-parse-common.js";
|
|
9
|
-
import { isTitleObject } from "./specbook-export-common.js";
|
|
10
|
-
import { optimizeImages } from "./specbook-export-image.js";
|
|
8
|
+
import { becauseRegex, embeddingRegex, embeddingCount, darkMark, markDark } from "./specbook-parse-common.js";
|
|
9
|
+
import { isTitleObject, imageDark } from "./specbook-export-common.js";
|
|
10
|
+
import { optimizeImages, analyzeImages, isDocument } from "./specbook-export-image.js";
|
|
11
|
+
import { renderEmbeddedDiagrams, embeddedSource, diagramKey } from "./specbook-export-diagram.js";
|
|
12
|
+
import { embeddedListing, listingFence } from "./specbook-export-code.js";
|
|
11
13
|
/* format a timestamp in the frontmatter format */
|
|
12
14
|
export const formatTimestamp = (date) => {
|
|
13
15
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -27,9 +29,11 @@ const renderKeyValuesMd = (properties, indent = "") => {
|
|
|
27
29
|
a ";" would split the segment, and a value carrying the rationale
|
|
28
30
|
marker would not be taken for a property at all on re-parsing */
|
|
29
31
|
const segmentableMd = (property) => !property.value.includes(";") && !becauseRegex.test(property.value);
|
|
30
|
-
/* render a description statement with its optional rationale
|
|
32
|
+
/* render a description statement with its optional rationale and
|
|
33
|
+
its optional elaboration blocks */
|
|
31
34
|
const renderDescriptionMd = (description) => description.description + (description.rationale !== undefined ?
|
|
32
|
-
`, BECAUSE ${description.rationale}` : "")
|
|
35
|
+
`, BECAUSE ${description.rationale}` : "") + (description.elaboration !== undefined ?
|
|
36
|
+
`\n\n${description.elaboration}` : "");
|
|
33
37
|
/* render the "(*)" primary marker plus the implicit "(xxx)"
|
|
34
38
|
and/or explicit "{{xxx}}" anchor suffix */
|
|
35
39
|
const nameSuffixMd = (object) => (object.primary === true ? " (*)" : "") +
|
|
@@ -119,10 +123,22 @@ export const renderMarkdown = async (specification, config, rebase, verbose) =>
|
|
|
119
123
|
export: an embeddable "" becomes the reference-style
|
|
120
124
|
"![alt][img-N]", whose "[img-N]: data:..." definition ends the
|
|
121
125
|
document (a "{theme}" one takes its light variant, as Markdown
|
|
122
|
-
knows no themes),
|
|
126
|
+
knows no themes), an embedded diagram source file becomes its
|
|
127
|
+
rendered SVG (the light variant, too, marked as inverted on the
|
|
128
|
+
dark theme where the diagram is), where every emitted SVG is marked
|
|
129
|
+
with its resolved treatment on the dark theme (an adapting diagram
|
|
130
|
+
flattened onto its light variant as the "auto" detection judges
|
|
131
|
+
it), so a re-parse keeps it, even where the "auto" detection would
|
|
132
|
+
judge a re-optimized SVG differently, an embedded source code file
|
|
133
|
+
of a description leaves its markup for a fenced code block collected
|
|
134
|
+
into "fences" (and one of a property value becomes a "data:"
|
|
135
|
+
definition, too), and only the remaining ones (including a failed
|
|
136
|
+
diagram) are re-based */
|
|
123
137
|
const optimized = await optimizeImages(specification, false, verbose);
|
|
138
|
+
const rasters = await analyzeImages(specification, verbose);
|
|
139
|
+
const sources = await renderEmbeddedDiagrams(specification, verbose);
|
|
124
140
|
const labels = new Map();
|
|
125
|
-
const embedMd = (text, embedding, from, to) => {
|
|
141
|
+
const embedMd = (text, embedding, from, to, fences) => {
|
|
126
142
|
let i = 0;
|
|
127
143
|
const embedded = text.replace(embeddingRegex, (match, alt, reference) => {
|
|
128
144
|
const count = embeddingCount(reference);
|
|
@@ -130,9 +146,24 @@ export const renderMarkdown = async (specification, config, rebase, verbose) =>
|
|
|
130
146
|
i += count;
|
|
131
147
|
if (content === undefined || content === "")
|
|
132
148
|
return match;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
const listing = embeddedListing(content);
|
|
150
|
+
if (listing !== undefined && fences !== undefined) {
|
|
151
|
+
fences.push(listingFence(listing));
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
else if (listing !== undefined) {
|
|
155
|
+
const label = labels.get(content) ?? `img-${labels.size + 1}`;
|
|
156
|
+
labels.set(content, label);
|
|
157
|
+
return `![${alt}][${label}]`;
|
|
158
|
+
}
|
|
159
|
+
const diagram = embeddedSource(content);
|
|
160
|
+
const variants = diagram !== undefined ? sources.get(diagramKey(diagram.language, diagram.source)) : undefined;
|
|
161
|
+
const light = diagram !== undefined ? variants?.light :
|
|
162
|
+
optimized.get(content) ?? (isDocument(content) ? undefined : content);
|
|
163
|
+
if (light === undefined)
|
|
164
|
+
return match;
|
|
165
|
+
const url = diagram !== undefined ? markDark(light, imageDark(light)) :
|
|
166
|
+
markDark(darkMark(light).content, imageDark(content, rasters));
|
|
136
167
|
const label = labels.get(url) ?? `img-${labels.size + 1}`;
|
|
137
168
|
labels.set(url, label);
|
|
138
169
|
return `![${alt}][${label}]`;
|
|
@@ -148,9 +179,26 @@ export const renderMarkdown = async (specification, config, rebase, verbose) =>
|
|
|
148
179
|
properties: object.properties.map((property) => ({ ...property, value: embedMd(property.value, property.embedding ?? [], from, to) })),
|
|
149
180
|
children: object.children.map((child) => embedObjectMd(child, from, to))
|
|
150
181
|
};
|
|
151
|
-
if (object.description !== undefined)
|
|
182
|
+
if (object.description !== undefined) {
|
|
183
|
+
/* the embeddings of the elaboration follow those of the statement,
|
|
184
|
+
and the fenced code blocks of the embedded source code files
|
|
185
|
+
end the elaboration */
|
|
186
|
+
const embedding = object.description.embedding ?? [];
|
|
187
|
+
const skip = Array.from(object.description.description.matchAll(embeddingRegex))
|
|
188
|
+
.reduce((sum, m) => sum + embeddingCount(m[2]), 0);
|
|
189
|
+
const fences = new Array();
|
|
152
190
|
clone.description = { ...object.description,
|
|
153
|
-
description: embedMd(object.description.description,
|
|
191
|
+
description: embedMd(object.description.description, embedding, from, to, fences) };
|
|
192
|
+
if (object.description.elaboration !== undefined)
|
|
193
|
+
clone.description.elaboration = embedMd(object.description.elaboration, embedding.slice(skip), from, to, fences);
|
|
194
|
+
if (fences.length > 0) {
|
|
195
|
+
/* the removed markups leave no empty paragraphs behind */
|
|
196
|
+
const collapse = (text) => text.replace(/\n[ \t]*(?:\n[ \t]*)+\n/g, "\n\n").trim();
|
|
197
|
+
clone.description.description = collapse(clone.description.description);
|
|
198
|
+
clone.description.elaboration = [collapse(clone.description.elaboration ?? ""), ...fences]
|
|
199
|
+
.filter((block) => block !== "").join("\n\n");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
154
202
|
const spec = diagrams.get(object);
|
|
155
203
|
if (spec !== undefined)
|
|
156
204
|
diagrams.set(clone, spec);
|
|
@@ -68,7 +68,7 @@ export type SchemaDiagram = {
|
|
|
68
68
|
config?: Partial<GradiaConfig>;
|
|
69
69
|
};
|
|
70
70
|
export type SchemaFormat = {
|
|
71
|
-
type?: "auto" | "complex" | "concise";
|
|
71
|
+
type?: "auto" | "complex" | "compact" | "concise";
|
|
72
72
|
maxTableColumns?: number;
|
|
73
73
|
maxCellHeight?: number;
|
|
74
74
|
withUnusedProps?: boolean;
|
|
@@ -80,7 +80,7 @@ const SchemaDiagram = v.strictObject({
|
|
|
80
80
|
config: v.optional(SchemaDiagramConfig)
|
|
81
81
|
});
|
|
82
82
|
const SchemaFormat = v.strictObject({
|
|
83
|
-
type: v.optional(v.picklist(["auto", "complex", "concise"])),
|
|
83
|
+
type: v.optional(v.picklist(["auto", "complex", "compact", "concise"])),
|
|
84
84
|
maxTableColumns: v.optional(v.pipe(v.number(), v.integer(), v.minValue(2))),
|
|
85
85
|
maxCellHeight: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
86
86
|
withUnusedProps: v.optional(v.boolean())
|