@rse/specbook 1.3.1 → 1.4.0

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 (45) hide show
  1. package/README.md +5 -2
  2. package/dst/specbook-api.js +7 -5
  3. package/dst/specbook-cli.js +0 -0
  4. package/dst/specbook-cmd-export.js +2 -2
  5. package/dst/specbook-coverage.js +1 -1
  6. package/dst/specbook-diagram.d.ts +1 -1
  7. package/dst/specbook-diagram.js +9 -3
  8. package/dst/specbook-export-ast.d.ts +1 -1
  9. package/dst/specbook-export-ast.js +21 -4
  10. package/dst/specbook-export-code.d.ts +14 -0
  11. package/dst/specbook-export-code.js +155 -0
  12. package/dst/specbook-export-common.d.ts +4 -2
  13. package/dst/specbook-export-common.js +110 -6
  14. package/dst/specbook-export-diagram.d.ts +15 -0
  15. package/dst/specbook-export-diagram.js +211 -0
  16. package/dst/specbook-export-html-fold.d.ts +20 -0
  17. package/dst/specbook-export-html-fold.js +458 -0
  18. package/dst/specbook-export-html-render.d.ts +51 -0
  19. package/dst/specbook-export-html-render.js +749 -0
  20. package/dst/specbook-export-html-search.js +6 -5
  21. package/dst/specbook-export-html-template.d.ts +21 -0
  22. package/dst/specbook-export-html-template.js +282 -0
  23. package/dst/specbook-export-html-widget.d.ts +12 -0
  24. package/dst/specbook-export-html-widget.js +605 -0
  25. package/dst/specbook-export-html.css +325 -50
  26. package/dst/specbook-export-html.js +62 -1931
  27. package/dst/specbook-export-image.d.ts +2 -0
  28. package/dst/specbook-export-image.js +161 -9
  29. package/dst/specbook-export-md.js +60 -12
  30. package/dst/specbook-format-schema.d.ts +1 -1
  31. package/dst/specbook-format-schema.js +1 -1
  32. package/dst/specbook-format-spec.d.ts +1 -0
  33. package/dst/specbook-format-spec.js +1 -0
  34. package/dst/specbook-format.md +169 -17
  35. package/dst/specbook-format.yaml +224 -23
  36. package/dst/specbook-ignore.js +2 -0
  37. package/dst/specbook-link.js +21 -5
  38. package/dst/specbook-parse-common.d.ts +24 -0
  39. package/dst/specbook-parse-common.js +137 -10
  40. package/dst/specbook-parse-semantic.js +2 -0
  41. package/dst/specbook-parse-syntax.js +81 -20
  42. package/dst/specbook-parse.js +1 -1
  43. package/dst/specbook-theme.d.ts +1 -0
  44. package/dst/specbook-theme.js +2 -0
  45. package/package.json +17 -11
@@ -0,0 +1,458 @@
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 textframe from "textframe";
7
+ import { safe } from "./specbook-export-html-template.js";
8
+ /* ==== Folding & Omitting ==== */
9
+ /* the default percentage by which the text of a table cell may exceed
10
+ the height of every other cell of its row before it folds (or is
11
+ omitted), overridden per object kind by "maxCellHeight", and the
12
+ minimum percentage of its own height a cell has to gain by that,
13
+ shared by the client-side folding and the server-side omitting */
14
+ const cellHeightDefault = 40;
15
+ const cellGainMin = 25;
16
+ /* the client-side script of the folding: it wraps every diagram of the
17
+ content into a fold container carrying a chevron mark at its top
18
+ left corner, folds the text of every table cell towering over the
19
+ other cells of its row behind a chevron mark of its own, and lets
20
+ the controls of the fold tab fold and unfold all diagrams of a type
21
+ ("graph", "hub", "grid", plus the embedded Mermaid/D2 ones as "code",
22
+ the embedded images and PDF pages as "image", and the code listings
23
+ as "listing"), all diagrams from an object tree nesting level on
24
+ (1, 2, 3), and all cell texts at once, with their state
25
+ persisted across page loads and their icons marked while they are
26
+ active (the diagram ones, OR-combined) or any cell text is folded
27
+ (the tab icon while anything at all is). The script runs at
28
+ the end of the body, as the content it wraps has to exist already,
29
+ and a live preview body swap replaces the containers and their
30
+ listeners along with the body. An export omitting aspects lacks
31
+ their controls ("standins" lets detached elements take their place)
32
+ and, with the long texts omitted, folds no cell text ("textless") */
33
+ export const foldScript = (standins, textless) => textframe `
34
+ (function () {
35
+ const tab = document.querySelector("div.fold-switch")
36
+ if (tab === null)
37
+ return
38
+ const controls = {
39
+ graph: tab.querySelector("div.fold-graphs"),
40
+ hub: tab.querySelector("div.fold-hubs"),
41
+ grid: tab.querySelector("div.fold-grids"),
42
+ code: tab.querySelector("div.fold-codes"),
43
+ image: tab.querySelector("div.fold-images"),
44
+ listing: tab.querySelector("div.fold-listings"),
45
+ level1: tab.querySelector("div.fold-level1"),
46
+ level2: tab.querySelector("div.fold-level2"),
47
+ level3: tab.querySelector("div.fold-level3"),
48
+ text: tab.querySelector("div.fold-texts")
49
+ }${standins ? `
50
+ for (const kind of Object.keys(controls))
51
+ controls[kind] ??= document.createElement("div")` : ""}
52
+
53
+ /* let the fold icon slide the controls out of the tab (and
54
+ back in again), remembering the choice across page loads */
55
+ try { if (localStorage.getItem("specbook-fold") === "open") tab.classList.add("open") }
56
+ catch { /* an inaccessible storage just means no stored state */ }
57
+ tab.querySelector("div.fold-toggle").addEventListener("click", () => {
58
+ tab.classList.toggle("open")
59
+ try { localStorage.setItem("specbook-fold", tab.classList.contains("open") ? "open" : "closed") }
60
+ catch { /* an inaccessible storage just loses the state */ }
61
+ })
62
+
63
+ /* the default percentage by which the text of a table cell may
64
+ exceed the height of every other cell of its row before it
65
+ folds, overridden per object kind by the "data-fold-height"
66
+ attribute the schema configuration puts onto its table */
67
+ const cellHeight = ${cellHeightDefault}
68
+
69
+ /* the chevron mark of a foldable element, rotated by the
70
+ stylesheet while the element is folded */
71
+ const svgNS = "http://www.w3.org/2000/svg"
72
+ const mark = (name, title) => {
73
+ const span = document.createElement("span")
74
+ span.className = name
75
+ span.title = title
76
+ const svg = document.createElementNS(svgNS, "svg")
77
+ svg.setAttribute("viewBox", "0 0 24 24")
78
+ svg.setAttribute("fill", "none")
79
+ svg.setAttribute("stroke", "currentColor")
80
+ svg.setAttribute("stroke-width", "2.5")
81
+ svg.setAttribute("stroke-linecap", "round")
82
+ svg.setAttribute("stroke-linejoin", "round")
83
+ const path = document.createElementNS(svgNS, "path")
84
+ path.setAttribute("d", "M5 9l7 7 7-7")
85
+ svg.appendChild(path)
86
+ span.appendChild(svg)
87
+ return span
88
+ }
89
+
90
+ /* wrap every diagram into its fold container, carrying the
91
+ chevron mark plus the muted placeholder the diagram leaves
92
+ behind while folded, which is the very icon of its type
93
+ control; a diagram joins the set of its type plus the sets
94
+ of all nesting levels up to its own one (3 standing for all
95
+ deeper ones), so the sets of the controls overlap */
96
+ const folds = { graph: [], hub: [], grid: [], code: [], image: [], listing: [],
97
+ level1: [], level2: [], level3: [], text: [] }
98
+ document.querySelectorAll("article div.diagram, nav.doc div.diagram").forEach((el) => {
99
+ const type = el.getAttribute("data-type") ?? "graph"
100
+ const level = Math.min(Number(el.getAttribute("data-level")) || 1, 3)
101
+ const fold = document.createElement("div")
102
+ fold.className = "fold"
103
+ el.parentNode.insertBefore(fold, el)
104
+ const chevron = mark("fold-mark", "fold/unfold this diagram")
105
+ const label = document.createElement("span")
106
+ label.className = "fold-label"
107
+ label.appendChild(controls[type].querySelector("svg").cloneNode(true))
108
+ fold.append(chevron, label, el)
109
+ chevron.addEventListener("click", () => {
110
+ fold.classList.toggle("folded")
111
+ sync()
112
+ })
113
+ folds[type].push(fold)
114
+ for (let n = 1; n <= level; n++)
115
+ folds["level" + n].push(fold)
116
+ })
117
+
118
+ /* the rendered height of the content of a table cell, or of its
119
+ leading part up to a word position: a cell box itself always
120
+ spans the whole row, so only a range over the content
121
+ measures the text */
122
+ const range = document.createRange()
123
+ const height = (cell, upto) => {
124
+ if (upto === undefined)
125
+ range.selectNodeContents(cell)
126
+ else {
127
+ range.setStart(cell, 0)
128
+ range.setEnd(upto.node, upto.offset)
129
+ }
130
+ return range.getBoundingClientRect().height
131
+ }
132
+
133
+ /* the word positions of a table cell, each right behind one word */
134
+ const positions = (cell) => {
135
+ const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT)
136
+ const list = []
137
+ let node
138
+ while ((node = walker.nextNode()) !== null) {
139
+ const regex = /\\S+/g
140
+ let m
141
+ while ((m = regex.exec(node.nodeValue ?? "")) !== null)
142
+ list.push({ node, offset: m.index + m[0].length })
143
+ }
144
+ return list
145
+ }
146
+
147
+ /* a cell taking part in the comparison of its row: one with
148
+ content of its own, i.e. neither empty (a not given property
149
+ draws its marker through the stylesheet alone) nor holding
150
+ further cells (a chunked or nested table, whose own cells are
151
+ compared instead) nor a diagram (which folds on its own and
152
+ whose SVG text cannot carry the wrapper of a hidden
153
+ remainder) */
154
+ const candidate = (cell) =>
155
+ cell.querySelector("td") === null
156
+ && cell.querySelector("div.diagram") === null
157
+ && (cell.textContent.trim() !== "" || cell.querySelector("img") !== null)
158
+
159
+ /* hide a node of the folded remainder of a table cell: an
160
+ element carries the class itself, a text node gets wrapped */
161
+ const rest = (node) => {
162
+ if (node.nodeType === Node.ELEMENT_NODE)
163
+ node.classList.add("fold-rest")
164
+ else if (node.nodeType === Node.TEXT_NODE) {
165
+ const span = document.createElement("span")
166
+ span.className = "fold-rest fold-wrap"
167
+ node.parentNode.insertBefore(span, node)
168
+ span.appendChild(node)
169
+ }
170
+ }
171
+
172
+ /* take the cut of a table cell back again: the chevron mark
173
+ leaves, the remainder sheds its class or wrapper, and the
174
+ split text nodes merge */
175
+ const uncut = (cell) => {
176
+ cell.classList.remove("folded")
177
+ cell.querySelectorAll("span.fold-more").forEach((el) => { el.remove() })
178
+ cell.querySelectorAll(".fold-rest").forEach((el) => {
179
+ if (el.classList.contains("fold-wrap"))
180
+ el.replaceWith(...el.childNodes)
181
+ else
182
+ el.classList.remove("fold-rest")
183
+ })
184
+ cell.normalize()
185
+ }
186
+
187
+ /* the minimum percentage of its own height a cell has to gain
188
+ by folding, as hiding less than that is no visible relief
189
+ and only costs the reader a chevron to click (measured after
190
+ the cut, as the chevron itself can claim a line of its own) */
191
+ const cellGain = ${cellGainMin}
192
+
193
+ /* lay out the text folds, which holds for the current line
194
+ breaks only and hence is redone on every change of them: the
195
+ previous cuts are taken back, with their fold state carried
196
+ over (a cell folding anew follows the others) */
197
+ const layout = () => {${textless ? `
198
+ return` : ""}
199
+ const all = folds.text.length > 0 && folds.text.every((cell) => cell.classList.contains("folded"))
200
+ const state = new Map(folds.text.map((cell) => [ cell, cell.classList.contains("folded") ]))
201
+ folds.text.forEach(uncut)
202
+ folds.text.length = 0
203
+
204
+ /* decide which table cells fold: one which is taller than the
205
+ configured percentage above every other cell of its row (the
206
+ leading name column of a compact table excluded, as it is no
207
+ text of its own). All rows are measured before the first cut,
208
+ as every cut shifts the layout of the ones still to come */
209
+ const cuts = []
210
+ document.querySelectorAll("article table tr").forEach((row) => {
211
+ const table = row.closest("table")
212
+ let cells = Array.from(row.children).filter((el) => el.tagName === "TD")
213
+ if (!table.classList.contains("chunks"))
214
+ cells = cells.slice(1)
215
+ cells = cells.filter(candidate)
216
+ if (cells.length < 2)
217
+ return
218
+ const configured = row.closest("table[data-fold-height]")
219
+ const percent = Number(configured?.getAttribute("data-fold-height")) || cellHeight
220
+ const heights = cells.map((cell) => height(cell))
221
+ cells.forEach((cell, i) => {
222
+ const other = Math.max(...heights.filter((_, j) => j !== i))
223
+ const limit = other * (1 + percent / 100)
224
+ if (heights[i] > limit)
225
+ cuts.push({ cell, limit, full: heights[i] })
226
+ })
227
+ })
228
+
229
+ /* fold the decided cells back onto their height limit: the last
230
+ word position still fitting is found by a binary search over
231
+ the measured word positions, the text node is split there and
232
+ the chevron mark takes its place, followed by the remainder
233
+ hidden up the ancestor chain of the cell */
234
+ for (const cut of cuts) {
235
+ const list = positions(cut.cell)
236
+ if (list.length === 0)
237
+ continue
238
+ let lo = 0
239
+ let hi = list.length - 1
240
+ while (lo < hi) {
241
+ const mid = Math.ceil((lo + hi) / 2)
242
+ if (height(cut.cell, list[mid]) <= cut.limit)
243
+ lo = mid
244
+ else
245
+ hi = mid - 1
246
+ }
247
+ const tail = list[lo].node.splitText(list[lo].offset)
248
+ const chevron = mark("fold-more", "fold/unfold the remaining text")
249
+ tail.parentNode.insertBefore(chevron, tail)
250
+ let current = chevron
251
+ while (current !== cut.cell) {
252
+ let next = current.nextSibling
253
+ while (next !== null) {
254
+ const following = next.nextSibling
255
+ rest(next)
256
+ next = following
257
+ }
258
+ current = current.parentNode
259
+ }
260
+
261
+ /* the gain is measured on the applied fold, as only that
262
+ one tells the real one, and a fold gaining less than the
263
+ minimum share is taken back again, while a kept fold
264
+ sheds the class again, as every cell starts out unfolded */
265
+ cut.cell.classList.add("folded")
266
+ const gained = height(cut.cell) <= cut.full * (1 - cellGain / 100)
267
+ if (!gained) {
268
+ uncut(cut.cell)
269
+ continue
270
+ }
271
+ cut.cell.classList.toggle("folded", state.get(cut.cell) ?? all)
272
+ folds.text.push(cut.cell)
273
+ chevron.addEventListener("click", (event) => {
274
+ /* the cut can land inside a hyperlink, whose navigation
275
+ the chevron has to suppress for its own click alone,
276
+ so the remaining link text still follows the link */
277
+ event.preventDefault()
278
+ cut.cell.classList.toggle("folded")
279
+ sync()
280
+ })
281
+ }
282
+ }
283
+ layout()
284
+
285
+ /* mark the text control while any cell text is folded, a diagram
286
+ control while it is active itself (as the sets of the diagram
287
+ controls overlap, the fold state of their diagrams tells
288
+ nothing), and the tab itself while anything at all is folded */
289
+ const active = new Set()
290
+ const sync = () => {
291
+ for (const kind of Object.keys(folds))
292
+ controls[kind].classList.toggle("folded", kind === "text" ?
293
+ folds.text.some((fold) => fold.classList.contains("folded")) : active.has(kind))
294
+ tab.classList.toggle("folded", Object.keys(folds).some((kind) =>
295
+ folds[kind].some((fold) => fold.classList.contains("folded"))))
296
+ }
297
+
298
+ /* fold exactly the diagrams at least one active control covers */
299
+ const apply = () => {
300
+ folds.level1.forEach((fold) => {
301
+ fold.classList.toggle("folded", Array.from(active).some((kind) => folds[kind].includes(fold)))
302
+ })
303
+ }
304
+
305
+ /* without a stored state the rendered one stands, which is
306
+ everything unfolded: the cell texts store their state as a
307
+ whole, the diagrams as the list of their active controls */
308
+ const stored = { text: null, diagrams: null }
309
+ for (const name of Object.keys(stored)) {
310
+ try { stored[name] = localStorage.getItem("specbook-fold-" + name) }
311
+ catch { /* an inaccessible storage just means no stored state */ }
312
+ }
313
+ if (stored.text !== null)
314
+ folds.text.forEach((fold) => { fold.classList.toggle("folded", stored.text === "folded") })
315
+ if (stored.diagrams !== null) {
316
+ stored.diagrams.split(",").filter((kind) => kind !== "text" && Object.hasOwn(folds, kind))
317
+ .forEach((kind) => { active.add(kind) })
318
+ apply()
319
+ }
320
+ for (const kind of Object.keys(folds)) {
321
+ controls[kind].addEventListener("click", () => {
322
+ try {
323
+ if (kind === "text") {
324
+ const all = !folds.text.every((fold) => fold.classList.contains("folded"))
325
+ folds.text.forEach((fold) => { fold.classList.toggle("folded", all) })
326
+ localStorage.setItem("specbook-fold-text", all ? "folded" : "unfolded")
327
+ }
328
+ else {
329
+ if (!active.delete(kind))
330
+ active.add(kind)
331
+ apply()
332
+ localStorage.setItem("specbook-fold-diagrams", Array.from(active).join(","))
333
+ }
334
+ }
335
+ catch { /* an inaccessible storage just loses the state */ }
336
+ sync()
337
+ })
338
+ }
339
+ sync()
340
+
341
+ /* redo the text folds once the line breaks change, which a new
342
+ viewport width and the arrival of the embedded fonts (still
343
+ loading while this script runs) cause. A running search
344
+ (which shows the remainders) defers it, and a live preview
345
+ body swap, which left this script behind, ends it */
346
+ let width = document.documentElement.clientWidth
347
+ let timer = 0
348
+ const relayout = () => {
349
+ if (!tab.isConnected)
350
+ return
351
+ if (document.body.classList.contains("searching")) {
352
+ schedule()
353
+ return
354
+ }
355
+ layout()
356
+ sync()
357
+ }
358
+ const schedule = () => {
359
+ clearTimeout(timer)
360
+ timer = setTimeout(relayout, 250)
361
+ }
362
+ const resized = () => {
363
+ if (!tab.isConnected)
364
+ window.removeEventListener("resize", resized)
365
+ else if (width !== document.documentElement.clientWidth) {
366
+ width = document.documentElement.clientWidth
367
+ schedule()
368
+ }
369
+ }
370
+ window.addEventListener("resize", resized)
371
+ document.fonts.ready.then(() => {
372
+ if (tab.isConnected)
373
+ schedule()
374
+ })
375
+ })()
376
+ `;
377
+ /* the estimated number of characters a text line spanning the
378
+ entire content width holds (60rem at the document font) */
379
+ const lineChars = 140;
380
+ /* omit the long texts of the comparable cells of a table row, the
381
+ server-side heuristic counterpart of the client-side cell text
382
+ folding, judging a cell by its estimated number of text lines (its
383
+ plain text per block, wrapped at the characters its column width
384
+ share holds per line) instead of its rendered height: a cell
385
+ exceeding every other one by more than the "maxCellHeight"
386
+ percentage is cut back onto the whole lines of that limit (less the
387
+ room of the mark) at a word boundary, its open elements closed
388
+ again, and ends in a grey "[...]" mark, unless that hides less than
389
+ 25% of it, where the empty cells and the ones carrying further cells
390
+ or a diagram take no part at all */
391
+ export const omitLong = (omit, cells, shares, percent = 0) => {
392
+ if (omit?.has("text:long") !== true)
393
+ return cells;
394
+ const htmls = cells.map(String);
395
+ const plain = (html) => html.replace(/<[^>]*>/g, "").trim().length;
396
+ const lengths = htmls.map((html) => (/<td[\s>]|<div class="diagram"/).test(html) ? 0 : plain(html));
397
+ if (lengths.filter((length) => length > 0).length < 2)
398
+ return cells;
399
+ const chars = shares.map((share) => Math.max(1, Math.floor(lineChars * share)));
400
+ const lines = htmls.map((html, i) => lengths[i] === 0 ? 0 :
401
+ html.split(/<\/(?:p|li|div|pre)>|<br\s*\/?>/)
402
+ .reduce((sum, block) => sum + Math.ceil(plain(block) / chars[i]), 0));
403
+ return htmls.map((html, i) => {
404
+ const other = Math.max(...lines.filter((_, j) => j !== i)) *
405
+ (1 + (percent > 0 ? percent : cellHeightDefault) / 100);
406
+ const limit = Math.floor(other) * chars[i] - " [...]".length;
407
+ if (lines[i] === 0 || lines[i] <= other || limit > lengths[i] * (1 - cellGainMin / 100))
408
+ return cells[i];
409
+ const open = new Array();
410
+ let out = "";
411
+ let count = 0;
412
+ for (const [token] of html.matchAll(/<[^>]*>|[^<]+/g)) {
413
+ if (token.startsWith("<")) {
414
+ const tag = (/^<(\/?)([a-zA-Z][^\s/>]*)[^>]*?(\/?)>$/).exec(token);
415
+ if (tag?.[1] === "/")
416
+ open.pop();
417
+ else if (tag !== null && tag[3] !== "/" && !(/^(?:br|hr|img|input|wbr)$/).test(tag[2]))
418
+ open.push(tag[2]);
419
+ }
420
+ else if (count + token.length > limit)
421
+ return safe(out + token.slice(0, limit - count).replace(/\S*$/, "") +
422
+ "<span class=\"omit\">[...]</span>" +
423
+ open.reverse().map((name) => `</${name}>`).join(""));
424
+ else
425
+ count += token.length;
426
+ out += token;
427
+ }
428
+ return cells[i];
429
+ });
430
+ };
431
+ /* the fold controls the omitted aspects drop, along with the implied
432
+ ones (no type control without diagrams of that type, no level
433
+ control without diagrams from that level on, no fold tab without
434
+ any control, which lets the tabs below it move up), plus the extra
435
+ style rules the omission brings (the "[...]" mark of the omitted
436
+ long texts and the moved-up tabs of a dropped fold tab) */
437
+ export const omittedControls = (omit) => {
438
+ const none = omit.has("diagram:1")
439
+ || (omit.has("diagram:graph") && omit.has("diagram:hub") && omit.has("diagram:grid")
440
+ && omit.has("diagram:code") && omit.has("diagram:image") && omit.has("diagram:listing"));
441
+ const omitted = {
442
+ graph: none || omit.has("diagram:graph"),
443
+ hub: none || omit.has("diagram:hub"),
444
+ grid: none || omit.has("diagram:grid"),
445
+ code: none || omit.has("diagram:code"),
446
+ image: none || omit.has("diagram:image"),
447
+ listing: none || omit.has("diagram:listing"),
448
+ level1: none,
449
+ level2: none || omit.has("diagram:2"),
450
+ level3: none || omit.has("diagram:2") || omit.has("diagram:3"),
451
+ text: omit.has("text:long"),
452
+ all: none && omit.has("text:long")
453
+ };
454
+ const css = (omitted.text ? "\nspan.omit { color: var(--theme-color-specbook-muted) }" : "") +
455
+ (omitted.all ? "\nnav.toc-panel div.toc-tab { top: 10.66rem }" +
456
+ "\ndiv.realtime-status { top: 13.89rem }" : "");
457
+ return { omitted, css };
458
+ };
@@ -0,0 +1,51 @@
1
+ import type nunjucks from "nunjucks";
2
+ import type { SpecArtifact, SpecObject } from "./specbook-format-spec.js";
3
+ import type { SchemaObject } from "./specbook-format-schema.js";
4
+ import { type LinkIndex } from "./specbook-link.js";
5
+ import { type ValueExpr } from "./specbook-parse-value.js";
6
+ import { type OmitAspect } from "./specbook-export-common.js";
7
+ import { type DiagramVariants } from "./specbook-export-diagram.js";
8
+ import { type ListingRenderer } from "./specbook-export-code.js";
9
+ import { type Coverage } from "./specbook-coverage.js";
10
+ import type { InfoEntry, SpecEntry } from "./specbook-export-html-widget.js";
11
+ export type DocumentState = {
12
+ lang: string | undefined;
13
+ linker: (text: string, compact: boolean) => string;
14
+ anchors: Map<SpecObject, string>;
15
+ members: Map<string, ValueExpr> | null;
16
+ schemas: Map<SpecObject, SchemaObject> | null;
17
+ diagrams: Map<SpecObject, string> | null;
18
+ images: Map<string, string>;
19
+ rasters: Map<string, boolean>;
20
+ embedded: Map<string, DiagramVariants>;
21
+ listings: ListingRenderer;
22
+ levels: Map<SpecObject, number>;
23
+ coverages: Map<SpecObject, Coverage[]> | null;
24
+ infoKeys: Map<SchemaObject, string> | null;
25
+ infoObjects: Map<SpecObject, string> | null;
26
+ omits: Set<OmitAspect>;
27
+ };
28
+ export declare const withDocument: <T>(state: DocumentState, body: () => T) => T;
29
+ export declare const makeLinker: (index: LinkIndex) => (text: string, compact: boolean) => string;
30
+ export declare const collectInfo: (nodes: SchemaObject[], keys: Map<SchemaObject, string>, info: InfoEntry[]) => void;
31
+ export declare const collectSpec: (objects: SpecObject[], spec: SpecEntry[]) => void;
32
+ export declare const collectMembers: (nodes: SchemaObject[], result: Map<string, ValueExpr>) => Map<string, ValueExpr>;
33
+ export declare const flowChildren: (object: SpecObject) => SpecObject[];
34
+ export declare const conciseGroup: (group: SpecObject[], schemaMap: Map<SpecObject, SchemaObject> | null, concise: boolean) => boolean;
35
+ export declare const groupChildren: (children: SpecObject[]) => SpecObject[][];
36
+ export declare const formatDate: (date: Date) => string;
37
+ export declare const renderTitlePage: (object: SpecObject, created: string, modified: string) => string;
38
+ export declare const renderArtifact: (artifact: SpecArtifact) => string;
39
+ type TocEntry = {
40
+ id: string;
41
+ kind: string;
42
+ name: nunjucks.runtime.SafeString;
43
+ level: number;
44
+ page?: number;
45
+ info?: string;
46
+ infopath?: string;
47
+ spec?: string;
48
+ };
49
+ export declare const tocEntries: (objects: SpecObject[], pages?: Map<string, number>) => TocEntry[];
50
+ export declare const renderTocPanel: (objects: SpecObject[], title: boolean, doc: boolean) => string;
51
+ export {};