@rse/specbook 1.2.3 → 1.2.5
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 +124 -204
- package/dst/specbook-cmd-export.js +4 -1
- package/dst/specbook-config.js +6 -6
- package/dst/specbook-diagram.js +154 -22
- package/dst/specbook-export-ast.js +1 -1
- package/dst/specbook-export-common.js +1 -1
- package/dst/specbook-export-html.css +178 -5
- package/dst/specbook-export-html.d.ts +1 -1
- package/dst/specbook-export-html.js +304 -42
- package/dst/specbook-export-md.js +9 -9
- package/dst/specbook-export-pdf.js +37 -13
- package/dst/specbook-format-schema.d.ts +11 -1
- package/dst/specbook-format-schema.js +12 -2
- package/dst/specbook-format-spec.d.ts +1 -1
- package/dst/specbook-format-spec.js +2 -2
- package/dst/specbook-format.md +80 -9
- package/dst/specbook-format.yaml +294 -197
- package/dst/specbook-link.js +2 -2
- package/dst/specbook-parse-semantic.js +22 -22
- package/dst/specbook-parse-syntax.js +5 -5
- package/dst/specbook-parse.js +1 -1
- package/package.json +6 -6
|
@@ -43,6 +43,13 @@ const templates = {
|
|
|
43
43
|
<span class="search-clear" id="search-clear" title="clear search">×</span>
|
|
44
44
|
</div>
|
|
45
45
|
</div>
|
|
46
|
+
<div class="fold-switch">
|
|
47
|
+
<div class="fold-toggle" title="toggle folding controls"><svg class="fold-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3.5l5 5 5-5"/><path d="M7 20.5l5-5 5 5"/></svg></div>
|
|
48
|
+
<div class="fold-controls">
|
|
49
|
+
<div class="fold-diagrams" title="fold/unfold all diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="5.5" cy="5.5" r="3"/><circle cx="18.5" cy="5.5" r="3"/><circle cx="12" cy="18.5" r="3"/><path d="M7.3 8.1 10.6 15.9"/><path d="M16.7 8.1 13.4 15.9"/></svg></div>
|
|
50
|
+
<div class="fold-texts" title="fold/unfold all cell texts"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 7V4.5h15V7"/><path d="M12 4.5v15"/><path d="M8.5 19.5h7"/></svg></div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
46
53
|
<div class="scroll-progress" title="scroll to top"><svg class="scroll-ring" viewBox="0 0 44 44" fill="none" stroke-width="2.5"><circle class="scroll-todo" cx="22" cy="22" r="20"/><circle class="scroll-done" cx="22" cy="22" r="20" stroke-linecap="round" pathLength="1" stroke-dasharray="1" stroke-dashoffset="1"/></svg><svg class="scroll-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="M5 12l7-7 7 7"/></svg></div>
|
|
47
54
|
{{ Document.tocpanel }}
|
|
48
55
|
{{ Document.titlepage }}
|
|
@@ -51,6 +58,7 @@ const templates = {
|
|
|
51
58
|
{{ Document.artifacts }}
|
|
52
59
|
{% if Document.search %}<script>{{ Document.search }}</script>{% endif %}
|
|
53
60
|
<script>{{ Document.progress }}</script>
|
|
61
|
+
<script>{{ Document.fold }}</script>
|
|
54
62
|
{% if Document.tocpanel %}<script>{{ Document.tocscript }}</script>{% endif %}
|
|
55
63
|
{% if Document.info %}<script>{{ Document.info }}</script>{% endif %}
|
|
56
64
|
{% if Document.realtime %}<script class="realtime">{{ Document.realtime }}</script>{% endif %}
|
|
@@ -122,7 +130,7 @@ const templates = {
|
|
|
122
130
|
"TocPanelEntries": textframe `
|
|
123
131
|
<ul>
|
|
124
132
|
{% for entry in Entries %}
|
|
125
|
-
<li><a href="#{{ entry.id }}"><span class="entry"><span class="object-kind"{% if entry.info %} data-info="{{ entry.info }}" data-info-path="{{ entry.infopath }}"{% endif %}>{{ entry.kind }}:</span> <span class="object-name"{% if entry.spec %} data-info-spec="{{ entry.spec }}" data-info-path="{{ entry.specpath }}"{% endif %}>{{ entry.name }}</span></span></a>{{ entry.
|
|
133
|
+
<li><a href="#{{ entry.id }}"><span class="entry"><span class="object-kind"{% if entry.info %} data-info="{{ entry.info }}" data-info-path="{{ entry.infopath }}"{% endif %}>{{ entry.kind }}:</span> <span class="object-name"{% if entry.spec %} data-info-spec="{{ entry.spec }}" data-info-path="{{ entry.specpath }}"{% endif %}>{{ entry.name }}</span></span></a>{{ entry.children }}</li>
|
|
126
134
|
{% endfor %}
|
|
127
135
|
</ul>
|
|
128
136
|
`,
|
|
@@ -146,12 +154,12 @@ const templates = {
|
|
|
146
154
|
{{ Object.diagram }}
|
|
147
155
|
{{ Object.properties }}
|
|
148
156
|
{{ Object.description }}
|
|
149
|
-
{{ Object.
|
|
157
|
+
{{ Object.children }}
|
|
150
158
|
</section>
|
|
151
159
|
`,
|
|
152
160
|
/* <Properties/> */
|
|
153
161
|
"Properties": textframe `
|
|
154
|
-
<table class="props">
|
|
162
|
+
<table class="props"{% if Fold %} data-fold-height="{{ Fold }}"{% endif %}>
|
|
155
163
|
{% for property in Properties %}
|
|
156
164
|
<tr><td class="key property-name"><span{% if property.info %} data-info="{{ property.info }}" data-info-path="{{ property.infopath }}" data-info-prop="{{ property.key }}"{% endif %}>{{ property.key }}</span></td><td>{{ property.value }}</td></tr>
|
|
157
165
|
{% endfor %}
|
|
@@ -172,7 +180,7 @@ const templates = {
|
|
|
172
180
|
`,
|
|
173
181
|
/* <Table/> */
|
|
174
182
|
"Table": textframe `
|
|
175
|
-
<table class="objects">
|
|
183
|
+
<table class="objects"{% if Table.fold %} data-fold-height="{{ Table.fold }}"{% endif %}>
|
|
176
184
|
<thead>
|
|
177
185
|
<tr>
|
|
178
186
|
<th class="object-kind"><span{% if Table.info %} data-info="{{ Table.info }}" data-info-path="{{ Table.infopath }}"{% endif %}>{{ Table.head }}</span></th>
|
|
@@ -193,7 +201,7 @@ const templates = {
|
|
|
193
201
|
`,
|
|
194
202
|
/* <TableChunked/> */
|
|
195
203
|
"TableChunked": textframe `
|
|
196
|
-
<table class="objects">
|
|
204
|
+
<table class="objects"{% if Table.fold %} data-fold-height="{{ Table.fold }}"{% endif %}>
|
|
197
205
|
<thead>
|
|
198
206
|
<tr>
|
|
199
207
|
<th class="object-kind" style="width: {{ Table.width }}%"><span{% if Table.info %} data-info="{{ Table.info }}" data-info-path="{{ Table.infopath }}"{% endif %}>{{ Table.head }}</span></th>
|
|
@@ -382,6 +390,255 @@ const scrollProgressScript = textframe `
|
|
|
382
390
|
update()
|
|
383
391
|
})()
|
|
384
392
|
`;
|
|
393
|
+
/* the client-side script of the folding: it wraps every diagram of the
|
|
394
|
+
content into a fold container carrying a chevron mark at its top
|
|
395
|
+
left corner, folds the
|
|
396
|
+
text of every table cell towering over the other cells of its row
|
|
397
|
+
behind a chevron mark of its own, and lets the two
|
|
398
|
+
controls of the fold tab fold and unfold all diagrams and all cell
|
|
399
|
+
texts at once, with their state persisted across page loads and
|
|
400
|
+
their icons (plus the tab icon) marked while anything of their kind
|
|
401
|
+
is folded. The script
|
|
402
|
+
runs at the end of the body, as the content it wraps has to exist
|
|
403
|
+
already, and a live preview body swap replaces the containers and
|
|
404
|
+
their listeners along with the body */
|
|
405
|
+
const foldScript = textframe `
|
|
406
|
+
(function () {
|
|
407
|
+
const tab = document.querySelector("div.fold-switch")
|
|
408
|
+
if (tab === null)
|
|
409
|
+
return
|
|
410
|
+
const controls = { diagram: tab.querySelector("div.fold-diagrams"),
|
|
411
|
+
text: tab.querySelector("div.fold-texts") }
|
|
412
|
+
|
|
413
|
+
/* let the fold icon slide the two controls out of the tab (and
|
|
414
|
+
back in again), remembering the choice across page loads */
|
|
415
|
+
try { if (localStorage.getItem("specbook-fold") === "open") tab.classList.add("open") }
|
|
416
|
+
catch { /* an inaccessible storage just means no stored state */ }
|
|
417
|
+
tab.querySelector("div.fold-toggle").addEventListener("click", () => {
|
|
418
|
+
tab.classList.toggle("open")
|
|
419
|
+
try { localStorage.setItem("specbook-fold", tab.classList.contains("open") ? "open" : "closed") }
|
|
420
|
+
catch { /* an inaccessible storage just loses the state */ }
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
/* the default percentage by which the text of a table cell may
|
|
424
|
+
exceed the height of every other cell of its row before it
|
|
425
|
+
folds, overridden per object kind by the "data-fold-height"
|
|
426
|
+
attribute the schema configuration puts onto its table */
|
|
427
|
+
const cellHeight = 40
|
|
428
|
+
|
|
429
|
+
/* the chevron mark of a foldable element, rotated by the
|
|
430
|
+
stylesheet while the element is folded */
|
|
431
|
+
const svgNS = "http://www.w3.org/2000/svg"
|
|
432
|
+
const mark = (name, title) => {
|
|
433
|
+
const span = document.createElement("span")
|
|
434
|
+
span.className = name
|
|
435
|
+
span.title = title
|
|
436
|
+
const svg = document.createElementNS(svgNS, "svg")
|
|
437
|
+
svg.setAttribute("viewBox", "0 0 24 24")
|
|
438
|
+
svg.setAttribute("fill", "none")
|
|
439
|
+
svg.setAttribute("stroke", "currentColor")
|
|
440
|
+
svg.setAttribute("stroke-width", "2.5")
|
|
441
|
+
svg.setAttribute("stroke-linecap", "round")
|
|
442
|
+
svg.setAttribute("stroke-linejoin", "round")
|
|
443
|
+
const path = document.createElementNS(svgNS, "path")
|
|
444
|
+
path.setAttribute("d", "M5 9l7 7 7-7")
|
|
445
|
+
svg.appendChild(path)
|
|
446
|
+
span.appendChild(svg)
|
|
447
|
+
return span
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/* wrap every diagram into its fold container, carrying the
|
|
451
|
+
chevron mark plus the muted placeholder the diagram leaves
|
|
452
|
+
behind while folded, which is the very icon of its tab
|
|
453
|
+
control */
|
|
454
|
+
const folds = { diagram: [], text: [] }
|
|
455
|
+
document.querySelectorAll("article div.diagram, nav.doc div.diagram").forEach((el) => {
|
|
456
|
+
const fold = document.createElement("div")
|
|
457
|
+
fold.className = "fold"
|
|
458
|
+
el.parentNode.insertBefore(fold, el)
|
|
459
|
+
const chevron = mark("fold-mark", "fold/unfold this diagram")
|
|
460
|
+
const label = document.createElement("span")
|
|
461
|
+
label.className = "fold-label"
|
|
462
|
+
label.appendChild(controls.diagram.querySelector("svg").cloneNode(true))
|
|
463
|
+
fold.append(chevron, label, el)
|
|
464
|
+
chevron.addEventListener("click", () => {
|
|
465
|
+
fold.classList.toggle("folded")
|
|
466
|
+
sync()
|
|
467
|
+
})
|
|
468
|
+
folds.diagram.push(fold)
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
/* the rendered height of the content of a table cell, or of its
|
|
472
|
+
leading part up to a word position: a cell box itself always
|
|
473
|
+
spans the whole row, so only a range over the content
|
|
474
|
+
measures the text */
|
|
475
|
+
const range = document.createRange()
|
|
476
|
+
const height = (cell, upto) => {
|
|
477
|
+
if (upto === undefined)
|
|
478
|
+
range.selectNodeContents(cell)
|
|
479
|
+
else {
|
|
480
|
+
range.setStart(cell, 0)
|
|
481
|
+
range.setEnd(upto.node, upto.offset)
|
|
482
|
+
}
|
|
483
|
+
return range.getBoundingClientRect().height
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/* the word positions of a table cell, each right behind one word */
|
|
487
|
+
const positions = (cell) => {
|
|
488
|
+
const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT)
|
|
489
|
+
const list = []
|
|
490
|
+
let node
|
|
491
|
+
while ((node = walker.nextNode()) !== null) {
|
|
492
|
+
const regex = /\\S+/g
|
|
493
|
+
let m
|
|
494
|
+
while ((m = regex.exec(node.nodeValue ?? "")) !== null)
|
|
495
|
+
list.push({ node, offset: m.index + m[0].length })
|
|
496
|
+
}
|
|
497
|
+
return list
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* a cell taking part in the comparison of its row: one with
|
|
501
|
+
content of its own, i.e. neither empty (a not given property
|
|
502
|
+
draws its marker through the stylesheet alone) nor holding
|
|
503
|
+
further cells (a chunked or nested table, whose own cells are
|
|
504
|
+
compared instead) nor a diagram (which folds on its own and
|
|
505
|
+
whose SVG text cannot carry the wrapper of a hidden
|
|
506
|
+
remainder) */
|
|
507
|
+
const candidate = (cell) =>
|
|
508
|
+
cell.querySelector("td") === null
|
|
509
|
+
&& cell.querySelector("div.diagram") === null
|
|
510
|
+
&& (cell.textContent.trim() !== "" || cell.querySelector("img") !== null)
|
|
511
|
+
|
|
512
|
+
/* decide which table cells fold: one which is taller than the
|
|
513
|
+
configured percentage above every other cell of its row (the
|
|
514
|
+
leading name column of a compact table excluded, as it is no
|
|
515
|
+
text of its own). All rows are measured before the first cut,
|
|
516
|
+
as every cut shifts the layout of the ones still to come */
|
|
517
|
+
const cuts = []
|
|
518
|
+
document.querySelectorAll("article table tr").forEach((row) => {
|
|
519
|
+
const table = row.closest("table")
|
|
520
|
+
let cells = Array.from(row.children).filter((el) => el.tagName === "TD")
|
|
521
|
+
if (!table.classList.contains("chunks"))
|
|
522
|
+
cells = cells.slice(1)
|
|
523
|
+
cells = cells.filter(candidate)
|
|
524
|
+
if (cells.length < 2)
|
|
525
|
+
return
|
|
526
|
+
const configured = row.closest("table[data-fold-height]")
|
|
527
|
+
const percent = Number(configured?.getAttribute("data-fold-height")) || cellHeight
|
|
528
|
+
const heights = cells.map((cell) => height(cell))
|
|
529
|
+
cells.forEach((cell, i) => {
|
|
530
|
+
const other = Math.max(...heights.filter((_, j) => j !== i))
|
|
531
|
+
const limit = other * (1 + percent / 100)
|
|
532
|
+
if (heights[i] > limit)
|
|
533
|
+
cuts.push({ cell, limit, full: heights[i] })
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
/* hide a node of the folded remainder of a table cell: an
|
|
538
|
+
element carries the class itself, a text node gets wrapped */
|
|
539
|
+
const rest = (node) => {
|
|
540
|
+
if (node.nodeType === Node.ELEMENT_NODE)
|
|
541
|
+
node.classList.add("fold-rest")
|
|
542
|
+
else if (node.nodeType === Node.TEXT_NODE) {
|
|
543
|
+
const span = document.createElement("span")
|
|
544
|
+
span.className = "fold-rest"
|
|
545
|
+
node.parentNode.insertBefore(span, node)
|
|
546
|
+
span.appendChild(node)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/* the minimum percentage of its own height a cell has to gain
|
|
551
|
+
by folding, as hiding less than that is no visible relief
|
|
552
|
+
and only costs the reader a chevron to click (measured after
|
|
553
|
+
the cut, as the chevron itself can claim a line of its own) */
|
|
554
|
+
const cellGain = 25
|
|
555
|
+
|
|
556
|
+
/* fold the decided cells back onto their height limit: the last
|
|
557
|
+
word position still fitting is found by a binary search over
|
|
558
|
+
the measured word positions, the text node is split there and
|
|
559
|
+
the chevron mark takes its place, followed by the remainder
|
|
560
|
+
hidden up the ancestor chain of the cell */
|
|
561
|
+
for (const cut of cuts) {
|
|
562
|
+
const list = positions(cut.cell)
|
|
563
|
+
if (list.length === 0)
|
|
564
|
+
continue
|
|
565
|
+
let lo = 0
|
|
566
|
+
let hi = list.length - 1
|
|
567
|
+
while (lo < hi) {
|
|
568
|
+
const mid = Math.ceil((lo + hi) / 2)
|
|
569
|
+
if (height(cut.cell, list[mid]) <= cut.limit)
|
|
570
|
+
lo = mid
|
|
571
|
+
else
|
|
572
|
+
hi = mid - 1
|
|
573
|
+
}
|
|
574
|
+
const tail = list[lo].node.splitText(list[lo].offset)
|
|
575
|
+
const chevron = mark("fold-more", "fold/unfold the remaining text")
|
|
576
|
+
tail.parentNode.insertBefore(chevron, tail)
|
|
577
|
+
let current = chevron
|
|
578
|
+
while (current !== cut.cell) {
|
|
579
|
+
let next = current.nextSibling
|
|
580
|
+
while (next !== null) {
|
|
581
|
+
const following = next.nextSibling
|
|
582
|
+
rest(next)
|
|
583
|
+
next = following
|
|
584
|
+
}
|
|
585
|
+
current = current.parentNode
|
|
586
|
+
}
|
|
587
|
+
/* the gain is measured on the applied fold, as only that
|
|
588
|
+
one tells the real one, and a fold gaining less than the
|
|
589
|
+
minimum share is taken back again, which the chevron
|
|
590
|
+
alone has to leave behind, as the hidden remainder stays
|
|
591
|
+
inert without the class -- which a kept fold sheds
|
|
592
|
+
again, too, as every cell starts out unfolded */
|
|
593
|
+
cut.cell.classList.add("folded")
|
|
594
|
+
const gained = height(cut.cell) <= cut.full * (1 - cellGain / 100)
|
|
595
|
+
cut.cell.classList.remove("folded")
|
|
596
|
+
if (!gained) {
|
|
597
|
+
chevron.remove()
|
|
598
|
+
continue
|
|
599
|
+
}
|
|
600
|
+
folds.text.push(cut.cell)
|
|
601
|
+
chevron.addEventListener("click", (event) => {
|
|
602
|
+
/* the cut can land inside a hyperlink, whose navigation
|
|
603
|
+
the chevron has to suppress for its own click alone,
|
|
604
|
+
so the remaining link text still follows the link */
|
|
605
|
+
event.preventDefault()
|
|
606
|
+
cut.cell.classList.toggle("folded")
|
|
607
|
+
sync()
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/* mark the control of a kind while anything of that kind is
|
|
612
|
+
folded, and the tab itself while any kind at all is */
|
|
613
|
+
const sync = () => {
|
|
614
|
+
let any = false
|
|
615
|
+
for (const kind of Object.keys(folds)) {
|
|
616
|
+
const folded = folds[kind].some((fold) => fold.classList.contains("folded"))
|
|
617
|
+
controls[kind].classList.toggle("folded", folded)
|
|
618
|
+
any ||= folded
|
|
619
|
+
}
|
|
620
|
+
tab.classList.toggle("folded", any)
|
|
621
|
+
}
|
|
622
|
+
for (const kind of Object.keys(folds)) {
|
|
623
|
+
/* without a stored state the rendered one stands, which is
|
|
624
|
+
everything unfolded */
|
|
625
|
+
let stored = null
|
|
626
|
+
try { stored = localStorage.getItem("specbook-fold-" + kind) }
|
|
627
|
+
catch { /* an inaccessible storage just means no stored state */ }
|
|
628
|
+
if (stored !== null)
|
|
629
|
+
folds[kind].forEach((fold) => { fold.classList.toggle("folded", stored === "folded") })
|
|
630
|
+
controls[kind].addEventListener("click", () => {
|
|
631
|
+
const all = !folds[kind].every((fold) => fold.classList.contains("folded"))
|
|
632
|
+
folds[kind].forEach((fold) => { fold.classList.toggle("folded", all) })
|
|
633
|
+
try { localStorage.setItem("specbook-fold-" + kind, all ? "folded" : "unfolded") }
|
|
634
|
+
catch { /* an inaccessible storage just loses the state */ }
|
|
635
|
+
sync()
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
sync()
|
|
639
|
+
|
|
640
|
+
})()
|
|
641
|
+
`;
|
|
385
642
|
/* the client-side script of the table of contents side panel: it
|
|
386
643
|
slides the panel out of and back into the viewport edge (closing it
|
|
387
644
|
also on a jump, on "Escape", and on a click outside), remembers the
|
|
@@ -470,6 +727,7 @@ const tocPanelScript = textframe `
|
|
|
470
727
|
&& event.target.closest("div.search") === null
|
|
471
728
|
&& event.target.closest("div.theme-switch") === null
|
|
472
729
|
&& event.target.closest("div.info-switch") === null
|
|
730
|
+
&& event.target.closest("div.fold-switch") === null
|
|
473
731
|
&& event.target.closest("div.realtime-status") === null)
|
|
474
732
|
toggle(false)
|
|
475
733
|
}
|
|
@@ -800,7 +1058,7 @@ const collectInfo = (nodes, prefix, keys, info) => {
|
|
|
800
1058
|
}
|
|
801
1059
|
if (entry.d !== undefined || entry.p !== undefined)
|
|
802
1060
|
info[path] = entry;
|
|
803
|
-
collectInfo(schema.
|
|
1061
|
+
collectInfo(schema.children ?? [], path, keys, info);
|
|
804
1062
|
}
|
|
805
1063
|
};
|
|
806
1064
|
/* inject the description popup maps into their client-side script ("<"
|
|
@@ -888,7 +1146,7 @@ const collectSpec = (objects, spec) => {
|
|
|
888
1146
|
.trim();
|
|
889
1147
|
if (text !== "")
|
|
890
1148
|
spec[anchorOf(object)] = scoped(object, () => block(text, true).toString().trim());
|
|
891
|
-
collectSpec(object.
|
|
1149
|
+
collectSpec(object.children, spec);
|
|
892
1150
|
}
|
|
893
1151
|
};
|
|
894
1152
|
/* the literal members of an "enum(...)"/"tags(...)" expression, or
|
|
@@ -913,7 +1171,7 @@ const collectMembers = (nodes, result) => {
|
|
|
913
1171
|
/* an invalid expression is the concern of lint */
|
|
914
1172
|
}
|
|
915
1173
|
}
|
|
916
|
-
collectMembers(schema.
|
|
1174
|
+
collectMembers(schema.children ?? [], result);
|
|
917
1175
|
}
|
|
918
1176
|
return result;
|
|
919
1177
|
};
|
|
@@ -976,20 +1234,20 @@ const effectiveProperties = (object) => {
|
|
|
976
1234
|
order (unknown keys appended in occurrence order), with
|
|
977
1235
|
"withUnusedProps" injecting the defined but still unused schema
|
|
978
1236
|
properties as additional columns */
|
|
979
|
-
const tableShape = (
|
|
980
|
-
const used = [...new Set(
|
|
981
|
-
const schema = schemas?.get(
|
|
1237
|
+
const tableShape = (children) => {
|
|
1238
|
+
const used = [...new Set(children.flatMap((child) => child.properties.map((property) => property.key)))];
|
|
1239
|
+
const schema = schemas?.get(children[0]);
|
|
982
1240
|
const names = (schema?.props ?? []).map((prop) => prop.name);
|
|
983
1241
|
const known = schema?.format?.withUnusedProps === true ?
|
|
984
1242
|
names : names.filter((name) => used.includes(name));
|
|
985
1243
|
const keys = [...known, ...used.filter((key) => !names.includes(key))];
|
|
986
|
-
return { keys, desc:
|
|
1244
|
+
return { keys, desc: children.some((child) => child.description !== undefined || child.children.length > 0) };
|
|
987
1245
|
};
|
|
988
|
-
/* provide the
|
|
1246
|
+
/* provide the children of an object taking part in the regular document
|
|
989
1247
|
flow: a nested title object leaves the flow, as it is rendered as the
|
|
990
1248
|
title page instead (a top-level one instead removes its whole artifact) */
|
|
991
|
-
const
|
|
992
|
-
/* decide whether a single-kind group of
|
|
1249
|
+
const flowChildren = (object) => object.children.filter((child) => !isTitleObject(child));
|
|
1250
|
+
/* decide whether a single-kind group of children collapses into the
|
|
993
1251
|
concise (tabular) rendering: an explicit "format" type of the kind
|
|
994
1252
|
wins, "auto" collapses the deepest level only, and inside an already
|
|
995
1253
|
concise rendering context "auto" groups implicitly stay concise, too */
|
|
@@ -997,12 +1255,12 @@ const conciseGroup = (group, schemaMap, concise) => {
|
|
|
997
1255
|
const type = schemaMap?.get(group[0])?.format?.type ?? "auto";
|
|
998
1256
|
if (type !== "auto")
|
|
999
1257
|
return type === "concise";
|
|
1000
|
-
return concise || group.every((child) => child.
|
|
1258
|
+
return concise || group.every((child) => child.children.length === 0);
|
|
1001
1259
|
};
|
|
1002
|
-
/* group the
|
|
1003
|
-
const
|
|
1260
|
+
/* group the children of an object by their kind, preserving order */
|
|
1261
|
+
const groupChildren = (children) => {
|
|
1004
1262
|
const groups = new Map();
|
|
1005
|
-
for (const child of
|
|
1263
|
+
for (const child of children) {
|
|
1006
1264
|
const group = groups.get(child.kind);
|
|
1007
1265
|
if (group === undefined)
|
|
1008
1266
|
groups.set(child.kind, [child]);
|
|
@@ -1011,21 +1269,21 @@ const groupChilds = (childs) => {
|
|
|
1011
1269
|
}
|
|
1012
1270
|
return [...groups.values()];
|
|
1013
1271
|
};
|
|
1014
|
-
/* render the
|
|
1272
|
+
/* render the children of an object group-wise by kind: a concise group
|
|
1015
1273
|
as one compact table, a complex group as regular nested object
|
|
1016
1274
|
renderings */
|
|
1017
|
-
const
|
|
1275
|
+
const renderChildren = (object, level, concise) => groupChildren(flowChildren(object)).map((group) => conciseGroup(group, schemas, concise) ?
|
|
1018
1276
|
renderTable(group, maxColumnsOf(group[0])) :
|
|
1019
1277
|
group.map((child) => renderObject(child, level, concise)).join("")).join("");
|
|
1020
1278
|
/* render the description cell of a table row: the description of the
|
|
1021
|
-
object followed by its recursively rendered
|
|
1279
|
+
object followed by its recursively rendered children (implicitly or
|
|
1022
1280
|
explicitly concise groups as nested sub-tables, explicitly complex
|
|
1023
1281
|
ones as regular nested object renderings pressed into the cell);
|
|
1024
1282
|
a cell left without any content renders as the absent marker,
|
|
1025
1283
|
exactly like a property cell of a not given property */
|
|
1026
1284
|
const renderCell = (child) => {
|
|
1027
1285
|
let html = child.description !== undefined ? renderDescription(child.description) : "";
|
|
1028
|
-
html +=
|
|
1286
|
+
html += renderChildren(child, 6, true);
|
|
1029
1287
|
return html.trim() !== "" ? html : "<span class=\"value-absent\"></span>";
|
|
1030
1288
|
};
|
|
1031
1289
|
/* the pre-rendered diagram of an object as an embeddable block
|
|
@@ -1034,27 +1292,28 @@ const diagramOf = (object) => {
|
|
|
1034
1292
|
const diagram = diagrams?.get(object);
|
|
1035
1293
|
return diagram !== undefined ? safe(`<div class="diagram">${diagram}</div>`) : "";
|
|
1036
1294
|
};
|
|
1037
|
-
/* render a single-kind group of
|
|
1295
|
+
/* render a single-kind group of children into one compact table:
|
|
1038
1296
|
the name first, then the property columns, then the description;
|
|
1039
1297
|
a group wider than maxColumns, or carrying diagrams, instead chunks
|
|
1040
1298
|
the property and description cells (and the leading diagram) of
|
|
1041
1299
|
every object into an embedded per-object table, as a diagram is
|
|
1042
1300
|
identifiable only under a header of its own, not under the column
|
|
1043
1301
|
headers of a plain table */
|
|
1044
|
-
const renderTable = (
|
|
1045
|
-
const { keys, desc } = tableShape(
|
|
1046
|
-
const diagrammed =
|
|
1302
|
+
const renderTable = (children, maxColumns) => {
|
|
1303
|
+
const { keys, desc } = tableShape(children);
|
|
1304
|
+
const diagrammed = children.some((child) => diagramOf(child) !== "");
|
|
1047
1305
|
if (!diagrammed && 1 + keys.length + (desc ? 1 : 0) <= maxColumns)
|
|
1048
1306
|
return render("Table", { Table: {
|
|
1049
|
-
head:
|
|
1050
|
-
info: infoKeyOf(
|
|
1051
|
-
infopath: infoPathOf(
|
|
1307
|
+
head: children[0].kind !== "" ? children[0].kind : "Name",
|
|
1308
|
+
info: infoKeyOf(children[0]),
|
|
1309
|
+
infopath: infoPathOf(children[0], false),
|
|
1052
1310
|
keys,
|
|
1053
1311
|
desc,
|
|
1312
|
+
fold: formatOf(children[0])?.maxCellHeight,
|
|
1054
1313
|
/* under the fixed table layout the description column claims
|
|
1055
1314
|
twice the share of a regular column, compressing the others */
|
|
1056
1315
|
width: Math.round(200 / (keys.length + 3)),
|
|
1057
|
-
rows:
|
|
1316
|
+
rows: children.map((child, i) => scoped(child, () => ({
|
|
1058
1317
|
id: anchorOf(child),
|
|
1059
1318
|
paren: child.paren,
|
|
1060
1319
|
primary: child.primary,
|
|
@@ -1071,11 +1330,12 @@ const renderTable = (childs, maxColumns) => {
|
|
|
1071
1330
|
with the last cell spanning the leftover columns of the final row */
|
|
1072
1331
|
const size = Math.max(1, maxColumns - 1);
|
|
1073
1332
|
return render("TableChunked", { Table: {
|
|
1074
|
-
head:
|
|
1075
|
-
info: infoKeyOf(
|
|
1076
|
-
infopath: infoPathOf(
|
|
1333
|
+
head: children[0].kind !== "" ? children[0].kind : "Name",
|
|
1334
|
+
info: infoKeyOf(children[0]),
|
|
1335
|
+
infopath: infoPathOf(children[0], false),
|
|
1336
|
+
fold: formatOf(children[0])?.maxCellHeight,
|
|
1077
1337
|
width: Math.round(100 / maxColumns),
|
|
1078
|
-
rows:
|
|
1338
|
+
rows: children.map((child, i) => scoped(child, () => {
|
|
1079
1339
|
const cells = keys.map((key) => ({ key, desc: false, span: 1,
|
|
1080
1340
|
value: inlineValue(child.kind, child.properties.find((property) => property.key === key)) }));
|
|
1081
1341
|
if (desc)
|
|
@@ -1122,10 +1382,11 @@ const renderObject = (object, level, concise) => {
|
|
|
1122
1382
|
name: inline(object.name),
|
|
1123
1383
|
diagram: diagramOf(object),
|
|
1124
1384
|
properties: properties.length > 0 ?
|
|
1125
|
-
safe(render("Properties", { Properties: inlineProperties(object, properties)
|
|
1385
|
+
safe(render("Properties", { Properties: inlineProperties(object, properties),
|
|
1386
|
+
Fold: formatOf(object)?.maxCellHeight })) : "",
|
|
1126
1387
|
description: object.description !== undefined ?
|
|
1127
1388
|
safe(renderDescription(object.description)) : "",
|
|
1128
|
-
|
|
1389
|
+
children: safe(renderChildren(object, level + 1, concise))
|
|
1129
1390
|
} }));
|
|
1130
1391
|
};
|
|
1131
1392
|
/* format a timestamp as its calendar date in local time (the
|
|
@@ -1167,10 +1428,10 @@ const renderTitlePage = (object, created, modified) => {
|
|
|
1167
1428
|
const renderArtifact = (artifact) => render("Artifact", { Artifact: {
|
|
1168
1429
|
objects: safe(artifact.objects.map((object) => renderObject(object, 1, false)).join(""))
|
|
1169
1430
|
} });
|
|
1170
|
-
/* provide the
|
|
1431
|
+
/* provide the children of an object rendered with headings of their own
|
|
1171
1432
|
(skipping the child groups collapsing into compact tables), which
|
|
1172
1433
|
are the ones taking part in the table of contents and its side panel */
|
|
1173
|
-
const
|
|
1434
|
+
const headingChildren = (object) => groupChildren(flowChildren(object))
|
|
1174
1435
|
.filter((group) => !conciseGroup(group, schemas, false))
|
|
1175
1436
|
.flat();
|
|
1176
1437
|
/* flatten the hierarchy of the rendered object headings (exactly like
|
|
@@ -1185,7 +1446,7 @@ const tocEntries = (objects, pages) => {
|
|
|
1185
1446
|
info: infoKeyOf(object), infopath: infoPathOf(object, false),
|
|
1186
1447
|
spec: infoParents !== null ? id : undefined, specpath: infoPathOf(object),
|
|
1187
1448
|
name: inline(object.name), level: Math.min(level, 6), page: pages?.get(id) });
|
|
1188
|
-
collect(
|
|
1449
|
+
collect(headingChildren(object), level + 1);
|
|
1189
1450
|
}
|
|
1190
1451
|
};
|
|
1191
1452
|
collect(objects, 1);
|
|
@@ -1205,7 +1466,7 @@ const renderTocPanel = (objects, title, doc) => {
|
|
|
1205
1466
|
spec: infoParents !== null ? anchorOf(object) : undefined,
|
|
1206
1467
|
specpath: infoPathOf(object),
|
|
1207
1468
|
name: inline(object.name),
|
|
1208
|
-
|
|
1469
|
+
children: safe(entries(headingChildren(object)))
|
|
1209
1470
|
})) });
|
|
1210
1471
|
return render("TocPanel", { TocPanel: { title, doc, entries: safe(entries(objects)) } });
|
|
1211
1472
|
};
|
|
@@ -1217,7 +1478,7 @@ export const htmlOutline = (specification, config) => {
|
|
|
1217
1478
|
const entry = (object) => ({
|
|
1218
1479
|
title: (object.kind !== "" ? `${object.kind}: ` : "") + plainText(object.name),
|
|
1219
1480
|
anchor: paths.get(object) ?? object.id,
|
|
1220
|
-
|
|
1481
|
+
children: groupChildren(flowChildren(object))
|
|
1221
1482
|
.filter((group) => !conciseGroup(group, schemaMap, false))
|
|
1222
1483
|
.flatMap((group) => group.map(entry))
|
|
1223
1484
|
});
|
|
@@ -1357,6 +1618,7 @@ export const renderHtml = async (specification, config, tocPages, css, realtime
|
|
|
1357
1618
|
safe(renderTitlePage(title, formatDate(created), formatDate(modified))) : "",
|
|
1358
1619
|
search: safe(searchScript()),
|
|
1359
1620
|
progress: safe(scrollProgressScript),
|
|
1621
|
+
fold: safe(foldScript),
|
|
1360
1622
|
info: info !== null ? safe(infoScript(info, spec ?? {})) : "",
|
|
1361
1623
|
realtime: realtime ? safe(realtimeScript) : "",
|
|
1362
1624
|
toc: entries.length > 0 ? safe(render("Toc", { Toc: { entries } })) : "",
|
|
@@ -39,11 +39,11 @@ const nameSuffixMd = (object) => (object.primary === true ? " (*)" : "") +
|
|
|
39
39
|
code block, an ordered list, a blockquote, or multiple paragraphs) or
|
|
40
40
|
one carrying a ";", as only a ";"-free description stays a single
|
|
41
41
|
segment on re-parsing instead of splitting off a spurious property */
|
|
42
|
-
const complexBelowMd = (object) => object.
|
|
42
|
+
const complexBelowMd = (object) => object.children.some((child) => child.name.includes(";")
|
|
43
43
|
|| (child.description !== undefined
|
|
44
44
|
&& /[\n;]/.test(renderDescriptionMd(child.description))) || complexBelowMd(child));
|
|
45
45
|
/* render an object in the Concise Format as a list item of the
|
|
46
|
-
given nesting depth, with its
|
|
46
|
+
given nesting depth, with its children as nested list items */
|
|
47
47
|
const renderConciseMd = (object, depth = 0) => {
|
|
48
48
|
const indent = " ".repeat(depth * 4);
|
|
49
49
|
const segments = [`${object.kind}: ${object.name}${nameSuffixMd(object)}`];
|
|
@@ -62,14 +62,14 @@ const renderConciseMd = (object, depth = 0) => {
|
|
|
62
62
|
`${indent}- ${segments.join("; ").replace(/\.?$/, ".")}` :
|
|
63
63
|
`${indent}- ${segments.join("; ")};`;
|
|
64
64
|
return [item, renderKeyValuesMd(wrapped, `${indent} `),
|
|
65
|
-
...object.
|
|
65
|
+
...object.children.map((child) => renderConciseMd(child, depth + 1))]
|
|
66
66
|
.filter((part) => part !== "").join("\n");
|
|
67
67
|
};
|
|
68
|
-
/* render an object in the Complex Format (levels 1-3), with
|
|
68
|
+
/* render an object in the Complex Format (levels 1-3), with children from
|
|
69
69
|
level 4 upwards in the Concise Format -- unless a descendant carries
|
|
70
|
-
a description no concise item can carry, so all
|
|
70
|
+
a description no concise item can carry, so all children (as siblings
|
|
71
71
|
have to share the format) stay in the Complex Format down to heading
|
|
72
|
-
level 6, the deepest one Markdown knows, below which the
|
|
72
|
+
level 6, the deepest one Markdown knows, below which the children are
|
|
73
73
|
concise regardless -- and the optionally derived Gradia diagram spec
|
|
74
74
|
embedded as a "gradia" fenced code block below the heading */
|
|
75
75
|
const renderObjectMd = (object, level, diagrams) => {
|
|
@@ -83,12 +83,12 @@ const renderObjectMd = (object, level, diagrams) => {
|
|
|
83
83
|
parts.push(renderKeyValuesMd(object.properties));
|
|
84
84
|
if (object.description !== undefined)
|
|
85
85
|
parts.push(renderDescriptionMd(object.description));
|
|
86
|
-
/* the
|
|
86
|
+
/* the children share one format, as a concise item following
|
|
87
87
|
a heading would attach to the heading's object instead */
|
|
88
88
|
if (level >= 6 || (level >= 3 && !complexBelowMd(object)))
|
|
89
|
-
parts.push(object.
|
|
89
|
+
parts.push(object.children.map((child) => renderConciseMd(child)).join("\n"));
|
|
90
90
|
else
|
|
91
|
-
parts.push(...object.
|
|
91
|
+
parts.push(...object.children.map((child) => renderObjectMd(child, level + 1, diagrams)));
|
|
92
92
|
return parts.filter((part) => part !== "").join("\n\n");
|
|
93
93
|
};
|
|
94
94
|
/* render the entire specification into normalized Markdown */
|