@rse/specbook 1.3.0 → 1.3.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/dst/specbook-api.d.ts +13 -16
- package/dst/specbook-api.js +34 -18
- package/dst/specbook-cli.js +8 -6
- package/dst/specbook-cmd-describe.js +11 -7
- package/dst/specbook-cmd-export.js +6 -0
- package/dst/specbook-cmd-preview.d.ts +1 -0
- package/dst/specbook-cmd-preview.js +5 -4
- package/dst/specbook-config.js +11 -6
- package/dst/specbook-diagnostic.js +1 -1
- package/dst/specbook-diagram.js +74 -58
- package/dst/specbook-export-common.d.ts +2 -2
- package/dst/specbook-export-common.js +29 -18
- package/dst/specbook-export-html-search.js +10 -4
- package/dst/specbook-export-html.css +21 -21
- package/dst/specbook-export-html.js +102 -71
- package/dst/specbook-export-image.js +9 -1
- package/dst/specbook-export-md.js +3 -3
- package/dst/specbook-export-pdf.js +12 -6
- package/dst/specbook-ignore.js +35 -18
- package/dst/specbook-link.js +2 -2
- package/dst/specbook-mcp.js +5 -4
- package/dst/specbook-parse-common.js +6 -3
- package/dst/specbook-parse-semantic.js +8 -9
- package/dst/specbook-parse-syntax.js +13 -4
- package/dst/specbook-project.js +16 -3
- package/dst/specbook-theme.js +6 -2
- package/dst/specbook-verbose.js +3 -2
- package/package.json +4 -4
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
+
import { embeddingThemes } from "./specbook-parse-common.js";
|
|
7
8
|
import { plainText } from "./specbook-link.js";
|
|
8
9
|
/* escape a text for embedding into template HTML (text and attributes) */
|
|
9
10
|
export const escapeHtml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
@@ -26,9 +27,9 @@ export const omitAspects = ["diagram:graph", "diagram:hub", "diagram:grid",
|
|
|
26
27
|
plain "diagram" is the alias of "diagram:1", hence of all diagrams */
|
|
27
28
|
export const parseOmit = (omit = []) => {
|
|
28
29
|
const aspects = new Set();
|
|
29
|
-
for (const item of omit.flatMap((list) => list.split(",")).map((
|
|
30
|
-
const aspect = item === "diagram" ? "diagram:1" : item;
|
|
31
|
-
if (
|
|
30
|
+
for (const item of omit.flatMap((list) => list.split(",")).map((entry) => entry.trim())) {
|
|
31
|
+
const aspect = omitAspects.find((known) => known === (item === "diagram" ? "diagram:1" : item));
|
|
32
|
+
if (aspect === undefined)
|
|
32
33
|
throw new Error(`unknown omit aspect "${item}" ` +
|
|
33
34
|
`(expected "diagram" or one of "${omitAspects.join("\", \"")}")`);
|
|
34
35
|
aspects.add(aspect);
|
|
@@ -52,22 +53,33 @@ export const titleObject = (specification) => {
|
|
|
52
53
|
};
|
|
53
54
|
return search(specification.artifacts.flatMap((artifact) => artifact.objects));
|
|
54
55
|
};
|
|
55
|
-
/* determine
|
|
56
|
-
value counting as an absent property, so every
|
|
57
|
-
onto its own default) */
|
|
56
|
+
/* determine the (trimmed) value of a property of the title object (a
|
|
57
|
+
present but empty value counting as an absent property, so every
|
|
58
|
+
consumer falls back onto its own default) */
|
|
58
59
|
const titleProperty = (specification, name) => {
|
|
59
60
|
const value = titleObject(specification)
|
|
60
|
-
?.properties.find((property) => property.key === name)?.value;
|
|
61
|
-
return value !== undefined && value
|
|
61
|
+
?.properties.find((property) => property.key === name)?.value.trim();
|
|
62
|
+
return value !== undefined && value !== "" ? value : undefined;
|
|
62
63
|
};
|
|
63
64
|
/* determine the document language (LANG) from the title object */
|
|
64
|
-
export const documentLang = (specification) => titleProperty(specification, "LANG")
|
|
65
|
+
export const documentLang = (specification) => titleProperty(specification, "LANG");
|
|
65
66
|
/* determine the document character set (CHARSET) from the title object */
|
|
66
|
-
export const documentCharset = (specification) => titleProperty(specification, "CHARSET")
|
|
67
|
-
/* determine the document theme style (THEME-STYLE) from the title
|
|
68
|
-
|
|
67
|
+
export const documentCharset = (specification) => titleProperty(specification, "CHARSET");
|
|
68
|
+
/* determine the document theme style (THEME-STYLE) from the title
|
|
69
|
+
object, lower-cased and rejecting an unknown style (the styles being
|
|
70
|
+
the very themes the "{theme}" image embeddings know) */
|
|
71
|
+
export const documentThemeStyle = (specification) => {
|
|
72
|
+
const value = titleProperty(specification, "THEME-STYLE");
|
|
73
|
+
if (value === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const style = embeddingThemes.find((name) => name === value.toLowerCase());
|
|
76
|
+
if (style === undefined)
|
|
77
|
+
throw new Error(`unknown theme style "${value}" ` +
|
|
78
|
+
`(expected ${embeddingThemes.join(", ")})`);
|
|
79
|
+
return style;
|
|
80
|
+
};
|
|
69
81
|
/* determine the document theme color tone (THEME-TONE) from the title object */
|
|
70
|
-
export const documentThemeTone = (specification) => titleProperty(specification, "THEME-TONE")
|
|
82
|
+
export const documentThemeTone = (specification) => titleProperty(specification, "THEME-TONE");
|
|
71
83
|
/* the supported paper sizes for print: ISO A4 in millimeters and the
|
|
72
84
|
two US sizes closest to it in inches, each with the default margins
|
|
73
85
|
of 1in (25mm) at the top/bottom and 0.8in (20mm) at the left/right */
|
|
@@ -86,7 +98,7 @@ export const paperLength = (setup, value) => `${value}${setup.unit}`;
|
|
|
86
98
|
matched case-insensitively, falling back onto the default if unset
|
|
87
99
|
and rejecting an unknown size */
|
|
88
100
|
export const documentPaperSize = (specification) => {
|
|
89
|
-
const value = titleProperty(specification, "PAPER-SIZE")
|
|
101
|
+
const value = titleProperty(specification, "PAPER-SIZE");
|
|
90
102
|
if (value === undefined)
|
|
91
103
|
return paperSizeDefault;
|
|
92
104
|
const paper = paperSizes.find((name) => name.toLowerCase() === value.toLowerCase());
|
|
@@ -204,11 +216,10 @@ export const subsetStylesheet = async (charset) => {
|
|
|
204
216
|
the inline code markup stripped, as the plain-text targets (the HTML
|
|
205
217
|
<title>, the PDF metadata and page header) render no Markdown */
|
|
206
218
|
export const documentTitle = (specification) => {
|
|
207
|
-
const title = titleProperty(specification, "TITLE")
|
|
208
|
-
const subtitle = titleProperty(specification, "SUBTITLE")
|
|
219
|
+
const title = titleProperty(specification, "TITLE");
|
|
220
|
+
const subtitle = titleProperty(specification, "SUBTITLE");
|
|
209
221
|
return {
|
|
210
|
-
title: plainText(title
|
|
211
|
-
titleObject(specification)?.name ?? "Specification"),
|
|
222
|
+
title: plainText(title ?? "Specification"),
|
|
212
223
|
subtitle: subtitle !== undefined ? plainText(subtitle) : undefined
|
|
213
224
|
};
|
|
214
225
|
};
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
event.preventDefault()
|
|
97
97
|
reset()
|
|
98
98
|
slide(false)
|
|
99
|
-
link.
|
|
99
|
+
link.click()
|
|
100
100
|
})
|
|
101
101
|
})
|
|
102
102
|
}
|
|
@@ -273,8 +273,11 @@
|
|
|
273
273
|
to two passes, literal substrings first and, only if those match
|
|
274
274
|
nothing at all, the fuzzy variants, as the edit-distance
|
|
275
275
|
neighborhood of a word is wide enough to dilute a correctly
|
|
276
|
-
spelled query and has to stay a fallback for misspelled ones
|
|
276
|
+
spelled query and has to stay a fallback for misspelled ones;
|
|
277
|
+
a debounced run a live preview body swap left behind is dropped */
|
|
277
278
|
const search = () => {
|
|
279
|
+
if (!tab.isConnected)
|
|
280
|
+
return
|
|
278
281
|
unmark()
|
|
279
282
|
const parts = input.value.toLowerCase().split(",")
|
|
280
283
|
.map((part) => part.split(/\s+/).filter((word) => word !== ""))
|
|
@@ -351,14 +354,17 @@
|
|
|
351
354
|
panel entries) along: an entry stays visible only while its
|
|
352
355
|
target still is, which covers the front matter entries of
|
|
353
356
|
the panel, too, as their targets are suppressed by the
|
|
354
|
-
search mode anyway
|
|
357
|
+
search mode anyway, where the entries are hidden only after
|
|
358
|
+
all targets got measured, as each hiding would force a layout again */
|
|
359
|
+
const hidden = []
|
|
355
360
|
document.querySelectorAll("nav.toc table tr, nav.toc-panel div.toc-list li").forEach((entry) => {
|
|
356
361
|
const link = entry.querySelector(":scope > a, :scope > td > a")
|
|
357
362
|
const target = link !== null ?
|
|
358
363
|
document.getElementById(decodeURIComponent(link.hash.slice(1))) : null
|
|
359
364
|
if (target === null || target.getClientRects().length === 0)
|
|
360
|
-
|
|
365
|
+
hidden.push(entry)
|
|
361
366
|
})
|
|
367
|
+
hidden.forEach((entry) => { entry.classList.add("search-hide") })
|
|
362
368
|
}
|
|
363
369
|
|
|
364
370
|
/* run the search debounced on every keystroke (750ms after the
|
|
@@ -202,6 +202,10 @@ body::before {
|
|
|
202
202
|
padding: 0;
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
+
/* a tab riding right of the brand bar (fixed independent of the
|
|
206
|
+
content scrolling, at its vertical offset and inset by a pixel or
|
|
207
|
+
two from the bar), which follows the bar when the table of contents
|
|
208
|
+
panel shifts it aside */
|
|
205
209
|
/* the theme-switching tab (screen-only): the topmost tab riding right
|
|
206
210
|
of the brand bar above the info tab (sized like the table of
|
|
207
211
|
contents tab, fixed independent of the content scrolling), showing
|
|
@@ -213,12 +217,12 @@ div.theme-switch {
|
|
|
213
217
|
top: 0.97rem;
|
|
214
218
|
left: calc(0.6rem - 1px);
|
|
215
219
|
z-index: 30;
|
|
216
|
-
padding: 0.9rem 0.35rem;
|
|
217
|
-
line-height: 0;
|
|
218
220
|
border-radius: 0 0.3rem 0.3rem 0;
|
|
219
221
|
color: var(--theme-color-specbook-background);
|
|
220
222
|
background-color: var(--theme-color-specbook-accent);
|
|
221
223
|
transition: left 0.3s ease;
|
|
224
|
+
padding: 0.9rem 0.35rem;
|
|
225
|
+
line-height: 0;
|
|
222
226
|
cursor: pointer;
|
|
223
227
|
-webkit-user-select: none;
|
|
224
228
|
user-select: none;
|
|
@@ -253,12 +257,12 @@ div.info-switch {
|
|
|
253
257
|
top: 4.2rem;
|
|
254
258
|
left: calc(0.6rem - 1px);
|
|
255
259
|
z-index: 30;
|
|
256
|
-
padding: 0.65rem 0.1rem;
|
|
257
|
-
line-height: 0;
|
|
258
260
|
border-radius: 0 0.3rem 0.3rem 0;
|
|
259
261
|
color: var(--theme-color-specbook-background);
|
|
260
262
|
background-color: var(--theme-color-specbook-accent);
|
|
261
263
|
transition: left 0.3s ease;
|
|
264
|
+
padding: 0.65rem 0.1rem;
|
|
265
|
+
line-height: 0;
|
|
262
266
|
cursor: pointer;
|
|
263
267
|
-webkit-user-select: none;
|
|
264
268
|
user-select: none;
|
|
@@ -286,6 +290,8 @@ body.info-on div.info-switch svg.info-icon {
|
|
|
286
290
|
display: none;
|
|
287
291
|
}
|
|
288
292
|
}
|
|
293
|
+
/* the circle icon leading an object kind (filled) or a property
|
|
294
|
+
name (not filled), sized and aligned onto the text */
|
|
289
295
|
/* the description popup (screen-only): a single shared, script-
|
|
290
296
|
positioned element showing the schema description of the hovered
|
|
291
297
|
object kind or property name, or the corpus description of the
|
|
@@ -440,12 +446,12 @@ div.fold-switch {
|
|
|
440
446
|
top: 10.66rem;
|
|
441
447
|
left: calc(0.6rem - 1px);
|
|
442
448
|
z-index: 30;
|
|
443
|
-
display: flex;
|
|
444
|
-
align-items: center;
|
|
445
449
|
border-radius: 0 0.3rem 0.3rem 0;
|
|
446
450
|
color: var(--theme-color-specbook-background);
|
|
447
451
|
background-color: var(--theme-color-specbook-accent);
|
|
448
452
|
transition: left 0.3s ease;
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
449
455
|
}
|
|
450
456
|
body:has(nav.toc-panel.open) div.fold-switch {
|
|
451
457
|
left: calc(31rem - 1px);
|
|
@@ -694,12 +700,12 @@ div.realtime-status {
|
|
|
694
700
|
top: 17.12rem;
|
|
695
701
|
left: calc(0.6rem - 2px);
|
|
696
702
|
z-index: 30;
|
|
697
|
-
padding: 0.65rem 0.1rem;
|
|
698
|
-
line-height: 0;
|
|
699
703
|
border-radius: 0 0.3rem 0.3rem 0;
|
|
700
704
|
color: var(--theme-color-specbook-background);
|
|
701
705
|
background-color: var(--theme-color-specbook-accent);
|
|
702
706
|
transition: left 0.3s ease;
|
|
707
|
+
padding: 0.65rem 0.1rem;
|
|
708
|
+
line-height: 0;
|
|
703
709
|
-webkit-user-select: none;
|
|
704
710
|
user-select: none;
|
|
705
711
|
}
|
|
@@ -954,7 +960,6 @@ h4 {
|
|
|
954
960
|
h4,
|
|
955
961
|
h5,
|
|
956
962
|
h6 {
|
|
957
|
-
page-break-after: avoid;
|
|
958
963
|
break-after: avoid;
|
|
959
964
|
}
|
|
960
965
|
}
|
|
@@ -1001,21 +1006,21 @@ a {
|
|
|
1001
1006
|
}
|
|
1002
1007
|
.object-kind::before {
|
|
1003
1008
|
content: "\25CF";
|
|
1004
|
-
color: var(--theme-color-specbook-accent);
|
|
1005
1009
|
font-weight: normal;
|
|
1006
1010
|
font-size: 120%;
|
|
1007
1011
|
margin-right: 0.25rem;
|
|
1008
1012
|
vertical-align: -0.075em;
|
|
1009
1013
|
line-height: 0;
|
|
1014
|
+
color: var(--theme-color-specbook-accent);
|
|
1010
1015
|
}
|
|
1011
1016
|
.property-name::before {
|
|
1012
1017
|
content: "\25CB";
|
|
1013
|
-
color: var(--theme-color-specbook-accent);
|
|
1014
1018
|
font-weight: normal;
|
|
1015
1019
|
font-size: 120%;
|
|
1016
1020
|
margin-right: 0.25rem;
|
|
1017
1021
|
vertical-align: -0.075em;
|
|
1018
1022
|
line-height: 0;
|
|
1023
|
+
color: var(--theme-color-specbook-accent);
|
|
1019
1024
|
}
|
|
1020
1025
|
span.link-broken {
|
|
1021
1026
|
color: var(--theme-color-specbook-error);
|
|
@@ -1048,12 +1053,12 @@ a.link-compact {
|
|
|
1048
1053
|
}
|
|
1049
1054
|
a.link-compact::before {
|
|
1050
1055
|
content: "\25CF";
|
|
1051
|
-
color: var(--theme-color-specbook-accent);
|
|
1052
1056
|
font-weight: normal;
|
|
1053
1057
|
font-size: 120%;
|
|
1054
1058
|
margin-right: 0.25rem;
|
|
1055
1059
|
vertical-align: -0.075em;
|
|
1056
1060
|
line-height: 0;
|
|
1061
|
+
color: var(--theme-color-specbook-accent);
|
|
1057
1062
|
}
|
|
1058
1063
|
span.anchor-paren {
|
|
1059
1064
|
color: var(--theme-color-specbook-muted);
|
|
@@ -1172,14 +1177,12 @@ table.objects table.chunks th:not(.property-name) {
|
|
|
1172
1177
|
/* the diagram leading the chunks of an object, kept in one piece */
|
|
1173
1178
|
table.objects table.chunks div.diagram {
|
|
1174
1179
|
margin: 0.15rem 0;
|
|
1175
|
-
page-break-inside: avoid;
|
|
1176
1180
|
break-inside: avoid;
|
|
1177
1181
|
}
|
|
1178
1182
|
/* bundle a chunk header row with its value row, so neither the
|
|
1179
1183
|
"Diagram" header nor the property names are orphaned at a page end */
|
|
1180
1184
|
@media print {
|
|
1181
1185
|
table.objects table.chunks tr:has(> th) {
|
|
1182
|
-
page-break-after: avoid;
|
|
1183
1186
|
break-after: avoid;
|
|
1184
1187
|
}
|
|
1185
1188
|
}
|
|
@@ -1359,7 +1362,6 @@ code {
|
|
|
1359
1362
|
div.titlepage {
|
|
1360
1363
|
text-align: center;
|
|
1361
1364
|
margin: 6rem 0 4rem 0;
|
|
1362
|
-
page-break-after: always;
|
|
1363
1365
|
break-after: page;
|
|
1364
1366
|
}
|
|
1365
1367
|
div.titlepage .logo {
|
|
@@ -1424,12 +1426,12 @@ div.search {
|
|
|
1424
1426
|
top: 7.43rem;
|
|
1425
1427
|
left: calc(0.6rem - 1px);
|
|
1426
1428
|
z-index: 30;
|
|
1427
|
-
display: flex;
|
|
1428
|
-
align-items: center;
|
|
1429
1429
|
border-radius: 0 0.3rem 0.3rem 0;
|
|
1430
1430
|
color: var(--theme-color-specbook-background);
|
|
1431
1431
|
background-color: var(--theme-color-specbook-accent);
|
|
1432
1432
|
transition: left 0.3s ease;
|
|
1433
|
+
display: flex;
|
|
1434
|
+
align-items: center;
|
|
1433
1435
|
}
|
|
1434
1436
|
body:has(nav.toc-panel.open) div.search {
|
|
1435
1437
|
left: calc(31rem - 1px);
|
|
@@ -1506,7 +1508,8 @@ div.search span.search-clear:hover {
|
|
|
1506
1508
|
/* the search result filtering and matched word highlighting, where
|
|
1507
1509
|
the SVG text labels of the diagrams (having no background) mimic
|
|
1508
1510
|
the <mark> look with a rounded <rect> placed below the glyphs */
|
|
1509
|
-
.search-hide
|
|
1511
|
+
.search-hide,
|
|
1512
|
+
div.fold:has(> div.diagram.search-hide) {
|
|
1510
1513
|
display: none;
|
|
1511
1514
|
}
|
|
1512
1515
|
body.searching nav.doc,
|
|
@@ -1531,7 +1534,6 @@ rect.search-hit {
|
|
|
1531
1534
|
the deeper ones unnumbered) and indented per nesting level, where
|
|
1532
1535
|
the (potentially multi-page) table paginates between its rows only */
|
|
1533
1536
|
nav.toc {
|
|
1534
|
-
page-break-after: always;
|
|
1535
1537
|
break-after: page;
|
|
1536
1538
|
counter-reset: toc1;
|
|
1537
1539
|
}
|
|
@@ -1585,7 +1587,6 @@ nav.toc table {
|
|
|
1585
1587
|
width: 100%;
|
|
1586
1588
|
}
|
|
1587
1589
|
nav.toc tr {
|
|
1588
|
-
page-break-inside: avoid;
|
|
1589
1590
|
break-inside: avoid;
|
|
1590
1591
|
}
|
|
1591
1592
|
nav.toc td {
|
|
@@ -1607,7 +1608,6 @@ nav.toc a {
|
|
|
1607
1608
|
}
|
|
1608
1609
|
/* the diagram of contents (the diagram of the title object) */
|
|
1609
1610
|
nav.doc {
|
|
1610
|
-
page-break-after: always;
|
|
1611
1611
|
break-after: page;
|
|
1612
1612
|
}
|
|
1613
1613
|
nav.doc h1 {
|
|
@@ -258,17 +258,19 @@ const themeScript = textframe `
|
|
|
258
258
|
let style = null
|
|
259
259
|
try { style = localStorage.getItem("specbook-theme") }
|
|
260
260
|
catch { /* an inaccessible storage just means no stored choice */ }
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
style =
|
|
264
|
-
}
|
|
261
|
+
const html = document.documentElement.classList
|
|
262
|
+
if (style === null)
|
|
263
|
+
style = html.contains("theme-dark") ? "dark" : (html.contains("theme-light") ? "light" : null)
|
|
265
264
|
if (style === null)
|
|
266
265
|
style = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
|
267
|
-
|
|
266
|
+
html.remove("theme-light", "theme-dark")
|
|
267
|
+
html.add("theme-" + style)
|
|
268
268
|
})()
|
|
269
269
|
function themeSwitch () {
|
|
270
|
-
const
|
|
271
|
-
|
|
270
|
+
const html = document.documentElement.classList
|
|
271
|
+
const style = html.contains("theme-dark") ? "light" : "dark"
|
|
272
|
+
html.remove("theme-light", "theme-dark")
|
|
273
|
+
html.add("theme-" + style)
|
|
272
274
|
try { localStorage.setItem("specbook-theme", style) }
|
|
273
275
|
catch { /* an inaccessible storage just loses the choice */ }
|
|
274
276
|
}
|
|
@@ -415,6 +417,13 @@ const scrollProgressScript = textframe `
|
|
|
415
417
|
update()
|
|
416
418
|
})()
|
|
417
419
|
`;
|
|
420
|
+
/* the default percentage by which the text of a table cell may exceed
|
|
421
|
+
the height of every other cell of its row before it folds (or is
|
|
422
|
+
omitted), overridden per object kind by "maxCellHeight", and the
|
|
423
|
+
minimum percentage of its own height a cell has to gain by that,
|
|
424
|
+
shared by the client-side folding and the server-side omitting */
|
|
425
|
+
const cellHeightDefault = 40;
|
|
426
|
+
const cellGainMin = 25;
|
|
418
427
|
/* the client-side script of the folding: it wraps every diagram of the
|
|
419
428
|
content into a fold container carrying a chevron mark at its top
|
|
420
429
|
left corner, folds the text of every table cell towering over the
|
|
@@ -461,7 +470,7 @@ const foldScript = (standins, textless) => textframe `
|
|
|
461
470
|
exceed the height of every other cell of its row before it
|
|
462
471
|
folds, overridden per object kind by the "data-fold-height"
|
|
463
472
|
attribute the schema configuration puts onto its table */
|
|
464
|
-
const cellHeight =
|
|
473
|
+
const cellHeight = ${cellHeightDefault}
|
|
465
474
|
|
|
466
475
|
/* the chevron mark of a foldable element, rotated by the
|
|
467
476
|
stylesheet while the element is folded */
|
|
@@ -584,7 +593,7 @@ const foldScript = (standins, textless) => textframe `
|
|
|
584
593
|
by folding, as hiding less than that is no visible relief
|
|
585
594
|
and only costs the reader a chevron to click (measured after
|
|
586
595
|
the cut, as the chevron itself can claim a line of its own) */
|
|
587
|
-
const cellGain =
|
|
596
|
+
const cellGain = ${cellGainMin}
|
|
588
597
|
|
|
589
598
|
/* lay out the text folds, which holds for the current line
|
|
590
599
|
breaks only and hence is redone on every change of them: the
|
|
@@ -709,7 +718,7 @@ const foldScript = (standins, textless) => textframe `
|
|
|
709
718
|
if (stored.text !== null)
|
|
710
719
|
folds.text.forEach((fold) => { fold.classList.toggle("folded", stored.text === "folded") })
|
|
711
720
|
if (stored.diagrams !== null) {
|
|
712
|
-
stored.diagrams.split(",").filter((kind) => kind !== "text" && folds
|
|
721
|
+
stored.diagrams.split(",").filter((kind) => kind !== "text" && Object.hasOwn(folds, kind))
|
|
713
722
|
.forEach((kind) => { active.add(kind) })
|
|
714
723
|
apply()
|
|
715
724
|
}
|
|
@@ -1026,21 +1035,12 @@ const infoPopupScript = textframe `
|
|
|
1026
1035
|
catch { /* an inaccessible storage just loses the choice */ }
|
|
1027
1036
|
apply()
|
|
1028
1037
|
})
|
|
1029
|
-
const show = (el) => {
|
|
1030
|
-
/* a diagram node box carries no popup attributes itself, but
|
|
1031
|
-
hyperlinks its object, whose heading name or table row name
|
|
1032
|
-
carries the instance popup attributes */
|
|
1033
|
-
let source = el
|
|
1034
|
-
if (!el.hasAttribute("data-info") && !el.hasAttribute("data-info-spec")) {
|
|
1035
|
-
const target = document.getElementById(
|
|
1036
|
-
decodeURIComponent((el.getAttribute("href") ?? "").replace(/^#/, "")))
|
|
1037
|
-
source = target === null ? null :
|
|
1038
|
-
(target.hasAttribute("data-info-spec") ? target : target.querySelector("[data-info-spec]"))
|
|
1039
|
-
if (source === null)
|
|
1040
|
-
return
|
|
1041
|
-
}
|
|
1042
|
-
current = el
|
|
1043
1038
|
|
|
1039
|
+
/* compose the title of a popup: the title path segments of the
|
|
1040
|
+
object (the last one kind-only for a kind popup), the anchor
|
|
1041
|
+
id trailing an instance popup, and the property name trailing
|
|
1042
|
+
a property popup */
|
|
1043
|
+
const titleOf = (source, spec, prop) => {
|
|
1044
1044
|
/* compose the title path segments of an object */
|
|
1045
1045
|
const pathOf = (n, named) => {
|
|
1046
1046
|
const [ , up, kind, title ] = SPEC[n]
|
|
@@ -1048,10 +1048,6 @@ const infoPopupScript = textframe `
|
|
|
1048
1048
|
return [ ...(up >= 0 ? pathOf(up, true) : []),
|
|
1049
1049
|
name !== "" && kind !== "" ? kind + ": " + name : (name !== "" ? name : kind) ]
|
|
1050
1050
|
}
|
|
1051
|
-
const key = source.getAttribute("data-info")
|
|
1052
|
-
const spec = source.getAttribute("data-info-spec")
|
|
1053
|
-
const prop = source.getAttribute("data-info-prop")
|
|
1054
|
-
popup.classList.toggle("spec", spec !== null)
|
|
1055
1051
|
const title = document.createElement("div")
|
|
1056
1052
|
title.className = "info-title"
|
|
1057
1053
|
|
|
@@ -1076,7 +1072,7 @@ const infoPopupScript = textframe `
|
|
|
1076
1072
|
description color), where a segment without a kind
|
|
1077
1073
|
prefix is the kind-only ending of a kind popup or the
|
|
1078
1074
|
name of a kind-less object */
|
|
1079
|
-
const m = /^([
|
|
1075
|
+
const m = /^([^\\s:]+): (.*)$/.exec(segment)
|
|
1080
1076
|
const bare = i === segments.length - 1 && spec === null && prop === null
|
|
1081
1077
|
const name = document.createElement("span")
|
|
1082
1078
|
name.className = "info-name"
|
|
@@ -1116,7 +1112,27 @@ const infoPopupScript = textframe `
|
|
|
1116
1112
|
name.textContent = prop
|
|
1117
1113
|
title.appendChild(name)
|
|
1118
1114
|
}
|
|
1119
|
-
|
|
1115
|
+
return title
|
|
1116
|
+
}
|
|
1117
|
+
const show = (el) => {
|
|
1118
|
+
/* a diagram node box carries no popup attributes itself, but
|
|
1119
|
+
hyperlinks its object, whose heading name or table row name
|
|
1120
|
+
carries the instance popup attributes */
|
|
1121
|
+
let source = el
|
|
1122
|
+
if (!el.hasAttribute("data-info") && !el.hasAttribute("data-info-spec")) {
|
|
1123
|
+
const target = document.getElementById(
|
|
1124
|
+
decodeURIComponent((el.getAttribute("href") ?? "").replace(/^#/, "")))
|
|
1125
|
+
source = target === null ? null :
|
|
1126
|
+
(target.hasAttribute("data-info-spec") ? target : target.querySelector("[data-info-spec]"))
|
|
1127
|
+
if (source === null)
|
|
1128
|
+
return
|
|
1129
|
+
}
|
|
1130
|
+
current = el
|
|
1131
|
+
const key = source.getAttribute("data-info")
|
|
1132
|
+
const spec = source.getAttribute("data-info-spec")
|
|
1133
|
+
const prop = source.getAttribute("data-info-prop")
|
|
1134
|
+
popup.classList.toggle("spec", spec !== null)
|
|
1135
|
+
popup.replaceChildren(titleOf(source, spec, prop))
|
|
1120
1136
|
const entry = key !== null ? INFO[key] : undefined
|
|
1121
1137
|
const desc = prop !== null ? entry?.p?.[prop] :
|
|
1122
1138
|
(spec !== null ? SPEC[spec]?.[4] : entry?.d)
|
|
@@ -1608,9 +1624,10 @@ const omitLong = (cells, shares, percent = 0) => {
|
|
|
1608
1624
|
html.split(/<\/(?:p|li|div|pre)>|<br\s*\/?>/)
|
|
1609
1625
|
.reduce((sum, block) => sum + Math.ceil(plain(block) / chars[i]), 0));
|
|
1610
1626
|
return htmls.map((html, i) => {
|
|
1611
|
-
const
|
|
1612
|
-
(1 + (percent > 0 ? percent :
|
|
1613
|
-
|
|
1627
|
+
const other = Math.max(...lines.filter((_, j) => j !== i)) *
|
|
1628
|
+
(1 + (percent > 0 ? percent : cellHeightDefault) / 100);
|
|
1629
|
+
const limit = Math.floor(other) * chars[i] - " [...]".length;
|
|
1630
|
+
if (lines[i] === 0 || lines[i] <= other || limit > lengths[i] * (1 - cellGainMin / 100))
|
|
1614
1631
|
return cells[i];
|
|
1615
1632
|
const open = new Array();
|
|
1616
1633
|
let out = "";
|
|
@@ -1620,7 +1637,7 @@ const omitLong = (cells, shares, percent = 0) => {
|
|
|
1620
1637
|
const tag = (/^<(\/?)([a-zA-Z][^\s/>]*)[^>]*?(\/?)>$/).exec(token);
|
|
1621
1638
|
if (tag?.[1] === "/")
|
|
1622
1639
|
open.pop();
|
|
1623
|
-
else if (tag !== null && tag[3] !== "/" && !(/^(?:br|hr|img|wbr)$/).test(tag[2]))
|
|
1640
|
+
else if (tag !== null && tag[3] !== "/" && !(/^(?:br|hr|img|input|wbr)$/).test(tag[2]))
|
|
1624
1641
|
open.push(tag[2]);
|
|
1625
1642
|
}
|
|
1626
1643
|
else if (count + token.length > limit)
|
|
@@ -1656,8 +1673,9 @@ const renderTable = (children, maxColumns) => {
|
|
|
1656
1673
|
twice the share of a regular column, compressing the others */
|
|
1657
1674
|
width: Math.round(200 / (keys.length + 3)),
|
|
1658
1675
|
rows: children.map((child, i) => scoped(child, () => {
|
|
1659
|
-
const
|
|
1660
|
-
|
|
1676
|
+
const values = keys.map((key) => inlineValue(child.kind, child.properties.find((property) => property.key === key)));
|
|
1677
|
+
const shares = desc ? [...keys.map(() => 1), 2] : keys.map(() => 1);
|
|
1678
|
+
const cells = omitLong(desc ? [...values, safe(renderCell(child))] : values, shares.map((share) => share / (keys.length + (desc ? 3 : 1))), formatOf(children[0])?.maxCellHeight);
|
|
1661
1679
|
return {
|
|
1662
1680
|
id: anchorOf(child),
|
|
1663
1681
|
anchor: child.anchor,
|
|
@@ -1750,15 +1768,15 @@ const renderTitlePage = (object, created, modified) => {
|
|
|
1750
1768
|
const prop = (name) => object.properties.find((property) => property.key === name)?.value;
|
|
1751
1769
|
const inlineProp = (name) => {
|
|
1752
1770
|
const value = prop(name);
|
|
1753
|
-
return value !== undefined ? inline(value) : "";
|
|
1771
|
+
return value !== undefined && value.trim() !== "" ? inline(value) : "";
|
|
1754
1772
|
};
|
|
1755
1773
|
const rest = object.properties.filter((property) => !["LOGO", "TITLE", "SUBTITLE", "AUTHOR", "VERSION",
|
|
1756
1774
|
"LANG", "CHARSET", "PAPER-SIZE", "THEME-STYLE", "THEME-TONE"].includes(property.key));
|
|
1757
1775
|
/* the logo is rendered above the title, from the embedded image content
|
|
1758
1776
|
of the LOGO property or, for a non-embeddable reference, as its inline
|
|
1759
|
-
Markdown; without a LOGO property the built-in SpecBook
|
|
1760
|
-
in both its theme variants */
|
|
1761
|
-
const logo = object.properties.find((property) => property.key === "LOGO");
|
|
1777
|
+
Markdown; without a (non-empty) LOGO property the built-in SpecBook
|
|
1778
|
+
logo is used, in both its theme variants */
|
|
1779
|
+
const logo = object.properties.find((property) => property.key === "LOGO" && property.value.trim() !== "");
|
|
1762
1780
|
const image = logo !== undefined ? renderEmbeddings(logo.value, logo.embedding ?? []) : [];
|
|
1763
1781
|
return scoped(object, () => render("TitlePage", { TitlePage: {
|
|
1764
1782
|
logo: logo === undefined ?
|
|
@@ -1920,24 +1938,13 @@ export const renderPlaceholder = (message) => render("Placeholder", { Placeholde
|
|
|
1920
1938
|
realtime: safe(realtimeScript),
|
|
1921
1939
|
message
|
|
1922
1940
|
} });
|
|
1923
|
-
/*
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
export const renderHtml = async (specification, config, tocPages, css, options = {}, verbose, print = false) => {
|
|
1931
|
-
/* pre-render the configured diagrams as scaled embeddable SVGs
|
|
1932
|
-
(except for the omitted ones, which hence leave no trace at all) */
|
|
1933
|
-
const omit = options.omit ?? new Set();
|
|
1934
|
-
const rendered = config !== undefined ?
|
|
1935
|
-
await scaledDiagrams(specification, config, verbose, omit) : null;
|
|
1936
|
-
/* the fold controls of the omitted aspects leave along with the
|
|
1937
|
-
implied ones (no type control without diagrams of that type, no
|
|
1938
|
-
level control without diagrams from that level on, no fold tab
|
|
1939
|
-
without any control, which lets the tabs below it move up), and
|
|
1940
|
-
the omitted long texts bring the style of their "[...]" mark */
|
|
1941
|
+
/* the fold controls the omitted aspects drop, along with the implied
|
|
1942
|
+
ones (no type control without diagrams of that type, no level
|
|
1943
|
+
control without diagrams from that level on, no fold tab without
|
|
1944
|
+
any control, which lets the tabs below it move up), plus the extra
|
|
1945
|
+
style rules the omission brings (the "[...]" mark of the omitted
|
|
1946
|
+
long texts and the moved-up tabs of a dropped fold tab) */
|
|
1947
|
+
const omittedControls = (omit) => {
|
|
1941
1948
|
const none = omit.has("diagram:1")
|
|
1942
1949
|
|| (omit.has("diagram:graph") && omit.has("diagram:hub") && omit.has("diagram:grid"));
|
|
1943
1950
|
const omitted = {
|
|
@@ -1950,9 +1957,43 @@ export const renderHtml = async (specification, config, tocPages, css, options =
|
|
|
1950
1957
|
text: omit.has("text:long"),
|
|
1951
1958
|
all: none && omit.has("text:long")
|
|
1952
1959
|
};
|
|
1953
|
-
const
|
|
1960
|
+
const css = (omitted.text ? "\nspan.omit { color: var(--theme-color-specbook-muted) }" : "") +
|
|
1954
1961
|
(omitted.all ? "\nnav.toc-panel div.toc-tab { top: 10.66rem }" +
|
|
1955
1962
|
"\ndiv.realtime-status { top: 13.89rem }" : "");
|
|
1963
|
+
return { omitted, css };
|
|
1964
|
+
};
|
|
1965
|
+
/* wrap the pre-rendered diagrams into their blocks, carrying the
|
|
1966
|
+
diagram type and the object tree nesting level the folding
|
|
1967
|
+
distinguishes (the top-level objects of an artifact being level 1) */
|
|
1968
|
+
const wrapDiagrams = (index, svgs) => {
|
|
1969
|
+
const blocks = new Map();
|
|
1970
|
+
for (const node of index) {
|
|
1971
|
+
const svg = svgs.get(node.object);
|
|
1972
|
+
if (svg === undefined)
|
|
1973
|
+
continue;
|
|
1974
|
+
let level = 1;
|
|
1975
|
+
for (let parent = node.parent; parent !== undefined; parent = parent.parent)
|
|
1976
|
+
level++;
|
|
1977
|
+
const type = schemas?.get(node.object)?.diagram?.type ?? "graph";
|
|
1978
|
+
blocks.set(node.object, `<div class="diagram" data-type="${type}" data-level="${level}">${svg}</div>`);
|
|
1979
|
+
}
|
|
1980
|
+
return blocks;
|
|
1981
|
+
};
|
|
1982
|
+
/* render the entire specification into a self-contained HTML document,
|
|
1983
|
+
with the build-time pre-assembled stylesheet embedded inline, the
|
|
1984
|
+
artifact timestamps aggregated into min(Created)/max(Modified),
|
|
1985
|
+
optional per-anchor page numbers attached to the ToC entries,
|
|
1986
|
+
optionally the client-side script of the live preview injected and
|
|
1987
|
+
aspects omitted (see "ExportOptions"), and the embedded images
|
|
1988
|
+
optimized for the screen or for print (the PDF) */
|
|
1989
|
+
export const renderHtml = async (specification, config, tocPages, css, options = {}, verbose, print = false) => {
|
|
1990
|
+
/* pre-render the configured diagrams as scaled embeddable SVGs
|
|
1991
|
+
(except for the omitted ones, which hence leave no trace at all) */
|
|
1992
|
+
const omit = options.omit ?? new Set();
|
|
1993
|
+
const rendered = config !== undefined ?
|
|
1994
|
+
await scaledDiagrams(specification, config, verbose, omit) : null;
|
|
1995
|
+
/* the fold controls and the extra style rules of the omitted aspects */
|
|
1996
|
+
const { omitted, css: omitCss } = omittedControls(omit);
|
|
1956
1997
|
/* pre-optimize the embedded images (downscaled and re-encoded) */
|
|
1957
1998
|
const optimized = await optimizeImages(specification, print, verbose);
|
|
1958
1999
|
/* the document language selects the smart typography quote style */
|
|
@@ -1970,17 +2011,7 @@ export const renderHtml = async (specification, config, tocPages, css, options =
|
|
|
1970
2011
|
omits = omit;
|
|
1971
2012
|
/* wrap the diagrams into their blocks, carrying the diagram type
|
|
1972
2013
|
and the object tree nesting level the folding distinguishes */
|
|
1973
|
-
diagrams = rendered !== null ?
|
|
1974
|
-
for (const node of index) {
|
|
1975
|
-
const svg = rendered?.svgs.get(node.object);
|
|
1976
|
-
if (svg === undefined)
|
|
1977
|
-
continue;
|
|
1978
|
-
let level = 1;
|
|
1979
|
-
for (let parent = node.parent; parent !== undefined; parent = parent.parent)
|
|
1980
|
-
level++;
|
|
1981
|
-
const type = schemas?.get(node.object)?.diagram?.type ?? "graph";
|
|
1982
|
-
diagrams?.set(node.object, `<div class="diagram" data-type="${type}" data-level="${level}">${svg}</div>`);
|
|
1983
|
-
}
|
|
2014
|
+
diagrams = rendered !== null ? wrapDiagrams(index, rendered.svgs) : null;
|
|
1984
2015
|
/* collect the schema descriptions for the description popups,
|
|
1985
2016
|
plus the object table composing their title paths */
|
|
1986
2017
|
let info = null;
|
|
@@ -2020,7 +2051,7 @@ export const renderHtml = async (specification, config, tocPages, css, options =
|
|
|
2020
2051
|
return render("Document", { Document: {
|
|
2021
2052
|
title: documentTitle(specification).title,
|
|
2022
2053
|
lang,
|
|
2023
|
-
theme: documentThemeStyle(specification)
|
|
2054
|
+
theme: documentThemeStyle(specification),
|
|
2024
2055
|
css: safe((css ?? stylesheet()) + (rendered !== null ? `\n${rendered.css}` : "") + omitCss),
|
|
2025
2056
|
themescript: safe(themeScript),
|
|
2026
2057
|
titlepage: title !== undefined ?
|
|
@@ -83,11 +83,15 @@ const collect = (object, contents) => {
|
|
|
83
83
|
for) or which does not shrink the image keeps the original, so the
|
|
84
84
|
export never depends on it */
|
|
85
85
|
export const optimizeImages = async (specification, print, verbose) => {
|
|
86
|
+
/* collect the distinct embedded image contents of the specification */
|
|
86
87
|
const medium = print ? "print" : "screen";
|
|
87
88
|
const contents = new Set();
|
|
88
89
|
for (const artifact of specification.artifacts)
|
|
89
90
|
for (const object of artifact.objects)
|
|
90
91
|
collect(object, contents);
|
|
92
|
+
/* optimize every image the cache of the medium cannot serve (an
|
|
93
|
+
optimization which fails or does not shrink keeps the original),
|
|
94
|
+
accounting the sizes for the report below */
|
|
91
95
|
const cache = new Map();
|
|
92
96
|
let cached = 0;
|
|
93
97
|
let before = 0;
|
|
@@ -101,7 +105,9 @@ export const optimizeImages = async (specification, print, verbose) => {
|
|
|
101
105
|
optimized = content.startsWith("data:") ?
|
|
102
106
|
await optimizeRaster(content, print) : await optimizeSvg(content);
|
|
103
107
|
}
|
|
104
|
-
catch {
|
|
108
|
+
catch (err) {
|
|
109
|
+
verbose?.("optimizing image failed (keeping the original): " +
|
|
110
|
+
(err instanceof Error ? err.message : String(err)));
|
|
105
111
|
optimized = content;
|
|
106
112
|
}
|
|
107
113
|
if (optimized.length >= content.length)
|
|
@@ -111,6 +117,8 @@ export const optimizeImages = async (specification, print, verbose) => {
|
|
|
111
117
|
before += content.length;
|
|
112
118
|
after += optimized.length;
|
|
113
119
|
}
|
|
120
|
+
/* sweep the cache of the medium to the images of this
|
|
121
|
+
optimization and report its figures */
|
|
114
122
|
imageCaches[medium] = cache;
|
|
115
123
|
if (contents.size > 0)
|
|
116
124
|
verbose?.((cached === contents.size ?
|