@rse/specbook 1.2.2 → 1.2.4

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,12 +43,14 @@ 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="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>
46
47
  {{ Document.tocpanel }}
47
48
  {{ Document.titlepage }}
48
49
  {{ Document.toc }}
49
50
  {{ Document.doc }}
50
51
  {{ Document.artifacts }}
51
52
  {% if Document.search %}<script>{{ Document.search }}</script>{% endif %}
53
+ <script>{{ Document.progress }}</script>
52
54
  {% if Document.tocpanel %}<script>{{ Document.tocscript }}</script>{% endif %}
53
55
  {% if Document.info %}<script>{{ Document.info }}</script>{% endif %}
54
56
  {% if Document.realtime %}<script class="realtime">{{ Document.realtime }}</script>{% endif %}
@@ -120,7 +122,7 @@ const templates = {
120
122
  "TocPanelEntries": textframe `
121
123
  <ul>
122
124
  {% for entry in Entries %}
123
- <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>
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.children }}</li>
124
126
  {% endfor %}
125
127
  </ul>
126
128
  `,
@@ -144,7 +146,7 @@ const templates = {
144
146
  {{ Object.diagram }}
145
147
  {{ Object.properties }}
146
148
  {{ Object.description }}
147
- {{ Object.childs }}
149
+ {{ Object.children }}
148
150
  </section>
149
151
  `,
150
152
  /* <Properties/> */
@@ -251,8 +253,8 @@ const placeholderStylesheet = textframe `
251
253
  div.placeholder { position: absolute; top: 50%; left: 0; right: 0;
252
254
  transform: translateY(-50%); text-align: center; opacity: 0.6 }
253
255
  `;
254
- /* the client-side script of the live preview: it connects back to the
255
- own page over WebSocket (the URL scheme "http"/"https" replaced with
256
+ /* the client-side script of the live preview: it connects back to
257
+ its own page over WebSocket (the URL scheme "http"/"https" replaced with
256
258
  "ws"/"wss"), re-connects every second after a lost connection, and
257
259
  updates the page on a received "RELOAD" command as well as on every
258
260
  re-established connection (as the server was restarted meanwhile).
@@ -275,7 +277,11 @@ const realtimeScript = textframe `
275
277
  icon.className = "realtime-status " + state
276
278
  }
277
279
  let blink = 0
280
+ let seq = 0
278
281
  const update = async () => {
282
+ /* a superseded update (an overlapping newer one started
283
+ meanwhile) is dropped, as its stale page could arrive last */
284
+ const mine = ++seq
279
285
  let response
280
286
  try {
281
287
  response = await fetch(window.location.pathname, { cache: "no-store" })
@@ -286,7 +292,10 @@ const realtimeScript = textframe `
286
292
  }
287
293
  if (!response.ok)
288
294
  return
289
- const doc = new DOMParser().parseFromString(await response.text(), "text/html")
295
+ const html = await response.text()
296
+ if (mine !== seq)
297
+ return
298
+ const doc = new DOMParser().parseFromString(html, "text/html")
290
299
  const x = window.scrollX
291
300
  const y = window.scrollY
292
301
  document.title = doc.title
@@ -332,6 +341,47 @@ const realtimeScript = textframe `
332
341
  connect()
333
342
  })()
334
343
  `;
344
+ /* the client-side script of the scroll progress meter: it drives the
345
+ DONE arc of the ring (through the dash offset of its unit-length
346
+ circle) with the scrolled fraction of the document, shows the meter
347
+ once the page is scrolled beyond 400px, and scrolls the page back
348
+ to the top on a click (stripping the URL hash, so a later reload
349
+ stays at the top). The updates are throttled onto animation frames,
350
+ and the window-bound listeners retire themselves once a live preview
351
+ body swap replaced the meter (whose fresh script re-attaches) */
352
+ const scrollProgressScript = textframe `
353
+ (function () {
354
+ const meter = document.querySelector("div.scroll-progress")
355
+ const ring = meter.querySelector("circle.scroll-done")
356
+ let ticking = false
357
+ const update = () => {
358
+ ticking = false
359
+ if (!meter.isConnected) {
360
+ window.removeEventListener("scroll", schedule)
361
+ window.removeEventListener("resize", schedule)
362
+ return
363
+ }
364
+ const doc = document.documentElement
365
+ const max = doc.scrollHeight - doc.clientHeight
366
+ ring.setAttribute("stroke-dashoffset", String(1 - (max > 0 ? doc.scrollTop / max : 0)))
367
+ meter.classList.toggle("shown", doc.scrollTop > 400)
368
+ }
369
+ const schedule = () => {
370
+ if (!ticking) {
371
+ ticking = true
372
+ requestAnimationFrame(update)
373
+ }
374
+ }
375
+ meter.addEventListener("click", () => {
376
+ window.scrollTo({ top: 0, behavior: "smooth" })
377
+ if (window.location.hash)
378
+ history.replaceState(null, "", window.location.pathname + window.location.search)
379
+ })
380
+ window.addEventListener("scroll", schedule, { passive: true })
381
+ window.addEventListener("resize", schedule)
382
+ update()
383
+ })()
384
+ `;
335
385
  /* the client-side script of the table of contents side panel: it
336
386
  slides the panel out of and back into the viewport edge (closing it
337
387
  also on a jump, on "Escape", and on a click outside), remembers the
@@ -656,8 +706,19 @@ const env = new nunjucks.Environment(null, { autoescape: true });
656
706
  nunjucksAddons(env);
657
707
  /* mark pre-rendered HTML as safe for template interpolation */
658
708
  const safe = (html) => new nunjucks.runtime.SafeString(html);
709
+ /* the built-in Nunjucks templates compiled on first use, as
710
+ renderString() would re-compile a template on every single call
711
+ (i.e. for every rendered object, property table, and description) */
712
+ const compiled = new Map();
659
713
  /* render one of the built-in Nunjucks templates with a context */
660
- const render = (name, context) => env.renderString(templates[name], context);
714
+ const render = (name, context) => {
715
+ let template = compiled.get(name);
716
+ if (template === undefined) {
717
+ template = new nunjucks.Template(templates[name], env, undefined, true);
718
+ compiled.set(name, template);
719
+ }
720
+ return template.render(context);
721
+ };
661
722
  /* the active per-document reference expander, fully-qualified
662
723
  anchor paths, member-carrying property value constraints, object
663
724
  schema nodes, pre-rendered diagram SVGs, description popup keys
@@ -739,7 +800,7 @@ const collectInfo = (nodes, prefix, keys, info) => {
739
800
  }
740
801
  if (entry.d !== undefined || entry.p !== undefined)
741
802
  info[path] = entry;
742
- collectInfo(schema.childs ?? [], path, keys, info);
803
+ collectInfo(schema.children ?? [], path, keys, info);
743
804
  }
744
805
  };
745
806
  /* inject the description popup maps into their client-side script ("<"
@@ -827,7 +888,7 @@ const collectSpec = (objects, spec) => {
827
888
  .trim();
828
889
  if (text !== "")
829
890
  spec[anchorOf(object)] = scoped(object, () => block(text, true).toString().trim());
830
- collectSpec(object.childs, spec);
891
+ collectSpec(object.children, spec);
831
892
  }
832
893
  };
833
894
  /* the literal members of an "enum(...)"/"tags(...)" expression, or
@@ -852,7 +913,7 @@ const collectMembers = (nodes, result) => {
852
913
  /* an invalid expression is the concern of lint */
853
914
  }
854
915
  }
855
- collectMembers(schema.childs ?? [], result);
916
+ collectMembers(schema.children ?? [], result);
856
917
  }
857
918
  return result;
858
919
  };
@@ -915,20 +976,20 @@ const effectiveProperties = (object) => {
915
976
  order (unknown keys appended in occurrence order), with
916
977
  "withUnusedProps" injecting the defined but still unused schema
917
978
  properties as additional columns */
918
- const tableShape = (childs) => {
919
- const used = [...new Set(childs.flatMap((child) => child.properties.map((property) => property.key)))];
920
- const schema = schemas?.get(childs[0]);
979
+ const tableShape = (children) => {
980
+ const used = [...new Set(children.flatMap((child) => child.properties.map((property) => property.key)))];
981
+ const schema = schemas?.get(children[0]);
921
982
  const names = (schema?.props ?? []).map((prop) => prop.name);
922
983
  const known = schema?.format?.withUnusedProps === true ?
923
984
  names : names.filter((name) => used.includes(name));
924
985
  const keys = [...known, ...used.filter((key) => !names.includes(key))];
925
- return { keys, desc: childs.some((child) => child.description !== undefined || child.childs.length > 0) };
986
+ return { keys, desc: children.some((child) => child.description !== undefined || child.children.length > 0) };
926
987
  };
927
- /* provide the childs of an object taking part in the regular document
988
+ /* provide the children of an object taking part in the regular document
928
989
  flow: a nested title object leaves the flow, as it is rendered as the
929
990
  title page instead (a top-level one instead removes its whole artifact) */
930
- const flowChilds = (object) => object.childs.filter((child) => !isTitleObject(child));
931
- /* decide whether a single-kind group of childs collapses into the
991
+ const flowChildren = (object) => object.children.filter((child) => !isTitleObject(child));
992
+ /* decide whether a single-kind group of children collapses into the
932
993
  concise (tabular) rendering: an explicit "format" type of the kind
933
994
  wins, "auto" collapses the deepest level only, and inside an already
934
995
  concise rendering context "auto" groups implicitly stay concise, too */
@@ -936,12 +997,12 @@ const conciseGroup = (group, schemaMap, concise) => {
936
997
  const type = schemaMap?.get(group[0])?.format?.type ?? "auto";
937
998
  if (type !== "auto")
938
999
  return type === "concise";
939
- return concise || group.every((child) => child.childs.length === 0);
1000
+ return concise || group.every((child) => child.children.length === 0);
940
1001
  };
941
- /* group the childs of an object by their kind, preserving order */
942
- const groupChilds = (childs) => {
1002
+ /* group the children of an object by their kind, preserving order */
1003
+ const groupChildren = (children) => {
943
1004
  const groups = new Map();
944
- for (const child of childs) {
1005
+ for (const child of children) {
945
1006
  const group = groups.get(child.kind);
946
1007
  if (group === undefined)
947
1008
  groups.set(child.kind, [child]);
@@ -950,21 +1011,21 @@ const groupChilds = (childs) => {
950
1011
  }
951
1012
  return [...groups.values()];
952
1013
  };
953
- /* render the childs of an object group-wise by kind: a concise group
1014
+ /* render the children of an object group-wise by kind: a concise group
954
1015
  as one compact table, a complex group as regular nested object
955
1016
  renderings */
956
- const renderChilds = (object, level, concise) => groupChilds(flowChilds(object)).map((group) => conciseGroup(group, schemas, concise) ?
1017
+ const renderChildren = (object, level, concise) => groupChildren(flowChildren(object)).map((group) => conciseGroup(group, schemas, concise) ?
957
1018
  renderTable(group, maxColumnsOf(group[0])) :
958
1019
  group.map((child) => renderObject(child, level, concise)).join("")).join("");
959
1020
  /* render the description cell of a table row: the description of the
960
- object followed by its recursively rendered childs (implicitly or
1021
+ object followed by its recursively rendered children (implicitly or
961
1022
  explicitly concise groups as nested sub-tables, explicitly complex
962
1023
  ones as regular nested object renderings pressed into the cell);
963
1024
  a cell left without any content renders as the absent marker,
964
1025
  exactly like a property cell of a not given property */
965
1026
  const renderCell = (child) => {
966
1027
  let html = child.description !== undefined ? renderDescription(child.description) : "";
967
- html += renderChilds(child, 6, true);
1028
+ html += renderChildren(child, 6, true);
968
1029
  return html.trim() !== "" ? html : "<span class=\"value-absent\"></span>";
969
1030
  };
970
1031
  /* the pre-rendered diagram of an object as an embeddable block
@@ -973,27 +1034,27 @@ const diagramOf = (object) => {
973
1034
  const diagram = diagrams?.get(object);
974
1035
  return diagram !== undefined ? safe(`<div class="diagram">${diagram}</div>`) : "";
975
1036
  };
976
- /* render a single-kind group of childs into one compact table:
1037
+ /* render a single-kind group of children into one compact table:
977
1038
  the name first, then the property columns, then the description;
978
1039
  a group wider than maxColumns, or carrying diagrams, instead chunks
979
1040
  the property and description cells (and the leading diagram) of
980
1041
  every object into an embedded per-object table, as a diagram is
981
1042
  identifiable only under a header of its own, not under the column
982
1043
  headers of a plain table */
983
- const renderTable = (childs, maxColumns) => {
984
- const { keys, desc } = tableShape(childs);
985
- const diagrams = childs.some((child) => diagramOf(child) !== "");
986
- if (!diagrams && 1 + keys.length + (desc ? 1 : 0) <= maxColumns)
1044
+ const renderTable = (children, maxColumns) => {
1045
+ const { keys, desc } = tableShape(children);
1046
+ const diagrammed = children.some((child) => diagramOf(child) !== "");
1047
+ if (!diagrammed && 1 + keys.length + (desc ? 1 : 0) <= maxColumns)
987
1048
  return render("Table", { Table: {
988
- head: childs[0].kind !== "" ? childs[0].kind : "Name",
989
- info: infoKeyOf(childs[0]),
990
- infopath: infoPathOf(childs[0], false),
1049
+ head: children[0].kind !== "" ? children[0].kind : "Name",
1050
+ info: infoKeyOf(children[0]),
1051
+ infopath: infoPathOf(children[0], false),
991
1052
  keys,
992
1053
  desc,
993
1054
  /* under the fixed table layout the description column claims
994
1055
  twice the share of a regular column, compressing the others */
995
1056
  width: Math.round(200 / (keys.length + 3)),
996
- rows: childs.map((child, i) => scoped(child, () => ({
1057
+ rows: children.map((child, i) => scoped(child, () => ({
997
1058
  id: anchorOf(child),
998
1059
  paren: child.paren,
999
1060
  primary: child.primary,
@@ -1010,11 +1071,11 @@ const renderTable = (childs, maxColumns) => {
1010
1071
  with the last cell spanning the leftover columns of the final row */
1011
1072
  const size = Math.max(1, maxColumns - 1);
1012
1073
  return render("TableChunked", { Table: {
1013
- head: childs[0].kind !== "" ? childs[0].kind : "Name",
1014
- info: infoKeyOf(childs[0]),
1015
- infopath: infoPathOf(childs[0], false),
1074
+ head: children[0].kind !== "" ? children[0].kind : "Name",
1075
+ info: infoKeyOf(children[0]),
1076
+ infopath: infoPathOf(children[0], false),
1016
1077
  width: Math.round(100 / maxColumns),
1017
- rows: childs.map((child, i) => scoped(child, () => {
1078
+ rows: children.map((child, i) => scoped(child, () => {
1018
1079
  const cells = keys.map((key) => ({ key, desc: false, span: 1,
1019
1080
  value: inlineValue(child.kind, child.properties.find((property) => property.key === key)) }));
1020
1081
  if (desc)
@@ -1064,7 +1125,7 @@ const renderObject = (object, level, concise) => {
1064
1125
  safe(render("Properties", { Properties: inlineProperties(object, properties) })) : "",
1065
1126
  description: object.description !== undefined ?
1066
1127
  safe(renderDescription(object.description)) : "",
1067
- childs: safe(renderChilds(object, level + 1, concise))
1128
+ children: safe(renderChildren(object, level + 1, concise))
1068
1129
  } }));
1069
1130
  };
1070
1131
  /* format a timestamp as its calendar date in local time (the
@@ -1106,10 +1167,10 @@ const renderTitlePage = (object, created, modified) => {
1106
1167
  const renderArtifact = (artifact) => render("Artifact", { Artifact: {
1107
1168
  objects: safe(artifact.objects.map((object) => renderObject(object, 1, false)).join(""))
1108
1169
  } });
1109
- /* provide the childs of an object rendered with headings of their own
1170
+ /* provide the children of an object rendered with headings of their own
1110
1171
  (skipping the child groups collapsing into compact tables), which
1111
1172
  are the ones taking part in the table of contents and its side panel */
1112
- const headingChilds = (object) => groupChilds(flowChilds(object))
1173
+ const headingChildren = (object) => groupChildren(flowChildren(object))
1113
1174
  .filter((group) => !conciseGroup(group, schemas, false))
1114
1175
  .flat();
1115
1176
  /* flatten the hierarchy of the rendered object headings (exactly like
@@ -1124,7 +1185,7 @@ const tocEntries = (objects, pages) => {
1124
1185
  info: infoKeyOf(object), infopath: infoPathOf(object, false),
1125
1186
  spec: infoParents !== null ? id : undefined, specpath: infoPathOf(object),
1126
1187
  name: inline(object.name), level: Math.min(level, 6), page: pages?.get(id) });
1127
- collect(headingChilds(object), level + 1);
1188
+ collect(headingChildren(object), level + 1);
1128
1189
  }
1129
1190
  };
1130
1191
  collect(objects, 1);
@@ -1144,7 +1205,7 @@ const renderTocPanel = (objects, title, doc) => {
1144
1205
  spec: infoParents !== null ? anchorOf(object) : undefined,
1145
1206
  specpath: infoPathOf(object),
1146
1207
  name: inline(object.name),
1147
- childs: safe(entries(headingChilds(object)))
1208
+ children: safe(entries(headingChildren(object)))
1148
1209
  })) });
1149
1210
  return render("TocPanel", { TocPanel: { title, doc, entries: safe(entries(objects)) } });
1150
1211
  };
@@ -1156,7 +1217,7 @@ export const htmlOutline = (specification, config) => {
1156
1217
  const entry = (object) => ({
1157
1218
  title: (object.kind !== "" ? `${object.kind}: ` : "") + plainText(object.name),
1158
1219
  anchor: paths.get(object) ?? object.id,
1159
- childs: groupChilds(flowChilds(object))
1220
+ children: groupChildren(flowChildren(object))
1160
1221
  .filter((group) => !conciseGroup(group, schemaMap, false))
1161
1222
  .flatMap((group) => group.map(entry))
1162
1223
  });
@@ -1242,47 +1303,48 @@ export const renderHtml = async (specification, config, tocPages, css, realtime
1242
1303
  /* the document language selects the smart typography quote style */
1243
1304
  const lang = documentLang(specification);
1244
1305
  quotes = quoteStyles[lang?.toLowerCase().split(/[-_]/)[0] ?? "en"] ?? quoteStyles.en;
1245
- /* establish the per-document rendering state */
1306
+ /* establish the per-document rendering state (released again
1307
+ below, also on a failure of the collecting or rendering) */
1246
1308
  const index = buildLinkIndex(specification);
1247
- anchors = anchorPaths(index);
1248
- members = config !== undefined ? collectMembers(config, new Map()) : null;
1249
- schemas = config !== undefined ? collectSchemas(specification, config) : null;
1250
- diagrams = rendered;
1251
- /* collect the schema descriptions for the description popups,
1252
- plus the object parents composing their title paths */
1253
- let info = null;
1254
- if (config !== undefined) {
1255
- infoKeys = new Map();
1256
- info = {};
1257
- collectInfo(config, "", infoKeys, info);
1258
- const parents = new Map();
1259
- for (const node of index)
1260
- if (node.parent !== undefined)
1261
- parents.set(node.object, node.parent.object);
1262
- infoParents = parents;
1263
- }
1264
- linker = makeLinker(index);
1265
- /* collect the corpus descriptions of the object instances for the
1266
- description popups (after the linker is in place, as the
1267
- pre-rendered descriptions expand their references, too) */
1268
- let spec = null;
1269
- if (info !== null) {
1270
- spec = {};
1271
- for (const artifact of specification.artifacts)
1272
- collectSpec(artifact.objects, spec);
1273
- }
1274
- /* the artifact timestamps aggregate into the earliest creation
1275
- and the latest modification timestamp of the document */
1276
- const created = new Date(Math.min(...specification.artifacts.map((artifact) => artifact.created.getTime())));
1277
- const modified = new Date(Math.max(...specification.artifacts.map((artifact) => artifact.modified.getTime())));
1278
- /* a "META: Title" object becomes the title page and leaves the regular
1279
- flow, while its diagram becomes the "Diagram of Contents" page */
1280
- const title = titlePageObject(specification);
1281
- const artifacts = specification.artifacts
1282
- .filter((artifact) => !artifact.objects.some(isTitleObject));
1283
- const meta = titleObject(specification);
1284
- const doc = meta !== undefined ? rendered?.get(meta) : undefined;
1285
1309
  try {
1310
+ anchors = anchorPaths(index);
1311
+ members = config !== undefined ? collectMembers(config, new Map()) : null;
1312
+ schemas = config !== undefined ? collectSchemas(specification, config) : null;
1313
+ diagrams = rendered;
1314
+ /* collect the schema descriptions for the description popups,
1315
+ plus the object parents composing their title paths */
1316
+ let info = null;
1317
+ if (config !== undefined) {
1318
+ infoKeys = new Map();
1319
+ info = {};
1320
+ collectInfo(config, "", infoKeys, info);
1321
+ const parents = new Map();
1322
+ for (const node of index)
1323
+ if (node.parent !== undefined)
1324
+ parents.set(node.object, node.parent.object);
1325
+ infoParents = parents;
1326
+ }
1327
+ linker = makeLinker(index);
1328
+ /* collect the corpus descriptions of the object instances for the
1329
+ description popups (after the linker is in place, as the
1330
+ pre-rendered descriptions expand their references, too) */
1331
+ let spec = null;
1332
+ if (info !== null) {
1333
+ spec = {};
1334
+ for (const artifact of specification.artifacts)
1335
+ collectSpec(artifact.objects, spec);
1336
+ }
1337
+ /* the artifact timestamps aggregate into the earliest creation
1338
+ and the latest modification timestamp of the document */
1339
+ const created = new Date(Math.min(...specification.artifacts.map((artifact) => artifact.created.getTime())));
1340
+ const modified = new Date(Math.max(...specification.artifacts.map((artifact) => artifact.modified.getTime())));
1341
+ /* a "META: Title" object becomes the title page and leaves the regular
1342
+ flow, while its diagram becomes the "Diagram of Contents" page */
1343
+ const title = titlePageObject(specification);
1344
+ const artifacts = specification.artifacts
1345
+ .filter((artifact) => !artifact.objects.some(isTitleObject));
1346
+ const meta = titleObject(specification);
1347
+ const doc = meta !== undefined ? rendered?.get(meta) : undefined;
1286
1348
  const objects = artifacts.flatMap((artifact) => artifact.objects);
1287
1349
  const entries = tocEntries(objects, tocPages);
1288
1350
  return render("Document", { Document: {
@@ -1294,6 +1356,7 @@ export const renderHtml = async (specification, config, tocPages, css, realtime
1294
1356
  titlepage: title !== undefined ?
1295
1357
  safe(renderTitlePage(title, formatDate(created), formatDate(modified))) : "",
1296
1358
  search: safe(searchScript()),
1359
+ progress: safe(scrollProgressScript),
1297
1360
  info: info !== null ? safe(infoScript(info, spec ?? {})) : "",
1298
1361
  realtime: realtime ? safe(realtimeScript) : "",
1299
1362
  toc: entries.length > 0 ? safe(render("Toc", { Toc: { entries } })) : "",
@@ -33,15 +33,17 @@ const renderDescriptionMd = (description) => description.description + (descript
33
33
  const nameSuffixMd = (object) => (object.primary === true ? " (*)" : "") +
34
34
  (object.paren !== undefined ? ` (${object.paren})` : "") +
35
35
  (object.anchor !== undefined ? ` {{${object.anchor}}}` : "");
36
- /* whether any descendant of an object carries a description which no
37
- Concise Format item can carry -- a multi-line one (a fenced code
38
- block, an ordered list, a blockquote, or multiple paragraphs) or one
39
- carrying a ";", as only a ";"-free description stays a single segment
40
- on re-parsing instead of splitting off a spurious property */
41
- const complexBelowMd = (object) => object.childs.some((child) => (child.description !== undefined
42
- && /[\n;]/.test(renderDescriptionMd(child.description))) || complexBelowMd(child));
36
+ /* whether any descendant of an object carries a name or a description
37
+ which no Concise Format item can carry -- a name carrying a ";" (it
38
+ would split the head segment), or a multi-line description (a fenced
39
+ code block, an ordered list, a blockquote, or multiple paragraphs) or
40
+ one carrying a ";", as only a ";"-free description stays a single
41
+ segment on re-parsing instead of splitting off a spurious property */
42
+ const complexBelowMd = (object) => object.children.some((child) => child.name.includes(";")
43
+ || (child.description !== undefined
44
+ && /[\n;]/.test(renderDescriptionMd(child.description))) || complexBelowMd(child));
43
45
  /* render an object in the Concise Format as a list item of the
44
- given nesting depth, with its childs as nested list items */
46
+ given nesting depth, with its children as nested list items */
45
47
  const renderConciseMd = (object, depth = 0) => {
46
48
  const indent = " ".repeat(depth * 4);
47
49
  const segments = [`${object.kind}: ${object.name}${nameSuffixMd(object)}`];
@@ -60,14 +62,14 @@ const renderConciseMd = (object, depth = 0) => {
60
62
  `${indent}- ${segments.join("; ").replace(/\.?$/, ".")}` :
61
63
  `${indent}- ${segments.join("; ")};`;
62
64
  return [item, renderKeyValuesMd(wrapped, `${indent} `),
63
- ...object.childs.map((child) => renderConciseMd(child, depth + 1))]
65
+ ...object.children.map((child) => renderConciseMd(child, depth + 1))]
64
66
  .filter((part) => part !== "").join("\n");
65
67
  };
66
- /* 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
67
69
  level 4 upwards in the Concise Format -- unless a descendant carries
68
- 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
69
71
  have to share the format) stay in the Complex Format down to heading
70
- 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
71
73
  concise regardless -- and the optionally derived Gradia diagram spec
72
74
  embedded as a "gradia" fenced code block below the heading */
73
75
  const renderObjectMd = (object, level, diagrams) => {
@@ -81,12 +83,12 @@ const renderObjectMd = (object, level, diagrams) => {
81
83
  parts.push(renderKeyValuesMd(object.properties));
82
84
  if (object.description !== undefined)
83
85
  parts.push(renderDescriptionMd(object.description));
84
- /* the childs share one format, as a concise item following
86
+ /* the children share one format, as a concise item following
85
87
  a heading would attach to the heading's object instead */
86
88
  if (level >= 6 || (level >= 3 && !complexBelowMd(object)))
87
- parts.push(object.childs.map((child) => renderConciseMd(child)).join("\n"));
89
+ parts.push(object.children.map((child) => renderConciseMd(child)).join("\n"));
88
90
  else
89
- parts.push(...object.childs.map((child) => renderObjectMd(child, level + 1, diagrams)));
91
+ parts.push(...object.children.map((child) => renderObjectMd(child, level + 1, diagrams)));
90
92
  return parts.filter((part) => part !== "").join("\n\n");
91
93
  };
92
94
  /* render the entire specification into normalized Markdown */
@@ -28,7 +28,7 @@ const anchorPages = async (pdf) => {
28
28
  };
29
29
  /* attach a hierarchical PDF outline (the "bookmarks" of the viewer
30
30
  side-bar) to a document, resolving the entry anchors against the
31
- named destinations and hoisting the childs of an unresolvable entry */
31
+ named destinations and hoisting the children of an unresolvable entry */
32
32
  const addOutline = async (doc, entries) => {
33
33
  const { PDFName, PDFDict, PDFArray, PDFRef, PDFHexString } = await import("pdf-lib");
34
34
  const context = doc.context;
@@ -39,15 +39,17 @@ const addOutline = async (doc, entries) => {
39
39
  if (dests !== undefined) {
40
40
  for (const [name] of dests.entries()) {
41
41
  const dest = dests.lookup(name);
42
- const page = dest instanceof PDFArray ? dest.get(0) : undefined;
43
- if (dest instanceof PDFArray && page instanceof PDFRef && live.has(page.toString()))
42
+ if (!(dest instanceof PDFArray))
43
+ continue;
44
+ const page = dest.get(0);
45
+ if (page instanceof PDFRef && live.has(page.toString()))
44
46
  targets.set(name.decodeText(), dest);
45
47
  }
46
48
  }
47
49
  const resolve = (entries) => entries.flatMap((entry) => {
48
50
  const dest = targets.get(entry.anchor);
49
- const childs = resolve(entry.childs);
50
- return dest !== undefined ? [{ title: entry.title, dest, childs }] : childs;
51
+ const children = resolve(entry.children);
52
+ return dest !== undefined ? [{ title: entry.title, dest, children }] : children;
51
53
  });
52
54
  const items = resolve(entries);
53
55
  if (items.length === 0)
@@ -57,16 +59,16 @@ const addOutline = async (doc, entries) => {
57
59
  const materialize = (items, parent) => {
58
60
  const refs = items.map(() => context.nextRef());
59
61
  items.forEach((item, i) => {
60
- const childs = item.childs.length > 0 ? materialize(item.childs, refs[i]) : undefined;
62
+ const children = item.children.length > 0 ? materialize(item.children, refs[i]) : undefined;
61
63
  context.assign(refs[i], context.obj({
62
64
  Title: PDFHexString.fromText(item.title),
63
65
  Parent: parent,
64
66
  Prev: i > 0 ? refs[i - 1] : undefined,
65
67
  Next: i < refs.length - 1 ? refs[i + 1] : undefined,
66
- First: childs?.first,
67
- Last: childs?.last,
68
+ First: children?.first,
69
+ Last: children?.last,
68
70
  /* a negative count keeps the sub-tree initially collapsed */
69
- Count: childs !== undefined ? -item.childs.length : undefined,
71
+ Count: children !== undefined ? -item.children.length : undefined,
70
72
  Dest: item.dest
71
73
  }));
72
74
  });
@@ -14,7 +14,7 @@ export type SchemaObject = {
14
14
  diagram?: SchemaDiagram;
15
15
  format?: SchemaFormat;
16
16
  props?: SchemaProperty[];
17
- childs?: SchemaObject[];
17
+ children?: SchemaObject[];
18
18
  };
19
19
  export type SchemaAutomaton = {
20
20
  nodes: string;
@@ -16,6 +16,8 @@ const GradiaConfigSchemas = Object.fromEntries(Object.entries(Gradia.config).map
16
16
  typeof value === "number" ? v.pipe(v.number(), v.finite(), v.minValue(0)) : v.string()]));
17
17
  const SchemaDiagramConfig = v.partial(v.strictObject(GradiaConfigSchemas, (issue) => issue.expected === "never" ? "unknown Gradia rendering option" :
18
18
  "expected a map of Gradia rendering options"));
19
+ /* the Valibot schemas mirroring the types above, with the object
20
+ kind schema referencing itself lazily for the nested children */
19
21
  const SchemaProperty = v.strictObject({
20
22
  name: v.string(),
21
23
  desc: v.optional(v.string()),
@@ -85,6 +87,6 @@ const SchemaObject = v.strictObject({
85
87
  diagram: v.optional(SchemaDiagram),
86
88
  format: v.optional(SchemaFormat),
87
89
  props: v.optional(v.array(SchemaProperty)),
88
- childs: v.optional(v.array(v.lazy(() => SchemaObject)))
90
+ children: v.optional(v.array(v.lazy(() => SchemaObject)))
89
91
  });
90
92
  export const Schema = v.pipe(v.array(SchemaObject), v.minLength(1));
@@ -16,7 +16,7 @@ export type SpecObject = {
16
16
  primary?: boolean;
17
17
  description?: SpecDescription;
18
18
  properties: SpecProperty[];
19
- childs: SpecObject[];
19
+ children: SpecObject[];
20
20
  };
21
21
  export type SpecDescription = {
22
22
  description: string;
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import * as v from "valibot";
7
7
  /* ==== Schema ==== */
8
+ /* the Valibot schemas mirroring the types above, with the object
9
+ schema referencing itself lazily for the nested children */
8
10
  const SpecProperty = v.object({
9
11
  key: v.string(),
10
12
  value: v.string(),
@@ -24,7 +26,7 @@ const SpecObject = v.object({
24
26
  primary: v.optional(v.boolean()),
25
27
  description: v.optional(SpecDescription),
26
28
  properties: v.array(SpecProperty),
27
- childs: v.array(v.lazy(() => SpecObject))
29
+ children: v.array(v.lazy(() => SpecObject))
28
30
  });
29
31
  const SpecArtifact = v.object({
30
32
  created: v.date(),
@@ -78,7 +78,7 @@ type SchemaObject = {
78
78
  diagram?: SchemaDiagram
79
79
  format?: SchemaFormat
80
80
  props?: SchemaProperty[]
81
- childs?: SchemaObject[]
81
+ children?: SchemaObject[]
82
82
  }
83
83
  type SchemaAutomaton = {
84
84
  nodes: string
@@ -251,7 +251,7 @@ type SchemaGradiaConfig = Partial<{
251
251
  properties allowed on objects of this kind,
252
252
  BECAUSE they carry the structured, machine-checkable content
253
253
 
254
- - `SchemaObject.childs?: SchemaObject[]`:
254
+ - `SchemaObject.children?: SchemaObject[]`:
255
255
  object kinds allowed one level below this object (RECURSION),
256
256
  BECAUSE the nesting defines and bounds the document structure
257
257
 
@@ -579,7 +579,7 @@ type SpecObject = {
579
579
  primary?: boolean
580
580
  description?: SpecDescription
581
581
  properties: SpecProperty[]
582
- childs: SpecObject[]
582
+ children: SpecObject[]
583
583
  }
584
584
  type SpecDescription = {
585
585
  description: string
@@ -653,7 +653,7 @@ type SpecProperty = {
653
653
  key/value properties of the object, in canonical order,
654
654
  BECAUSE properties carry the structured, machine-checkable content
655
655
 
656
- - `SpecObject.childs: SpecObject[]`:
656
+ - `SpecObject.children: SpecObject[]`:
657
657
  child objects nested one level below this object (RECURSION),
658
658
  BECAUSE a specification is a tree, not a flat list of statements
659
659