@rse/specbook 0.9.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 +199 -0
- package/dst/specbook-api.d.ts +47 -0
- package/dst/specbook-api.js +77 -0
- package/dst/specbook-cli.d.ts +2 -0
- package/dst/specbook-cli.js +156 -0
- package/dst/specbook-cmd-describe.d.ts +2 -0
- package/dst/specbook-cmd-describe.js +97 -0
- package/dst/specbook-cmd-edit.d.ts +9 -0
- package/dst/specbook-cmd-edit.js +69 -0
- package/dst/specbook-cmd-export.d.ts +9 -0
- package/dst/specbook-cmd-export.js +78 -0
- package/dst/specbook-cmd-import.d.ts +9 -0
- package/dst/specbook-cmd-import.js +73 -0
- package/dst/specbook-cmd-init.d.ts +7 -0
- package/dst/specbook-cmd-init.js +37 -0
- package/dst/specbook-cmd-lint.d.ts +15 -0
- package/dst/specbook-cmd-lint.js +46 -0
- package/dst/specbook-config.d.ts +6 -0
- package/dst/specbook-config.js +91 -0
- package/dst/specbook-diagnostic.d.ts +8 -0
- package/dst/specbook-diagnostic.js +23 -0
- package/dst/specbook-diagram.d.ts +9 -0
- package/dst/specbook-diagram.js +271 -0
- package/dst/specbook-export-ast.d.ts +4 -0
- package/dst/specbook-export-ast.js +36 -0
- package/dst/specbook-export-common.d.ts +33 -0
- package/dst/specbook-export-common.js +142 -0
- package/dst/specbook-export-html-search.js +272 -0
- package/dst/specbook-export-html.css +259 -0
- package/dst/specbook-export-html.d.ts +9 -0
- package/dst/specbook-export-html.js +452 -0
- package/dst/specbook-export-logo.svg +35 -0
- package/dst/specbook-export-md.d.ts +3 -0
- package/dst/specbook-export-md.js +74 -0
- package/dst/specbook-export-pdf.d.ts +7 -0
- package/dst/specbook-export-pdf.js +198 -0
- package/dst/specbook-link.d.ts +17 -0
- package/dst/specbook-link.js +113 -0
- package/dst/specbook-llm.d.ts +13 -0
- package/dst/specbook-llm.js +61 -0
- package/dst/specbook-mcp.d.ts +1 -0
- package/dst/specbook-mcp.js +163 -0
- package/dst/specbook-parse-common.d.ts +26 -0
- package/dst/specbook-parse-common.js +33 -0
- package/dst/specbook-parse-semantic.d.ts +5 -0
- package/dst/specbook-parse-semantic.js +254 -0
- package/dst/specbook-parse-syntax.d.ts +3 -0
- package/dst/specbook-parse-syntax.js +363 -0
- package/dst/specbook-parse-value.d.ts +19 -0
- package/dst/specbook-parse-value.js +123 -0
- package/dst/specbook-parse.d.ts +8 -0
- package/dst/specbook-parse.js +43 -0
- package/dst/specbook-struct-schema.d.ts +35 -0
- package/dst/specbook-struct-schema.js +47 -0
- package/dst/specbook-struct-spec.d.ts +31 -0
- package/dst/specbook-struct-spec.js +36 -0
- package/dst/specbook-theme.d.ts +16 -0
- package/dst/specbook-theme.js +38 -0
- package/package.json +77 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Specification } from "./specbook-struct-spec.js";
|
|
2
|
+
import type { SchemaSpecification } from "./specbook-struct-schema.js";
|
|
3
|
+
export type OutlineEntry = {
|
|
4
|
+
title: string;
|
|
5
|
+
anchor: string;
|
|
6
|
+
childs: OutlineEntry[];
|
|
7
|
+
};
|
|
8
|
+
export declare const htmlOutline: (specification: Specification, maxColumns: number) => OutlineEntry[];
|
|
9
|
+
export declare const renderHtml: (specification: Specification, maxColumns: number, config?: SchemaSpecification, tocPages?: Map<string, number>, css?: string) => Promise<string>;
|
|
@@ -0,0 +1,452 @@
|
|
|
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 { markedSmartypants } from "marked-smartypants";
|
|
8
|
+
import nunjucks from "nunjucks";
|
|
9
|
+
import nunjucksAddons from "@rse/nunjucks-addons";
|
|
10
|
+
import textframe from "textframe";
|
|
11
|
+
import { Gradia } from "@rse/gradia";
|
|
12
|
+
import { buildLinkIndex, resolveUnique, expandReferences, anchorPaths } from "./specbook-link.js";
|
|
13
|
+
import { compileValueExpr, splitItems } from "./specbook-parse-value.js";
|
|
14
|
+
import { embeddingRegex, embeddingMimeType } from "./specbook-parse-common.js";
|
|
15
|
+
import { escapeHtml, stylesheet, searchScript, fallbackLogo, isTitleObject, documentTitle, documentLang, documentThemeStyle } from "./specbook-export-common.js";
|
|
16
|
+
import { specDiagrams } from "./specbook-diagram.js";
|
|
17
|
+
/* ==== Templates ==== */
|
|
18
|
+
/* the built-in Nunjucks templates for the HTML export */
|
|
19
|
+
const templates = {
|
|
20
|
+
/* <Document/> */
|
|
21
|
+
"Document": textframe `
|
|
22
|
+
<!DOCTYPE html>
|
|
23
|
+
<html{% if Document.lang %} lang="{{ Document.lang }}"{% endif %}{% if Document.theme %} class="theme-{{ Document.theme }}"{% endif %}>
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="utf-8"/>
|
|
26
|
+
<title>{{ Document.title }}</title>
|
|
27
|
+
<style>
|
|
28
|
+
{{ Document.css }}
|
|
29
|
+
</style>
|
|
30
|
+
<script>
|
|
31
|
+
(function () {
|
|
32
|
+
let style = null
|
|
33
|
+
try { style = localStorage.getItem("specbook-theme") } catch (ex) {}
|
|
34
|
+
if (style === null) {
|
|
35
|
+
const m = document.documentElement.className.match(/theme-(light|dark)/)
|
|
36
|
+
style = m !== null ? m[1] : null
|
|
37
|
+
}
|
|
38
|
+
if (style === null)
|
|
39
|
+
style = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
40
|
+
document.documentElement.className = "theme-" + style
|
|
41
|
+
})()
|
|
42
|
+
function themeSwitch () {
|
|
43
|
+
const style = document.documentElement.className === "theme-dark" ? "light" : "dark"
|
|
44
|
+
document.documentElement.className = "theme-" + style
|
|
45
|
+
try { localStorage.setItem("specbook-theme", style) } catch (ex) {}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
</head>
|
|
49
|
+
<body>
|
|
50
|
+
<div class="theme-switch" onclick="themeSwitch()" title="switch color theme">◐</div>
|
|
51
|
+
{{ Document.titlepage }}
|
|
52
|
+
{% if not Document.titlepage %}<div class="meta">created: {{ Document.created }}, modified: {{ Document.modified }}</div>{% endif %}
|
|
53
|
+
{{ Document.toc }}
|
|
54
|
+
{{ Document.artifacts }}
|
|
55
|
+
{% if Document.search %}<script>{{ Document.search }}</script>{% endif %}
|
|
56
|
+
</body>
|
|
57
|
+
</html>
|
|
58
|
+
`,
|
|
59
|
+
/* <TitlePage/> */
|
|
60
|
+
"TitlePage": textframe `
|
|
61
|
+
<div class="titlepage">
|
|
62
|
+
{% if TitlePage.logo %}<div class="logo">{{ TitlePage.logo }}</div>{% endif %}
|
|
63
|
+
<div class="title">{{ TitlePage.title }}</div>
|
|
64
|
+
{% if TitlePage.subtitle %}<div class="subtitle">{{ TitlePage.subtitle }}</div>{% endif %}
|
|
65
|
+
{% if TitlePage.author %}<div class="author">{{ TitlePage.author }}</div>{% endif %}
|
|
66
|
+
<table class="meta">
|
|
67
|
+
{% if TitlePage.version %}<tr><td class="label">Version:</td><td>{{ TitlePage.version }}</td></tr>{% endif %}
|
|
68
|
+
<tr><td class="label">Created:</td><td>{{ TitlePage.created }}</td></tr>
|
|
69
|
+
<tr><td class="label">Modified:</td><td>{{ TitlePage.modified }}</td></tr>
|
|
70
|
+
</table>
|
|
71
|
+
{{ TitlePage.description }}
|
|
72
|
+
{{ TitlePage.properties }}
|
|
73
|
+
<div class="search">
|
|
74
|
+
<input type="text" id="search-input" placeholder="Search…" autocomplete="off" spellcheck="false"/>
|
|
75
|
+
<span class="search-clear" id="search-clear" title="clear search">×</span>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
`,
|
|
79
|
+
/* <Toc/> */
|
|
80
|
+
"Toc": textframe `
|
|
81
|
+
<nav class="toc">
|
|
82
|
+
<h1>Table of Contents</h1>
|
|
83
|
+
<table>
|
|
84
|
+
{% for entry in Toc.entries %}
|
|
85
|
+
<tr><td><a href="#{{ entry.id }}"><span class="object-kind">{{ entry.kind }}:</span> {{ entry.name }} <span class="link-symbol">⚭</span></a></td>{% if entry.page %}<td class="page"><a href="#{{ entry.id }}">{{ entry.page }}</a></td>{% endif %}</tr>
|
|
86
|
+
{% endfor %}
|
|
87
|
+
</table>
|
|
88
|
+
</nav>
|
|
89
|
+
`,
|
|
90
|
+
/* <Artifact/> */
|
|
91
|
+
"Artifact": textframe `
|
|
92
|
+
<article>
|
|
93
|
+
{{ Artifact.objects }}
|
|
94
|
+
</article>
|
|
95
|
+
`,
|
|
96
|
+
/* <Object/> */
|
|
97
|
+
"Object": textframe `
|
|
98
|
+
<section>
|
|
99
|
+
<h{{ Object.level }} id="{{ Object.id }}"><span class="object-kind">{{ Object.kind }}:</span> {{ Object.name }}{% if Object.primary %} <span class="primary-marker">⌘</span>{% endif %}{% if Object.paren %} <span class="anchor-paren">({{ Object.paren }})</span>{% endif %} <a href="#{{ Object.id }}"><span class="anchor-symbol">⚓︎</span></a></h{{ Object.level }}>
|
|
100
|
+
{{ Object.diagram }}
|
|
101
|
+
{{ Object.properties }}
|
|
102
|
+
{{ Object.description }}
|
|
103
|
+
{{ Object.childs }}
|
|
104
|
+
</section>
|
|
105
|
+
`,
|
|
106
|
+
/* <Properties/> */
|
|
107
|
+
"Properties": textframe `
|
|
108
|
+
<table class="props">
|
|
109
|
+
{% for property in Properties %}
|
|
110
|
+
<tr><td class="key property-name">{{ property.key }}</td><td>{{ property.value }}</td></tr>
|
|
111
|
+
{% endfor %}
|
|
112
|
+
</table>
|
|
113
|
+
`,
|
|
114
|
+
/* <Description/> */
|
|
115
|
+
"Description": textframe `
|
|
116
|
+
{%- if Description.block %}
|
|
117
|
+
<div class="description">{{ Description.description }}{% if Description.rationale %}
|
|
118
|
+
<p class="rationale">— <span class="keyword">BECAUSE</span> {{ Description.rationale }}</p>{% endif %}</div>
|
|
119
|
+
{%- else %}
|
|
120
|
+
<p class="description">{{ Description.description }}{% if Description.rationale %}
|
|
121
|
+
<span class="rationale">— <span class="keyword">BECAUSE</span> {{ Description.rationale }}</span>{% endif %}</p>
|
|
122
|
+
{%- endif %}
|
|
123
|
+
{%- for embedding in Description.embeddings %}
|
|
124
|
+
<div class="embedding">{{ embedding }}</div>
|
|
125
|
+
{%- endfor %}
|
|
126
|
+
`,
|
|
127
|
+
/* <Table/> */
|
|
128
|
+
"Table": textframe `
|
|
129
|
+
<table class="objects">
|
|
130
|
+
<thead>
|
|
131
|
+
<tr>
|
|
132
|
+
<th class="object-kind">{{ Table.head }}</th>
|
|
133
|
+
{%- for key in Table.keys %}<th class="property-name">{{ key }}</th>{% endfor %}
|
|
134
|
+
{%- if Table.desc %}<th class="description" style="width: {{ Table.width }}%">Description</th>{% endif %}
|
|
135
|
+
</tr>
|
|
136
|
+
</thead>
|
|
137
|
+
<tbody>
|
|
138
|
+
{% for row in Table.rows %}
|
|
139
|
+
<tr id="{{ row.id }}">
|
|
140
|
+
<td>{{ row.name }}{% if row.primary %} <span class="primary-marker">⌘</span>{% endif %}{% if row.paren %} <span class="anchor-paren">({{ row.paren }})</span>{% endif %} <a href="#{{ row.id }}"><span class="anchor-symbol">⚓︎</span></a></td>
|
|
141
|
+
{%- for value in row.values %}<td>{{ value }}</td>{% endfor %}
|
|
142
|
+
{%- if Table.desc %}<td>{{ row.description }}</td>{% endif %}
|
|
143
|
+
</tr>
|
|
144
|
+
{% endfor %}
|
|
145
|
+
</tbody>
|
|
146
|
+
</table>
|
|
147
|
+
`
|
|
148
|
+
};
|
|
149
|
+
/* ==== Rendering ==== */
|
|
150
|
+
/* the per-language double quote styles for the smart typography
|
|
151
|
+
(single quotes stay curly, as smartypants cannot distinguish a
|
|
152
|
+
closing single quote from an apostrophe) */
|
|
153
|
+
const quoteStyles = {
|
|
154
|
+
"en": ["“", "”"], /* “...” */
|
|
155
|
+
"nl": ["“", "”"], /* “...” */
|
|
156
|
+
"de": ["„", "“"], /* „...“ */
|
|
157
|
+
"cs": ["„", "“"], /* „...“ */
|
|
158
|
+
"sk": ["„", "“"], /* „...“ */
|
|
159
|
+
"pl": ["„", "”"], /* „...” */
|
|
160
|
+
"fr": ["« ", " »"], /* « ... » */
|
|
161
|
+
"it": ["«", "»"], /* «...» */
|
|
162
|
+
"es": ["«", "»"], /* «...» */
|
|
163
|
+
"pt": ["«", "»"], /* «...» */
|
|
164
|
+
"no": ["«", "»"], /* «...» */
|
|
165
|
+
"da": ["»", "«"], /* »...« */
|
|
166
|
+
"sv": ["”", "”"], /* ”...” */
|
|
167
|
+
"fi": ["”", "”"] /* ”...” */
|
|
168
|
+
};
|
|
169
|
+
/* the active double quote style (set per document during rendering) */
|
|
170
|
+
let quotes = quoteStyles.en;
|
|
171
|
+
/* improve the typography of all rendered Markdown text (curly quotes,
|
|
172
|
+
"--" as the em dash, ellipsis); as smartypants requires unescaped text,
|
|
173
|
+
the extension switches off the text escaping of marked, so the stray
|
|
174
|
+
ampersands left behind have to be re-escaped afterwards; the English
|
|
175
|
+
double quotes of smartypants are remapped to the document language;
|
|
176
|
+
as marked runs later-registered pass-through hooks first, the
|
|
177
|
+
post-processing hook has to be registered before smartypants */
|
|
178
|
+
marked.use({ hooks: { postprocess: (html) => html.replace(/“/g, quotes[0]).replace(/”/g, quotes[1])
|
|
179
|
+
.replace(/&(?![a-zA-Z][a-zA-Z0-9]*;|#\d+;|#x[0-9a-fA-F]+;)/g, "&") } });
|
|
180
|
+
marked.use(markedSmartypants({ config: 1 }));
|
|
181
|
+
/* the Nunjucks environment with the @rse/nunjucks-addons extensions */
|
|
182
|
+
const env = new nunjucks.Environment(null, { autoescape: true });
|
|
183
|
+
nunjucksAddons(env);
|
|
184
|
+
/* mark pre-rendered HTML as safe for template interpolation */
|
|
185
|
+
const safe = (html) => new nunjucks.runtime.SafeString(html);
|
|
186
|
+
const render = (name, context) => env.renderString(templates[name], context);
|
|
187
|
+
/* the active per-document reference expander, fully-qualified
|
|
188
|
+
anchor paths, enum/tags property value kinds, and pre-rendered
|
|
189
|
+
diagram SVGs (all set during HTML rendering) */
|
|
190
|
+
let linker = null;
|
|
191
|
+
let anchors = null;
|
|
192
|
+
let members = null;
|
|
193
|
+
let diagrams = null;
|
|
194
|
+
/* determine the fully-qualified anchor path of an object */
|
|
195
|
+
const anchorOf = (object) => anchors?.get(object) ?? object.id;
|
|
196
|
+
/* expand the inline Markdown of a text (code spans, emphasis, etc.),
|
|
197
|
+
with Wiki-style references expanded upfront */
|
|
198
|
+
const inline = (text) => safe(marked.parseInline(linker !== null ? linker(text) : text));
|
|
199
|
+
/* expand the full Markdown of a text, keeping its block-level
|
|
200
|
+
constructs (lists, quotes, code blocks, tables) intact */
|
|
201
|
+
const block = (text) => safe(marked.parse(linker !== null ? linker(text) : text));
|
|
202
|
+
/* check whether a text carries any block-level Markdown, i.e. is
|
|
203
|
+
anything else than the single paragraph a description usually is */
|
|
204
|
+
const isBlock = (text) => {
|
|
205
|
+
const tokens = marked.lexer(text).filter((token) => token.type !== "space");
|
|
206
|
+
return tokens.length > 0 && !(tokens.length === 1 && tokens[0].type === "paragraph");
|
|
207
|
+
};
|
|
208
|
+
/* render the embedded image files of a text into HTML (SVG inlined
|
|
209
|
+
as-is, PNG/JPEG placed onto <img> tags), taking the image alternate
|
|
210
|
+
texts from the corresponding "" markups */
|
|
211
|
+
const renderEmbeddings = (text, embedding) => {
|
|
212
|
+
const alts = [...text.matchAll(embeddingRegex)]
|
|
213
|
+
.filter((m) => embeddingMimeType(m[2].trim()) !== undefined)
|
|
214
|
+
.map((m) => m[1].trim());
|
|
215
|
+
return embedding.map((content, i) => content.startsWith("data:") ?
|
|
216
|
+
`<img src="${content}" alt="${escapeHtml(alts[i] ?? "")}"/>` :
|
|
217
|
+
content.replace(/^\s*<\?xml[^>]*\?>\s*(?:<!DOCTYPE[^>]*>\s*)?/i, ""));
|
|
218
|
+
};
|
|
219
|
+
/* render a description into HTML, expanding its inline Markdown and
|
|
220
|
+
moving the file embeddings to the end of the description */
|
|
221
|
+
const renderDescription = (description) => {
|
|
222
|
+
const text = description.description
|
|
223
|
+
.replace(embeddingRegex, (markup, _alt, reference) => embeddingMimeType(reference.trim()) !== undefined ? "" : markup)
|
|
224
|
+
.replace(/(?<=\S)[ \t]{2,}/g, " ").trim();
|
|
225
|
+
const embeddings = renderEmbeddings(description.description, description.embedding ?? []).map((content) => safe(content));
|
|
226
|
+
const blocked = isBlock(text);
|
|
227
|
+
return render("Description", { Description: {
|
|
228
|
+
block: blocked,
|
|
229
|
+
description: blocked ? block(text) : inline(text),
|
|
230
|
+
rationale: description.rationale !== undefined ?
|
|
231
|
+
inline(description.rationale) : undefined,
|
|
232
|
+
embeddings
|
|
233
|
+
} });
|
|
234
|
+
};
|
|
235
|
+
/* collect the "enum(...)"/"tags(...)" constrained properties of the
|
|
236
|
+
schema configuration, keyed by object kind and property name */
|
|
237
|
+
const collectMembers = (schemas, result) => {
|
|
238
|
+
for (const schema of schemas) {
|
|
239
|
+
for (const property of schema.props ?? []) {
|
|
240
|
+
if (property.value === undefined)
|
|
241
|
+
continue;
|
|
242
|
+
try {
|
|
243
|
+
const expr = compileValueExpr(property.value);
|
|
244
|
+
if (expr.kind === "enum" || expr.kind === "tags")
|
|
245
|
+
result.set(`${schema.kind}${property.name}`, expr.kind);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* an invalid expression is the concern of lint */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
collectMembers(schema.childs ?? [], result);
|
|
252
|
+
}
|
|
253
|
+
return result;
|
|
254
|
+
};
|
|
255
|
+
/* render a property value, badging the individual members of an
|
|
256
|
+
"enum(...)" (a single member) or "tags(...)" (a member set) value */
|
|
257
|
+
const inlineValue = (kind, key, value) => {
|
|
258
|
+
const member = members?.get(`${kind}${key.replace(/\s*\([^)]*\)\s*$/, "").trim()}`);
|
|
259
|
+
if (member === undefined)
|
|
260
|
+
return inline(value);
|
|
261
|
+
const items = member === "tags" ? splitItems(value) : [value.trim()];
|
|
262
|
+
return safe(items.map((item) => `<span class="value-member">${inline(item)}</span>`).join(" "));
|
|
263
|
+
};
|
|
264
|
+
/* expand the inline Markdown of the property values */
|
|
265
|
+
const inlineProperties = (kind, properties) => properties.map((property) => ({ key: property.key,
|
|
266
|
+
value: inlineValue(kind, property.key, property.value) }));
|
|
267
|
+
/* determine the column shape of a potential compact table */
|
|
268
|
+
const tableShape = (childs) => {
|
|
269
|
+
const keys = new Array();
|
|
270
|
+
for (const child of childs)
|
|
271
|
+
for (const property of child.properties)
|
|
272
|
+
if (!keys.includes(property.key))
|
|
273
|
+
keys.push(property.key);
|
|
274
|
+
return { keys, desc: childs.some((child) => child.description !== undefined) };
|
|
275
|
+
};
|
|
276
|
+
/* check whether the childs of an object form the deepest level and every
|
|
277
|
+
per-kind group is compact enough (at most maxColumns name/property/
|
|
278
|
+
description columns) for a tabular rendering */
|
|
279
|
+
const tabularChilds = (object, maxColumns) => {
|
|
280
|
+
if (object.childs.length === 0
|
|
281
|
+
|| !object.childs.every((child) => child.childs.length === 0))
|
|
282
|
+
return false;
|
|
283
|
+
return groupChilds(object.childs).every((group) => {
|
|
284
|
+
const { keys, desc } = tableShape(group);
|
|
285
|
+
return 1 + keys.length + (desc ? 1 : 0) <= maxColumns;
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
/* group the childs of an object by their kind, preserving order */
|
|
289
|
+
const groupChilds = (childs) => {
|
|
290
|
+
const groups = new Map();
|
|
291
|
+
for (const child of childs) {
|
|
292
|
+
const group = groups.get(child.kind);
|
|
293
|
+
if (group === undefined)
|
|
294
|
+
groups.set(child.kind, [child]);
|
|
295
|
+
else
|
|
296
|
+
group.push(child);
|
|
297
|
+
}
|
|
298
|
+
return [...groups.values()];
|
|
299
|
+
};
|
|
300
|
+
/* render a single-kind group of leaf childs into one compact table:
|
|
301
|
+
the name first, then the property columns, then the description */
|
|
302
|
+
const renderTable = (childs) => {
|
|
303
|
+
const { keys, desc } = tableShape(childs);
|
|
304
|
+
return render("Table", { Table: {
|
|
305
|
+
head: childs[0].kind !== "" ? childs[0].kind : "Name",
|
|
306
|
+
keys,
|
|
307
|
+
desc,
|
|
308
|
+
/* under the fixed table layout the description column claims
|
|
309
|
+
twice the share of a regular column, compressing the others */
|
|
310
|
+
width: Math.round(200 / (keys.length + 3)),
|
|
311
|
+
rows: childs.map((child) => ({
|
|
312
|
+
id: anchorOf(child),
|
|
313
|
+
paren: child.paren,
|
|
314
|
+
primary: child.primary,
|
|
315
|
+
name: inline(child.name),
|
|
316
|
+
values: keys.map((key) => {
|
|
317
|
+
const value = child.properties.find((property) => property.key === key)?.value;
|
|
318
|
+
return value !== undefined ? inlineValue(child.kind, key, value) : "";
|
|
319
|
+
}),
|
|
320
|
+
description: child.description !== undefined ?
|
|
321
|
+
safe(renderDescription(child.description)) : ""
|
|
322
|
+
}))
|
|
323
|
+
} });
|
|
324
|
+
};
|
|
325
|
+
/* recursively render an object into HTML */
|
|
326
|
+
const renderObject = (object, level, maxColumns) => render("Object", { Object: {
|
|
327
|
+
level: Math.min(level, 6),
|
|
328
|
+
kind: object.kind,
|
|
329
|
+
id: anchorOf(object),
|
|
330
|
+
paren: object.paren,
|
|
331
|
+
primary: object.primary,
|
|
332
|
+
name: inline(object.name),
|
|
333
|
+
diagram: diagrams?.has(object) === true ?
|
|
334
|
+
safe(`<div class="diagram">${diagrams.get(object) ?? ""}</div>`) : "",
|
|
335
|
+
properties: object.properties.length > 0 ?
|
|
336
|
+
safe(render("Properties", { Properties: inlineProperties(object.kind, object.properties) })) : "",
|
|
337
|
+
description: object.description !== undefined ?
|
|
338
|
+
safe(renderDescription(object.description)) : "",
|
|
339
|
+
childs: tabularChilds(object, maxColumns) ?
|
|
340
|
+
safe(groupChilds(object.childs).map(renderTable).join("")) :
|
|
341
|
+
safe(object.childs.map((child) => renderObject(child, level + 1, maxColumns)).join(""))
|
|
342
|
+
} });
|
|
343
|
+
/* render the title object into a title page */
|
|
344
|
+
const renderTitlePage = (object, created, modified) => {
|
|
345
|
+
const prop = (name) => object.properties.find((property) => property.key === name)?.value;
|
|
346
|
+
const rest = object.properties.filter((property) => !["LOGO", "TITLE", "SUBTITLE", "AUTHOR", "VERSION",
|
|
347
|
+
"LANG", "CHARSET", "PAPER-SIZE", "THEME-STYLE", "THEME-TONE"].includes(property.key));
|
|
348
|
+
/* the logo is rendered above the title, from the embedded image content
|
|
349
|
+
of the LOGO property or, for a non-embeddable reference, as its inline
|
|
350
|
+
Markdown; without a LOGO property the built-in SpecBook logo is used */
|
|
351
|
+
const logo = object.properties.find((property) => property.key === "LOGO");
|
|
352
|
+
const image = logo !== undefined ? renderEmbeddings(logo.value, logo.embedding ?? []) : [];
|
|
353
|
+
return render("TitlePage", { TitlePage: {
|
|
354
|
+
logo: logo === undefined ? safe(`<img src="${fallbackLogo()}" alt="SpecBook"/>`) :
|
|
355
|
+
(image.length > 0 ? safe(image.join("")) : inline(logo.value)),
|
|
356
|
+
title: inline(prop("TITLE") ?? object.name),
|
|
357
|
+
subtitle: prop("SUBTITLE") !== undefined ? inline(prop("SUBTITLE") ?? "") : "",
|
|
358
|
+
author: prop("AUTHOR") !== undefined ? inline(prop("AUTHOR") ?? "") : "",
|
|
359
|
+
version: prop("VERSION") !== undefined ? inline(prop("VERSION") ?? "") : "",
|
|
360
|
+
description: object.description !== undefined ?
|
|
361
|
+
safe(renderDescription(object.description)) : "",
|
|
362
|
+
properties: rest.length > 0 ?
|
|
363
|
+
safe(render("Properties", { Properties: inlineProperties(object.kind, rest) })) : "",
|
|
364
|
+
created, modified
|
|
365
|
+
} });
|
|
366
|
+
};
|
|
367
|
+
/* render an artifact into HTML */
|
|
368
|
+
const renderArtifact = (artifact, maxColumns) => render("Artifact", { Artifact: {
|
|
369
|
+
objects: safe(artifact.objects.map((object) => renderObject(object, 1, maxColumns)).join(""))
|
|
370
|
+
} });
|
|
371
|
+
/* derive the hierarchy of the rendered object headings, skipping the
|
|
372
|
+
title page object and the leaf childs collapsing into compact tables */
|
|
373
|
+
export const htmlOutline = (specification, maxColumns) => {
|
|
374
|
+
const paths = anchorPaths(buildLinkIndex(specification));
|
|
375
|
+
const entry = (object) => ({
|
|
376
|
+
title: (object.kind !== "" ? `${object.kind}: ` : "") + object.name,
|
|
377
|
+
anchor: paths.get(object) ?? object.id,
|
|
378
|
+
childs: tabularChilds(object, maxColumns) ? [] : object.childs.map(entry)
|
|
379
|
+
});
|
|
380
|
+
return specification.artifacts
|
|
381
|
+
.filter((artifact) => !artifact.objects.some(isTitleObject))
|
|
382
|
+
.flatMap((artifact) => artifact.objects)
|
|
383
|
+
.map(entry);
|
|
384
|
+
};
|
|
385
|
+
/* render the entire specification into a self-contained HTML document,
|
|
386
|
+
with the build-time pre-assembled stylesheet embedded inline, the
|
|
387
|
+
artifact timestamps aggregated into min(Created)/max(Modified), and
|
|
388
|
+
optional per-anchor page numbers attached to the ToC entries */
|
|
389
|
+
export const renderHtml = async (specification, maxColumns, config, tocPages, css) => {
|
|
390
|
+
/* the document language selects the smart typography quote style */
|
|
391
|
+
const lang = documentLang(specification);
|
|
392
|
+
quotes = quoteStyles[lang?.toLowerCase().split(/[-_]/)[0] ?? "en"] ?? quoteStyles.en;
|
|
393
|
+
/* pre-render the configured diagrams as embeddable SVGs (a runtime
|
|
394
|
+
rendering failure omits the diagram, as the statically detectable
|
|
395
|
+
invalid situations are already reported as lint diagnostics),
|
|
396
|
+
displayed at a reduced coordinate scale, as the Gradia geometry
|
|
397
|
+
(node boxes, font sizes) is dimensioned for a stand-alone
|
|
398
|
+
canvas and would dwarf the document text at 1:1 */
|
|
399
|
+
const scale = 0.75;
|
|
400
|
+
diagrams = null;
|
|
401
|
+
if (config !== undefined) {
|
|
402
|
+
diagrams = new Map();
|
|
403
|
+
for (const [object, result] of specDiagrams(specification, config)) {
|
|
404
|
+
if (result.spec === undefined)
|
|
405
|
+
continue;
|
|
406
|
+
try {
|
|
407
|
+
const svg = await Gradia.render(result.spec, { format: "svg:embedded" });
|
|
408
|
+
diagrams.set(object, svg.replace(/(<svg[^>]*) width="([0-9.]+)" height="([0-9.]+)"/, (_, head, w, h) => `${head} width="${Number(w) * scale}" height="${Number(h) * scale}"`));
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
/* intentionally omitted */
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/* expand "[[xxx]]" references into hyperlinks (an unresolvable or
|
|
416
|
+
ambiguous reference stays literal, marked as broken), targeting
|
|
417
|
+
the fully-qualified anchor paths of the objects */
|
|
418
|
+
const index = buildLinkIndex(specification);
|
|
419
|
+
anchors = anchorPaths(index);
|
|
420
|
+
members = config !== undefined ? collectMembers(config, new Map()) : null;
|
|
421
|
+
linker = (text) => expandReferences(text, (reference) => {
|
|
422
|
+
const target = resolveUnique(index, reference).target;
|
|
423
|
+
if (target === undefined)
|
|
424
|
+
return `<span class="link-broken">[[${escapeHtml(reference)}]]</span>`;
|
|
425
|
+
return `<a href="#${escapeHtml(anchorOf(target))}">` +
|
|
426
|
+
`<span class="object-kind">${escapeHtml(target.kind)}:</span> <strong>${target.name}</strong>` +
|
|
427
|
+
" <span class=\"link-symbol\">⚭</span></a>";
|
|
428
|
+
});
|
|
429
|
+
const created = new Date(Math.min(...specification.artifacts.map((artifact) => artifact.created.getTime())));
|
|
430
|
+
const modified = new Date(Math.max(...specification.artifacts.map((artifact) => artifact.modified.getTime())));
|
|
431
|
+
/* a "META: Title" object becomes the title page and leaves the regular flow */
|
|
432
|
+
const title = specification.artifacts
|
|
433
|
+
.flatMap((artifact) => artifact.objects).find(isTitleObject);
|
|
434
|
+
const artifacts = specification.artifacts
|
|
435
|
+
.filter((artifact) => !artifact.objects.some(isTitleObject));
|
|
436
|
+
const entries = artifacts.flatMap((artifact) => artifact.objects)
|
|
437
|
+
.map((object) => ({ id: anchorOf(object), kind: object.kind, name: inline(object.name),
|
|
438
|
+
page: tocPages?.get(anchorOf(object)) }));
|
|
439
|
+
return render("Document", { Document: {
|
|
440
|
+
title: documentTitle(specification).title,
|
|
441
|
+
lang,
|
|
442
|
+
theme: documentThemeStyle(specification)?.toLowerCase(),
|
|
443
|
+
css: safe(css ?? stylesheet()),
|
|
444
|
+
created: created.toISOString(),
|
|
445
|
+
modified: modified.toISOString(),
|
|
446
|
+
titlepage: title !== undefined ?
|
|
447
|
+
safe(renderTitlePage(title, created.toISOString().slice(0, 10), modified.toISOString().slice(0, 10))) : "",
|
|
448
|
+
search: title !== undefined ? safe(searchScript()) : "",
|
|
449
|
+
toc: entries.length > 0 ? safe(render("Toc", { Toc: { entries } })) : "",
|
|
450
|
+
artifacts: safe(artifacts.map((artifact) => renderArtifact(artifact, maxColumns)).join(""))
|
|
451
|
+
} });
|
|
452
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 260.81 110.54">
|
|
3
|
+
<!-- Generator: Adobe Illustrator 30.7.0, SVG Export Plug-In . SVG Version: 2.1.4 Build 114) -->
|
|
4
|
+
<defs>
|
|
5
|
+
<style>
|
|
6
|
+
.st0 {
|
|
7
|
+
fill: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.st1 {
|
|
11
|
+
fill: #231f20;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.st2 {
|
|
15
|
+
fill: #666766;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
18
|
+
</defs>
|
|
19
|
+
<g>
|
|
20
|
+
<rect class="st0" x="128.02" y="5.89" width="297.69" height="245.38"/>
|
|
21
|
+
<path class="st1" d="M132.56,41.28c2.62,1.34,6.66,2.69,10.82,2.69,4.48,0,6.85-1.86,6.85-4.67s-2.05-4.22-7.23-6.08c-7.17-2.5-11.84-6.46-11.84-12.73,0-7.36,6.14-12.99,16.32-12.99,4.86,0,8.45,1.02,11.01,2.18l-2.18,7.87c-1.73-.83-4.8-2.05-9.02-2.05s-6.27,1.92-6.27,4.16c0,2.75,2.43,3.97,8,6.08,7.62,2.82,11.2,6.78,11.2,12.86,0,7.23-5.57,13.38-17.41,13.38-4.93,0-9.79-1.28-12.22-2.62l1.98-8.06Z"/>
|
|
22
|
+
<path class="st1" d="M163.28,30.53c0-4.1-.13-7.62-.26-10.5h8.45l.45,4.35h.13c2.3-3.33,5.89-5.06,10.43-5.06,6.85,0,12.99,5.95,12.99,15.93,0,11.39-7.23,16.77-14.21,16.77-3.78,0-6.72-1.54-8.13-3.58h-.13v15.55h-9.73V30.53ZM173.01,37.89c0,.77.06,1.47.19,2.05.64,2.62,2.88,4.61,5.7,4.61,4.22,0,6.72-3.52,6.72-8.83,0-4.99-2.24-8.77-6.59-8.77-2.75,0-5.18,2.05-5.82,4.93-.13.51-.19,1.15-.19,1.73v4.29Z"/>
|
|
23
|
+
<path class="st1" d="M206.03,38.78c.32,4.03,4.29,5.95,8.83,5.95,3.33,0,6.02-.45,8.64-1.28l1.28,6.59c-3.2,1.28-7.1,1.92-11.33,1.92-10.62,0-16.7-6.14-16.7-15.94,0-7.94,4.93-16.7,15.81-16.7,10.11,0,13.95,7.87,13.95,15.61,0,1.66-.19,3.14-.32,3.84h-20.16ZM217.48,32.13c0-2.37-1.02-6.34-5.5-6.34-4.1,0-5.76,3.71-6.02,6.34h11.52Z"/>
|
|
24
|
+
<path class="st1" d="M252.62,50.56c-1.73.77-4.99,1.41-8.7,1.41-10.11,0-16.57-6.14-16.57-16,0-9.15,6.27-16.64,17.92-16.64,2.56,0,5.38.45,7.42,1.22l-1.54,7.23c-1.15-.51-2.88-.96-5.44-.96-5.12,0-8.45,3.65-8.38,8.77,0,5.76,3.84,8.77,8.58,8.77,2.3,0,4.1-.38,5.57-.96l1.15,7.17Z"/>
|
|
25
|
+
</g>
|
|
26
|
+
<path d="M25.37,18.54h0c-1.03,0-2,.4-2.73,1.13-.74.74-1.15,1.72-1.15,2.77v58.42c0,2.14,1.75,3.89,3.9,3.9,9.07.02,24.26,1.91,34.75,12.88v-61.14c0-.73-.19-1.41-.54-1.97-8.6-13.85-25.14-15.96-34.23-15.98Z"/>
|
|
27
|
+
<path d="M104.82,80.86V22.43c0-1.05-.41-2.03-1.15-2.77-.73-.73-1.7-1.13-2.73-1.13h0c-9.09.02-25.62,2.13-34.23,15.98-.35.56-.54,1.25-.54,1.97v61.14c10.48-10.97,25.68-12.86,34.75-12.88,2.15,0,3.9-1.75,3.9-3.89Z"/>
|
|
28
|
+
<path class="st2" d="M113.7,32.01h-2.83v48.85c0,5.47-4.46,9.93-9.94,9.94-7.69.02-20.38,1.52-29.36,10.02,15.54-3.8,31.91-1.33,41.25.8,1.17.27,2.37,0,3.3-.75.93-.74,1.46-1.85,1.46-3.04v-61.92c0-2.15-1.75-3.9-3.9-3.9Z"/>
|
|
29
|
+
<path class="st2" d="M15.43,80.86v-48.85h-2.83c-2.15,0-3.9,1.75-3.9,3.9v61.92c0,1.19.53,2.3,1.46,3.04.93.74,2.14,1.02,3.3.75,9.33-2.13,25.71-4.6,41.25-.8-8.98-8.5-21.67-10.01-29.36-10.02-5.48-.01-9.94-4.47-9.94-9.94Z"/>
|
|
30
|
+
<g>
|
|
31
|
+
<path class="st2" d="M131.67,55.5c2.63-.55,6.77-.97,10.99-.97,6.01,0,9.88,1.04,12.78,3.39,2.42,1.8,3.87,4.56,3.87,8.22,0,4.49-2.97,8.43-7.88,10.23v.14c4.42,1.11,9.6,4.77,9.6,11.68,0,4.01-1.59,7.05-3.94,9.33-3.25,2.97-8.5,4.35-16.1,4.35-4.15,0-7.32-.28-9.33-.55v-45.81ZM137.68,74.57h5.46c6.36,0,10.09-3.32,10.09-7.81,0-5.46-4.15-7.6-10.23-7.6-2.76,0-4.35.21-5.32.41v14.99ZM137.68,96.89c1.17.21,2.9.28,5.04.28,6.22,0,11.95-2.28,11.95-9.05,0-6.36-5.46-8.98-12.02-8.98h-4.97v17.76Z"/>
|
|
32
|
+
<path class="st2" d="M206.14,66.9c-9.26,0-16.58,6.56-16.58,17.75,0,.12.01.24.01.36h-.04c-.27,7.16-4.51,12.28-10.13,12.28-5.94,0-10.16-5.53-10.16-12.85,0-6.36,3.11-12.99,10.29-12.99,4.29,0,7.02,2.57,8.53,5.85.79-2.15,1.94-3.99,3.37-5.51-2.83-3.1-6.89-4.9-11.76-4.9-9.26,0-16.58,6.56-16.58,17.75,0,10.57,6.98,17.2,16.03,17.2,7.88,0,16.19-5.14,16.61-16.84h0c0-.19-.02-.38-.02-.57,0-6.36,3.11-12.99,10.29-12.99s10.02,7.19,10.02,12.85c0,7.53-4.35,12.99-10.15,12.99-4.05,0-7.29-2.57-8.94-6.51-.75,2.31-1.89,4.24-3.29,5.8,2.9,3.37,7.13,5.27,11.96,5.27,8.08,0,16.65-5.39,16.65-17.75,0-10.22-6.49-17.2-16.1-17.2Z"/>
|
|
33
|
+
<path class="st2" d="M230.65,83.76h.14c.83-1.17,2-2.63,2.97-3.8l9.81-11.54h7.32l-12.92,13.75,14.72,19.69h-7.39l-11.54-16.03-3.11,3.45v12.57h-6.01v-49.06h6.01v30.95Z"/>
|
|
34
|
+
</g>
|
|
35
|
+
</svg>
|
|
@@ -0,0 +1,74 @@
|
|
|
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 { specDiagrams } from "./specbook-diagram.js";
|
|
7
|
+
/* format a timestamp in the frontmatter format */
|
|
8
|
+
const formatTimestamp = (date) => {
|
|
9
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
10
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
|
11
|
+
`${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
12
|
+
};
|
|
13
|
+
/* render the properties as a value-aligned key/value list */
|
|
14
|
+
const renderKeyValuesMd = (properties) => {
|
|
15
|
+
const width = Math.max(...properties.map((property) => property.key.length)) + 1;
|
|
16
|
+
return properties.map((property) => `- ${`${property.key}:`.padEnd(width)} ${property.value}`).join("\n");
|
|
17
|
+
};
|
|
18
|
+
/* render a description statement with its optional rationale */
|
|
19
|
+
const renderDescriptionMd = (description) => description.description + (description.rationale !== undefined ?
|
|
20
|
+
`, BECAUSE ${description.rationale}` : "");
|
|
21
|
+
/* render the "(*)" primary marker plus the implicit "(xxx)"
|
|
22
|
+
and/or explicit "{{xxx}}" anchor suffix */
|
|
23
|
+
const nameSuffixMd = (object) => (object.primary === true ? " (*)" : "") +
|
|
24
|
+
(object.paren !== undefined ? ` (${object.paren})` : "") +
|
|
25
|
+
(object.anchor !== undefined ? ` {{${object.anchor}}}` : "");
|
|
26
|
+
/* render an object in the Concise Format (level 4 and deeper),
|
|
27
|
+
with its childs as nested list items */
|
|
28
|
+
const renderConciseMd = (object, level) => {
|
|
29
|
+
const indent = " ".repeat((level - 4) * 4);
|
|
30
|
+
const segments = [`${object.kind}: ${object.name}${nameSuffixMd(object)}`];
|
|
31
|
+
segments.push(...object.properties.map((property) => `${property.key}: ${property.value}`));
|
|
32
|
+
if (object.description !== undefined)
|
|
33
|
+
segments.push(renderDescriptionMd(object.description));
|
|
34
|
+
const item = `${indent}- ${segments.join("; ").replace(/\.?$/, ".")}`;
|
|
35
|
+
return [item, ...object.childs.map((child) => renderConciseMd(child, level + 1))].join("\n");
|
|
36
|
+
};
|
|
37
|
+
/* render an object in the Complex Format (levels 1-3),
|
|
38
|
+
with childs from level 4 upwards in the Concise Format
|
|
39
|
+
and the optionally derived Gradia diagram spec embedded
|
|
40
|
+
as a "gradia" fenced code block below the heading */
|
|
41
|
+
const renderObjectMd = (object, level, diagrams) => {
|
|
42
|
+
const heading = level === 1 ?
|
|
43
|
+
`# ${object.kind}: ${object.name}${nameSuffixMd(object)}` :
|
|
44
|
+
`${"#".repeat(level)}${" ".repeat(4 - level)}${object.kind}: ${object.name}${nameSuffixMd(object)}`;
|
|
45
|
+
const parts = [heading];
|
|
46
|
+
const spec = diagrams?.get(object);
|
|
47
|
+
if (spec !== undefined)
|
|
48
|
+
parts.push(`\`\`\`gradia\n${spec}\`\`\``);
|
|
49
|
+
if (object.properties.length > 0)
|
|
50
|
+
parts.push(renderKeyValuesMd(object.properties));
|
|
51
|
+
if (object.description !== undefined)
|
|
52
|
+
parts.push(renderDescriptionMd(object.description));
|
|
53
|
+
if (level >= 3)
|
|
54
|
+
parts.push(object.childs.map((child) => renderConciseMd(child, level + 1)).join("\n"));
|
|
55
|
+
else
|
|
56
|
+
parts.push(...object.childs.map((child) => renderObjectMd(child, level + 1, diagrams)));
|
|
57
|
+
return parts.filter((part) => part !== "").join("\n\n");
|
|
58
|
+
};
|
|
59
|
+
/* render the entire specification into normalized Markdown */
|
|
60
|
+
export const renderMarkdown = async (specification, config) => {
|
|
61
|
+
/* derive the Gradia specs of the diagram-configured objects
|
|
62
|
+
(an invalid diagram situation omits the diagram, as it is
|
|
63
|
+
already reported as a lint diagnostic) */
|
|
64
|
+
const diagrams = new Map();
|
|
65
|
+
if (config !== undefined)
|
|
66
|
+
for (const [object, result] of specDiagrams(specification, config))
|
|
67
|
+
if (result.spec !== undefined)
|
|
68
|
+
diagrams.set(object, result.spec);
|
|
69
|
+
return specification.artifacts.map((artifact) => artifact.objects.map((object) => "---\n" +
|
|
70
|
+
`Created: ${formatTimestamp(artifact.created)}\n` +
|
|
71
|
+
`Modified: ${formatTimestamp(artifact.modified)}\n` +
|
|
72
|
+
"---\n\n" +
|
|
73
|
+
renderObjectMd(object, 1, diagrams)).join("\n\n")).join("\n\n") + "\n";
|
|
74
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { OutlineEntry } from "./specbook-export-html.js";
|
|
2
|
+
import type { ThemeMapping } from "./specbook-theme.js";
|
|
3
|
+
export declare const htmlToPdf: (renderHtmlPass: (tocPages?: Map<string, number>) => Promise<string>, heading: {
|
|
4
|
+
title: string;
|
|
5
|
+
subtitle?: string;
|
|
6
|
+
logo: string;
|
|
7
|
+
}, outline: OutlineEntry[], verbose: (msg: string) => void, css: string, theme: ThemeMapping, paper?: string) => Promise<Buffer>;
|