@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
|
@@ -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 {};
|