@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.
@@ -43,6 +43,13 @@ const templates = {
43
43
  <span class="search-clear" id="search-clear" title="clear search">&#x00D7;</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.childs }}</li>
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.childs }}
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.childs ?? [], path, keys, info);
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.childs, spec);
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.childs ?? [], result);
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 = (childs) => {
980
- const used = [...new Set(childs.flatMap((child) => child.properties.map((property) => property.key)))];
981
- const schema = schemas?.get(childs[0]);
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: childs.some((child) => child.description !== undefined || child.childs.length > 0) };
1244
+ return { keys, desc: children.some((child) => child.description !== undefined || child.children.length > 0) };
987
1245
  };
988
- /* provide the childs of an object taking part in the regular document
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 flowChilds = (object) => object.childs.filter((child) => !isTitleObject(child));
992
- /* decide whether a single-kind group of childs collapses into the
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.childs.length === 0);
1258
+ return concise || group.every((child) => child.children.length === 0);
1001
1259
  };
1002
- /* group the childs of an object by their kind, preserving order */
1003
- const groupChilds = (childs) => {
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 childs) {
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 childs of an object group-wise by kind: a concise group
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 renderChilds = (object, level, concise) => groupChilds(flowChilds(object)).map((group) => conciseGroup(group, schemas, concise) ?
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 childs (implicitly or
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 += renderChilds(child, 6, true);
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 childs into one compact table:
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 = (childs, maxColumns) => {
1045
- const { keys, desc } = tableShape(childs);
1046
- const diagrammed = childs.some((child) => diagramOf(child) !== "");
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: childs[0].kind !== "" ? childs[0].kind : "Name",
1050
- info: infoKeyOf(childs[0]),
1051
- infopath: infoPathOf(childs[0], false),
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: childs.map((child, i) => scoped(child, () => ({
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: childs[0].kind !== "" ? childs[0].kind : "Name",
1075
- info: infoKeyOf(childs[0]),
1076
- infopath: infoPathOf(childs[0], false),
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: childs.map((child, i) => scoped(child, () => {
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
- childs: safe(renderChilds(object, level + 1, concise))
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 childs of an object rendered with headings of their own
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 headingChilds = (object) => groupChilds(flowChilds(object))
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(headingChilds(object), level + 1);
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
- childs: safe(entries(headingChilds(object)))
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
- childs: groupChilds(flowChilds(object))
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.childs.some((child) => child.name.includes(";")
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 childs as nested list items */
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.childs.map((child) => renderConciseMd(child, depth + 1))]
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 childs from
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 childs (as siblings
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 childs are
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 childs share one format, as a concise item following
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.childs.map((child) => renderConciseMd(child)).join("\n"));
89
+ parts.push(object.children.map((child) => renderConciseMd(child)).join("\n"));
90
90
  else
91
- parts.push(...object.childs.map((child) => renderObjectMd(child, level + 1, diagrams)));
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 */