@rse/specbook 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +288 -43
  3. package/dst/specbook-api.d.ts +23 -3
  4. package/dst/specbook-api.js +133 -59
  5. package/dst/specbook-cli.js +93 -26
  6. package/dst/specbook-cmd-describe.js +4 -4
  7. package/dst/specbook-cmd-export-schema.d.ts +8 -0
  8. package/dst/specbook-cmd-export-schema.js +441 -0
  9. package/dst/specbook-cmd-export.js +23 -6
  10. package/dst/specbook-cmd-init-sample.d.ts +4 -0
  11. package/dst/specbook-cmd-init-sample.js +322 -0
  12. package/dst/specbook-cmd-init.d.ts +1 -0
  13. package/dst/specbook-cmd-init.js +42 -10
  14. package/dst/specbook-cmd-lint.js +46 -48
  15. package/dst/specbook-cmd-query.d.ts +30 -0
  16. package/dst/specbook-cmd-query.js +165 -0
  17. package/dst/specbook-config.d.ts +9 -1
  18. package/dst/specbook-config.js +135 -10
  19. package/dst/specbook-coverage.js +2 -2
  20. package/dst/specbook-diagram.d.ts +7 -5
  21. package/dst/specbook-diagram.js +546 -143
  22. package/dst/specbook-export-ast.d.ts +1 -1
  23. package/dst/specbook-export-ast.js +11 -8
  24. package/dst/specbook-export-code.js +3 -3
  25. package/dst/specbook-export-common.d.ts +1 -1
  26. package/dst/specbook-export-common.js +3 -2
  27. package/dst/specbook-export-diagram.d.ts +1 -1
  28. package/dst/specbook-export-diagram.js +25 -7
  29. package/dst/specbook-export-html-fold.d.ts +16 -6
  30. package/dst/specbook-export-html-fold.js +65 -44
  31. package/dst/specbook-export-html-render.js +107 -26
  32. package/dst/specbook-export-html-search.js +18 -20
  33. package/dst/specbook-export-html-template.js +38 -9
  34. package/dst/specbook-export-html-widget.d.ts +2 -0
  35. package/dst/specbook-export-html-widget.js +219 -19
  36. package/dst/specbook-export-html.css +208 -38
  37. package/dst/specbook-export-html.js +96 -53
  38. package/dst/specbook-export-md.d.ts +2 -1
  39. package/dst/specbook-export-md.js +7 -8
  40. package/dst/specbook-export-pdf.js +1 -1
  41. package/dst/specbook-files.d.ts +2 -0
  42. package/dst/specbook-files.js +41 -0
  43. package/dst/specbook-format-meta.yaml +401 -0
  44. package/dst/specbook-format-pm.yaml +1240 -0
  45. package/dst/specbook-format-schema.d.ts +33 -3
  46. package/dst/specbook-format-schema.js +49 -3
  47. package/dst/specbook-format.md +427 -47
  48. package/dst/specbook-format.yaml +524 -228
  49. package/dst/specbook-ignore.js +2 -2
  50. package/dst/specbook-link.d.ts +4 -0
  51. package/dst/specbook-link.js +61 -6
  52. package/dst/specbook-mcp.js +103 -19
  53. package/dst/specbook-parse-semantic.d.ts +1 -0
  54. package/dst/specbook-parse-semantic.js +11 -4
  55. package/dst/specbook-parse-syntax.d.ts +1 -0
  56. package/dst/specbook-parse-syntax.js +1 -1
  57. package/dst/specbook-parse.d.ts +1 -1
  58. package/dst/specbook-parse.js +3 -3
  59. package/dst/specbook-project.d.ts +2 -0
  60. package/dst/specbook-project.js +15 -5
  61. package/dst/specbook-remote.d.ts +28 -0
  62. package/dst/specbook-remote.js +429 -0
  63. package/package.json +32 -20
@@ -25,12 +25,12 @@
25
25
  /* let the search icon slide the input field out of the tab (and
26
26
  back in again), remembering the choice across page loads */
27
27
  const slide = (open) => {
28
- tab.classList.toggle("open", open)
29
- if (tab.classList.contains("open"))
28
+ const opened = tab.classList.toggle("open", open)
29
+ if (opened)
30
30
  input.focus()
31
31
  else
32
32
  input.blur()
33
- try { localStorage.setItem("specbook-search", tab.classList.contains("open") ? "open" : "closed") }
33
+ try { localStorage.setItem("specbook-search", opened ? "open" : "closed") }
34
34
  catch { /* an inaccessible storage just loses the state */ }
35
35
  }
36
36
  try { if (localStorage.getItem("specbook-search") === "open") tab.classList.add("open") }
@@ -83,23 +83,6 @@
83
83
  units.push({ el, id, text, words, keep: false })
84
84
  })
85
85
  })
86
-
87
- /* let a hyperlink whose target the filtering hid leave the
88
- search mode (like the Escape key) before it jumps: the click
89
- is intercepted, the document revealed again, and the click
90
- re-issued, so the browser performs the regular jump */
91
- document.querySelectorAll("a[href^='#']").forEach((link) => {
92
- link.addEventListener("click", (event) => {
93
- const target = document.getElementById(
94
- decodeURIComponent((link.getAttribute("href") ?? "").slice(1)))
95
- if (target === null || target.closest(".search-hide") === null)
96
- return
97
- event.preventDefault()
98
- reset()
99
- slide(false)
100
- link.click()
101
- })
102
- })
103
86
  }
104
87
 
105
88
  /* the Levenshtein edit distance of two words */
@@ -399,4 +382,19 @@
399
382
  reset()
400
383
  input.focus()
401
384
  })
385
+
386
+ /* let a hyperlink whose target the filtering hid leave the search
387
+ mode (like the Escape key) before it jumps, so the browser
388
+ performs the regular jump onto the revealed target */
389
+ document.body.addEventListener("click", (event) => {
390
+ const link = event.target.closest("a[href^='#']")
391
+ if (link === null || !document.body.classList.contains("searching"))
392
+ return
393
+ const target = document.getElementById(
394
+ decodeURIComponent((link.getAttribute("href") ?? "").slice(1)))
395
+ if (target === null || target.closest(".search-hide") === null)
396
+ return
397
+ reset()
398
+ slide(false)
399
+ })
402
400
  })()
@@ -34,18 +34,45 @@ const templates = {
34
34
  <span class="search-clear" id="search-clear" title="clear search">&#x00D7;</span>
35
35
  </div>
36
36
  </div>
37
+ <div class="filter-switch">
38
+ <div class="filter-toggle" title="toggle artifact filter"><svg class="filter-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l-9 4.5 9 4.5 9-4.5z"/><path d="M3 12.25l9 4.5 9-4.5"/><path d="M3 17l9 4.5 9-4.5"/></svg></div>
39
+ <div class="filter-panel">
40
+ <div class="filter-grid" style="grid-template-columns: repeat({{ Document.filtergrid.columns }}, max-content)">
41
+ {% for row in Document.filtergrid.rows %}
42
+ <div class="filter-kind" title="toggle all {{ row.kind }} artifacts">{{ row.kind }}</div>
43
+ {%- for cell in row.cells %}<div class="filter-cell" data-id="{{ cell.id }}" title="{{ cell.title }}">{{ cell.label }}</div>{% endfor %}
44
+ {% endfor %}
45
+ </div>
46
+ </div>
47
+ </div>
37
48
  {%- if not Document.omitted.all %}
38
49
  <div class="fold-switch">
39
50
  <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>
40
- <div class="fold-controls">
51
+ <div class="fold-panel">
52
+ <div class="fold-rows">
53
+ {%- if not Document.omitted.gradia %}
54
+ <div class="fold-row">
41
55
  {% if not Document.omitted.graph %}<div class="fold-graphs" title="fold/unfold all graph 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>{% endif %}
42
56
  {% if not Document.omitted.hub %}<div class="fold-hubs" title="fold/unfold all hub diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.5"/><circle cx="3.5" cy="4.5" r="2"/><circle cx="3.5" cy="19.5" r="2"/><circle cx="20.5" cy="4.5" r="2"/><circle cx="20.5" cy="19.5" r="2"/><path d="M5.5 4.5H10V9.1"/><path d="M18.5 4.5H14V9.1"/><path d="M5.5 19.5H10V14.9"/><path d="M18.5 19.5H14V14.9"/></svg></div>{% endif %}
43
57
  {% if not Document.omitted.grid %}<div class="fold-grids" title="fold/unfold all grid diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3.5" width="7" height="7" rx="1"/><rect x="13.5" y="3.5" width="7" height="7" rx="1"/><rect x="3.5" y="13.5" width="7" height="7" rx="1"/><rect x="13.5" y="13.5" width="7" height="7" rx="1"/></svg></div>{% endif %}
58
+ {% if not Document.omitted.state %}<div class="fold-states" title="fold/unfold all state diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="3.5" cy="12" r="2" fill="currentColor"/><path d="M6.5 12h4"/><path d="M8.5 10 10.5 12l-2 2"/><rect x="12.5" y="7" width="9.5" height="10" rx="3"/></svg></div>{% endif %}
59
+ {% if not Document.omitted.layer %}<div class="fold-layers" title="fold/unfold all layer diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3.5" width="17" height="4" rx="1"/><rect x="3.5" y="10" width="17" height="4" rx="1"/><rect x="3.5" y="16.5" width="17" height="4" rx="1"/></svg></div>{% endif %}
60
+ {% if not Document.omitted.slice %}<div class="fold-slices" title="fold/unfold all slice diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3.5" width="4" height="17" rx="1"/><rect x="10" y="3.5" width="4" height="17" rx="1"/><rect x="16.5" y="3.5" width="4" height="17" rx="1"/></svg></div>{% endif %}
61
+ {% if not Document.omitted.tree %}<div class="fold-trees" title="fold/unfold all tree diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2.5" width="6" height="5" rx="1"/><rect x="2.5" y="16.5" width="6" height="5" rx="1"/><rect x="15.5" y="16.5" width="6" height="5" rx="1"/><path d="M12 7.5v4.5"/><path d="M5.5 16.5V12h13v4.5"/></svg></div>{% endif %}
62
+ {% if not Document.omitted.matrix %}<div class="fold-matrices" title="fold/unfold all matrix diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3.5" width="17" height="17" rx="1.5"/><path d="M9.5 3.5v17"/><path d="M3.5 9.5h17"/><path d="M13 13h.01"/><path d="M17 17h.01"/></svg></div>{% endif %}
63
+ {% if not Document.omitted.sequence %}<div class="fold-sequences" title="fold/unfold all sequence diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="2.5" width="7" height="4" rx="1"/><rect x="14.5" y="2.5" width="7" height="4" rx="1"/><path d="M6 6.5v15" stroke-dasharray="1.5 2.5"/><path d="M18 6.5v15" stroke-dasharray="1.5 2.5"/><path d="M6 12h10"/><path d="M14 9.5l2.5 2.5-2.5 2.5"/></svg></div>{% endif %}
64
+ {% if not Document.omitted.petri %}<div class="fold-petris" title="fold/unfold all Petri net diagrams"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6.5" cy="12" r="4.5"/><path d="M6.5 12h.01"/><path d="M11 12h5"/><path d="M14 9.5l2.5 2.5-2.5 2.5"/><path d="M19.5 5.5v13"/></svg></div>{% endif %}
65
+ {% if not Document.omitted.gantt %}<div class="fold-gantts" title="fold/unfold all Gantt charts"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="3.5" width="8" height="4" rx="1"/><rect x="7.5" y="10" width="9" height="4" rx="1"/><rect x="12.5" y="16.5" width="9" height="4" rx="1"/></svg></div>{% endif %}
66
+ </div>
67
+ {%- endif %}
68
+ <div class="fold-row">
44
69
  {% if not Document.omitted.code %}<div class="fold-codes" title="fold/unfold all diagrams as code"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 7l-5 5 5 5"/><path d="M16 7l5 5-5 5"/><path d="M13.5 4l-3 16"/></svg></div>{% endif %}
45
70
  {% if not Document.omitted.image %}<div class="fold-images" title="fold/unfold all images"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="3.5" width="19" height="17" rx="2"/><circle cx="8.5" cy="9" r="2"/><path d="M21.5 15.5l-5-5-11 10"/></svg></div>{% endif %}
46
71
  {% if not Document.omitted.listing %}<div class="fold-listings" title="fold/unfold all code listings"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6h11.5"/><path d="M9 12h11.5"/><path d="M9 18h7"/><path d="M3.5 6h.01"/><path d="M3.5 12h.01"/><path d="M3.5 18h.01"/></svg></div>{% endif %}
47
72
  {% for level in [ 1, 2, 3 ] %}{% if not Document.omitted["level" ~ level] %}<div class="fold-level{{ level }}" title="fold/unfold all diagrams from nesting level {{ level }} on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="2.5" width="11" height="11" rx="1.5"/><rect x="6.5" y="6.5" width="3" height="3" rx="0.5"/><text x="19.5" y="23" text-anchor="middle" font-size="14.5" font-weight="bold" fill="currentColor" stroke="none">{{ level }}</text></svg></div>{% endif %}{% endfor %}
48
73
  {% if not Document.omitted.text %}<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>{% endif %}
74
+ </div>
75
+ </div>
49
76
  </div>
50
77
  </div>
51
78
  {%- endif %}
@@ -56,10 +83,11 @@ const templates = {
56
83
  {{ Document.doc }}
57
84
  {{ Document.artifacts }}
58
85
  {% if Document.search %}<script>{{ Document.search }}</script>{% endif %}
59
- <script>{{ Document.progress }}</script>
60
- <script>{{ Document.fold }}</script>
61
- <script>{{ Document.maximize }}</script>
62
- {% if Document.tocpanel %}<script>{{ Document.tocscript }}</script>{% endif %}
86
+ {% if Document.progress %}<script>{{ Document.progress }}</script>{% endif %}
87
+ {% if Document.fold %}<script>{{ Document.fold }}</script>{% endif %}
88
+ {% if Document.filter %}<script>{{ Document.filter }}</script>{% endif %}
89
+ {% if Document.maximize %}<script>{{ Document.maximize }}</script>{% endif %}
90
+ {% if Document.tocscript %}<script>{{ Document.tocscript }}</script>{% endif %}
63
91
  {% if Document.info %}<script>{{ Document.info }}</script>{% endif %}
64
92
  {% if Document.realtime %}<script class="realtime">{{ Document.realtime }}</script>{% endif %}
65
93
  </body>
@@ -183,10 +211,10 @@ const templates = {
183
211
  "Description": textframe `
184
212
  {%- if Description.block %}
185
213
  <div class="description">{{ Description.description }}{% if Description.rationale %}
186
- <p class="rationale">&mdash; <span class="keyword">BECAUSE</span> {{ Description.rationale }}</p>{% endif %}</div>
214
+ <p class="rationale">&mdash; <span class="keyword">BECAUSE</span>{% if Description.keyword %} ({{ Description.keyword }}){% endif %} {{ Description.rationale }}</p>{% endif %}</div>
187
215
  {%- elif Description.description or Description.rationale %}
188
216
  <p class="description">{{ Description.description }}{% if Description.rationale %}
189
- <span class="rationale">&mdash; <span class="keyword">BECAUSE</span> {{ Description.rationale }}</span>{% endif %}</p>
217
+ <span class="rationale">&mdash; <span class="keyword">BECAUSE</span>{% if Description.keyword %} ({{ Description.keyword }}){% endif %} {{ Description.rationale }}</span>{% endif %}</p>
190
218
  {%- endif %}
191
219
  {%- if Description.elaboration %}
192
220
  <div class="description">{{ Description.elaboration }}</div>
@@ -201,8 +229,8 @@ const templates = {
201
229
  <thead>
202
230
  <tr>
203
231
  <th class="object-kind"><span{% if Table.info %} data-info="{{ Table.info }}" data-info-path="{{ Table.infopath }}"{% endif %}>{{ Table.head }}</span></th>
204
- {%- for key in Table.keys %}<th class="property-name"><span{% if Table.info %} data-info="{{ Table.info }}" data-info-path="{{ Table.infopath }}" data-info-prop="{{ key }}"{% endif %}>{{ key }}</span></th>{% endfor %}
205
- {%- if Table.desc %}<th class="description" style="width: {{ Table.width }}%">Description</th>{% endif %}
232
+ {%- for key in Table.keys %}<th class="property-name" style="width: {{ Table.widths[loop.index0] }}%"><span{% if Table.info %} data-info="{{ Table.info }}" data-info-path="{{ Table.infopath }}" data-info-prop="{{ key }}"{% endif %}>{{ key }}</span></th>{% endfor %}
233
+ {%- if Table.desc %}<th class="description" style="width: {{ Table.widths[Table.keys.length] }}%">Description</th>{% endif %}
206
234
  </tr>
207
235
  </thead>
208
236
  <tbody>
@@ -231,6 +259,7 @@ const templates = {
231
259
  <td><span{% if row.spec %} data-info-spec="{{ row.spec }}"{% endif %}>{{ row.name }}</span>{% if row.primary %} <span class="primary-marker">&#x2318;</span>{% endif %}{% if row.paren %} <span class="anchor-paren">({{ row.paren }})</span>{% endif %} <a href="#{{ row.id }}"><span class="anchor-symbol">&#x2693;&#xFE0E;</span></a></td>
232
260
  <td class="chunks">
233
261
  <table class="chunks">
262
+ <colgroup>{% for width in Table.widths %}<col style="width: {{ width }}%">{% endfor %}</colgroup>
234
263
  {% for chunk in row.chunks %}
235
264
  <tr>{% for cell in chunk %}<th{% if not cell.desc %} class="property-name"{% endif %}{% if cell.span > 1 %} colspan="{{ cell.span }}"{% endif %}><span{% if not cell.desc and Table.info %} data-info="{{ Table.info }}" data-info-path="{{ row.spec }}" data-info-prop="{{ cell.key }}"{% endif %}>{{ cell.key }}</span></th>{% endfor %}</tr>
236
265
  <tr>{% for cell in chunk %}<td{% if cell.span > 1 %} colspan="{{ cell.span }}"{% endif %}>{{ cell.value }}</td>{% endfor %}</tr>
@@ -4,6 +4,8 @@ export declare const realtimeScript: string;
4
4
  export declare const scrollProgressScript: string;
5
5
  export declare const maximizeScript: string;
6
6
  export declare const tocPanelScript: string;
7
+ export type FilterEntry = [parent: number, segment: string, targets: number[]];
8
+ export declare const filterScript: (links: FilterEntry[]) => string;
7
9
  export type InfoEntry = {
8
10
  d?: string;
9
11
  p?: Record<string, string>;
@@ -235,10 +235,10 @@ export const maximizeScript = textframe `
235
235
  .find((el) => el.getClientRects().length > 0)
236
236
  if (shown === undefined)
237
237
  return
238
- const svg = shown.cloneNode(true)
239
- if (svg.tagName !== "PRE")
240
- svg.removeAttribute("style")
241
- content.replaceChildren(svg)
238
+ const clone = shown.cloneNode(true)
239
+ if (clone.tagName !== "PRE")
240
+ clone.removeAttribute("style")
241
+ content.replaceChildren(clone)
242
242
  overlay.classList.add("open")
243
243
  document.documentElement.classList.add("maximized")
244
244
  if (fullscreen)
@@ -323,7 +323,7 @@ export const tocPanelScript = textframe `
323
323
  }
324
324
  let current = null
325
325
  for (let i = 0; i < targets.length; i++) {
326
- if (targets[i] === null)
326
+ if (targets[i] === null || targets[i].getClientRects().length === 0)
327
327
  continue
328
328
  const top = targets[i].getBoundingClientRect().top
329
329
  if (top >= -1 && top < window.innerHeight) {
@@ -359,8 +359,8 @@ export const tocPanelScript = textframe `
359
359
  anchored.classList.add("anchored")
360
360
  }
361
361
  const toggle = (open) => {
362
- panel.classList.toggle("open", open)
363
- try { localStorage.setItem("specbook-toc", panel.classList.contains("open") ? "open" : "closed") }
362
+ const opened = panel.classList.toggle("open", open)
363
+ try { localStorage.setItem("specbook-toc", opened ? "open" : "closed") }
364
364
  catch { /* an inaccessible storage just loses the state */ }
365
365
  }
366
366
  try { if (localStorage.getItem("specbook-toc") === "open") panel.classList.add("open") }
@@ -375,6 +375,7 @@ export const tocPanelScript = textframe `
375
375
  && event.target.closest("div.search") === null
376
376
  && event.target.closest("div.theme-switch") === null
377
377
  && event.target.closest("div.info-switch") === null
378
+ && event.target.closest("div.filter-switch") === null
378
379
  && event.target.closest("div.fold-switch") === null
379
380
  && event.target.closest("div.realtime-status") === null)
380
381
  toggle(false)
@@ -445,12 +446,11 @@ const infoPopupScript = textframe `
445
446
  id trailing an instance popup, and the property name trailing
446
447
  a property popup */
447
448
  const titleOf = (source, spec, prop) => {
448
- /* compose the title path segments of an object */
449
+ /* compose the title path segments of an object, each a pair
450
+ of its kind and its name (empty for a kind-only segment) */
449
451
  const pathOf = (n, named) => {
450
452
  const [ , up, kind, title ] = SPEC[n]
451
- const name = named ? title : ""
452
- return [ ...(up >= 0 ? pathOf(up, true) : []),
453
- name !== "" && kind !== "" ? kind + ": " + name : (name !== "" ? name : kind) ]
453
+ return [ ...(up >= 0 ? pathOf(up, true) : []), [ kind, named ? title : "" ] ]
454
454
  }
455
455
  const title = document.createElement("div")
456
456
  title.className = "info-title"
@@ -465,7 +465,7 @@ const infoPopupScript = textframe `
465
465
  const ref = spec ?? source.getAttribute("data-info-path") ?? ""
466
466
  const index = parseInt(ref, 10)
467
467
  const segments = SPEC[index] !== undefined ? pathOf(index, !ref.endsWith("^")) : []
468
- for (const [ i, segment ] of segments.entries()) {
468
+ for (const [ i, [ kindText, nameText ] ] of segments.entries()) {
469
469
  if (i > 0)
470
470
  title.appendChild(pointer())
471
471
  const path = document.createElement("span")
@@ -476,22 +476,21 @@ const infoPopupScript = textframe `
476
476
  description color), where a segment without a kind
477
477
  prefix is the kind-only ending of a kind popup or the
478
478
  name of a kind-less object */
479
- const m = /^([^\\s:]+): (.*)$/.exec(segment)
480
479
  const bare = i === segments.length - 1 && spec === null && prop === null
481
480
  const name = document.createElement("span")
482
481
  name.className = "info-name"
483
- if (m !== null || bare) {
482
+ if (kindText !== "" && (nameText !== "" || bare)) {
484
483
  const kind = document.createElement("span")
485
484
  kind.className = "info-kind"
486
- kind.textContent = m !== null ? m[1] + ":" : segment
485
+ kind.textContent = nameText !== "" ? kindText + ":" : kindText
487
486
  path.appendChild(kind)
488
- if (m !== null) {
489
- name.textContent = m[2]
487
+ if (nameText !== "") {
488
+ name.textContent = nameText
490
489
  path.append(" ", name)
491
490
  }
492
491
  }
493
492
  else {
494
- name.textContent = segment
493
+ name.textContent = nameText !== "" ? nameText : kindText
495
494
  path.appendChild(name)
496
495
  }
497
496
  title.appendChild(path)
@@ -583,7 +582,10 @@ const infoPopupScript = textframe `
583
582
  return
584
583
  cancel()
585
584
  pending = el
586
- pendingId = setTimeout(() => { pending = null; show(el) }, 400)
585
+ pendingId = setTimeout(() => {
586
+ pending = null
587
+ show(el)
588
+ }, 400)
587
589
  })
588
590
  document.body.addEventListener("mouseout", (event) => {
589
591
  const el = event.target.closest(targets)
@@ -596,6 +598,204 @@ const infoPopupScript = textframe `
596
598
  })
597
599
  })(@INFO@, @SPEC@)
598
600
  `;
601
+ /* the client-side script of the artifact filter: the filter tab
602
+ slides a grid out, holding one row per artifact kind -- the kind
603
+ cell first, then one cell per top-level artifact of the kind --,
604
+ whose cells are sticky checkboxes (persisted across page loads as
605
+ the anchor paths of the enabled artifacts), a kind cell toggling
606
+ all artifacts of its row. Every enabled artifact is an origin shown
607
+ entirely, while in all other artifacts only the objects stay
608
+ visible which the closure reaches from the origin objects through
609
+ the outgoing references (the own references of a reached object
610
+ continuing it, not those of its hidden children), each shown
611
+ without its child objects, plus the headings of its enclosing
612
+ objects; the table of contents (page and side panel) is filtered
613
+ along and the diagram of contents hidden. The closure is computed
614
+ from the injected LINKS table (per object: parent index, anchor
615
+ path segment, and referenced object indices), from which the anchor
616
+ paths identifying the DOM elements are composed. A hyperlink whose
617
+ target the filter hid clears the filter before it jumps. The script
618
+ runs at the end of the body, as the content it hides has to exist
619
+ already, and a live preview body swap replaces the tab and its
620
+ listeners along with the body */
621
+ const artifactFilterScript = textframe `
622
+ (function (LINKS) {
623
+ const tab = document.querySelector("div.filter-switch")
624
+ if (tab === null)
625
+ return
626
+
627
+ /* let the filter icon slide the grid out of the tab (and
628
+ back in again), remembering the choice across page loads */
629
+ try { if (localStorage.getItem("specbook-filter") === "open") tab.classList.add("open") }
630
+ catch { /* an inaccessible storage just means no stored state */ }
631
+ tab.querySelector("div.filter-toggle").addEventListener("click", () => {
632
+ const opened = tab.classList.toggle("open")
633
+ try { localStorage.setItem("specbook-filter", opened ? "open" : "closed") }
634
+ catch { /* an inaccessible storage just loses the state */ }
635
+ })
636
+
637
+ /* the anchor path of every object (composed from the segments
638
+ along its ancestor chain), the object index per path, and
639
+ the top-level object (the artifact) of every object */
640
+ const paths = []
641
+ const roots = []
642
+ const byPath = new Map()
643
+ LINKS.forEach(([ parent, segment ], n) => {
644
+ paths[n] = parent >= 0 ? paths[parent] + "." + segment : segment
645
+ roots[n] = parent >= 0 ? roots[parent] : n
646
+ byPath.set(paths[n], n)
647
+ })
648
+
649
+ /* the grid rows: the kind cell leading a row toggles the
650
+ artifact cells following it, each identified by the anchor
651
+ path (and hence the index) of its top-level object */
652
+ const rows = []
653
+ const cells = new Map()
654
+ for (const el of tab.querySelector("div.filter-grid").children) {
655
+ if (el.classList.contains("filter-kind"))
656
+ rows.push({ kind: el, cells: [] })
657
+ else {
658
+ const n = byPath.get(el.getAttribute("data-id"))
659
+ if (n === undefined || rows.length === 0)
660
+ continue
661
+ cells.set(n, el)
662
+ rows[rows.length - 1].cells.push(n)
663
+ }
664
+ }
665
+
666
+ /* the active origins (the indices of the enabled artifacts),
667
+ persisted across page loads as the list of their anchor paths */
668
+ const active = new Set()
669
+ const store = () => {
670
+ try { localStorage.setItem("specbook-filter-origins", Array.from(active).map((n) => paths[n]).join(",")) }
671
+ catch { /* an inaccessible storage just loses the state */ }
672
+ }
673
+ try {
674
+ for (const path of (localStorage.getItem("specbook-filter-origins") ?? "").split(",")) {
675
+ const n = byPath.get(path)
676
+ if (n !== undefined && cells.has(n))
677
+ active.add(n)
678
+ }
679
+ }
680
+ catch { /* an inaccessible storage just means no stored state */ }
681
+
682
+ /* apply the active origins onto the cells and the content */
683
+ const apply = () => {
684
+ document.querySelectorAll(".filter-hide").forEach((el) => { el.classList.remove("filter-hide") })
685
+ document.body.classList.toggle("filtering", active.size > 0)
686
+ for (const [ n, cell ] of cells)
687
+ cell.classList.toggle("on", active.has(n))
688
+ for (const row of rows) {
689
+ const count = row.cells.filter((n) => active.has(n)).length
690
+ row.kind.classList.toggle("on", count > 0 && count === row.cells.length)
691
+ row.kind.classList.toggle("some", count > 0 && count !== row.cells.length)
692
+ }
693
+ if (active.size === 0)
694
+ return
695
+
696
+ /* the visibility state of every object: the objects of the
697
+ origin artifacts are shown entirely (3), the objects the
698
+ closure reaches from them through the own references of
699
+ a reached object are shown without their children (2),
700
+ the ancestors of those as headings only (1), and all
701
+ other objects are hidden (0) */
702
+ const state = LINKS.map((_, n) => active.has(roots[n]) ? 3 : 0)
703
+ const queue = LINKS.map((_, n) => n).filter((n) => state[n] === 3)
704
+ for (let i = 0; i < queue.length; i++) {
705
+ for (const target of LINKS[queue[i]][2]) {
706
+ if (state[target] >= 2)
707
+ continue
708
+ state[target] = 2
709
+ queue.push(target)
710
+ for (let p = LINKS[target][0]; p >= 0 && state[p] === 0; p = LINKS[p][0])
711
+ state[p] = 1
712
+ }
713
+ }
714
+
715
+ /* hide a hidden object entirely (its section or table row)
716
+ and the own content of a heading-only one (everything in
717
+ its section but the heading and the child containers,
718
+ whose objects carry states of their own), plus the tables
719
+ and articles which ran entirely empty in the process */
720
+ state.forEach((s, n) => {
721
+ if (s >= 2)
722
+ return
723
+ const el = document.getElementById(paths[n])
724
+ if (el === null)
725
+ return
726
+ const box = el.tagName === "TR" ? el : el.closest("section")
727
+ if (box === null)
728
+ return
729
+ if (s === 0)
730
+ box.classList.add("filter-hide")
731
+ else if (box !== el)
732
+ for (const child of box.children)
733
+ if (child !== el && child.tagName !== "SECTION" && !child.classList.contains("objects"))
734
+ child.classList.add("filter-hide")
735
+ })
736
+ document.querySelectorAll("article table.objects").forEach((table) => {
737
+ if (table.querySelector(":scope > tbody > tr:not(.filter-hide)") === null)
738
+ table.classList.add("filter-hide")
739
+ })
740
+ document.querySelectorAll("article").forEach((article) => {
741
+ if (article.querySelector(":scope > section:not(.filter-hide)") === null)
742
+ article.classList.add("filter-hide")
743
+ })
744
+
745
+ /* filter the table of contents (its page rows and its side
746
+ panel entries) along: an entry stays visible only while
747
+ its target still is (the diagram of contents, spanning
748
+ all artifacts, is hidden by the stylesheet) */
749
+ document.querySelectorAll("nav.toc table tr, nav.toc-panel div.toc-list li").forEach((entry) => {
750
+ const link = entry.querySelector(":scope > a, :scope > td > a")
751
+ const target = link !== null ?
752
+ document.getElementById(decodeURIComponent(link.hash.slice(1))) : null
753
+ if (target !== null && (target.closest(".filter-hide") !== null || target.closest("nav.doc") !== null))
754
+ entry.classList.add("filter-hide")
755
+ })
756
+ }
757
+ for (const [ n, cell ] of cells)
758
+ cell.addEventListener("click", () => {
759
+ if (!active.delete(n))
760
+ active.add(n)
761
+ store()
762
+ apply()
763
+ })
764
+ for (const row of rows)
765
+ row.kind.addEventListener("click", () => {
766
+ const all = row.cells.every((n) => active.has(n))
767
+ for (const n of row.cells) {
768
+ if (all)
769
+ active.delete(n)
770
+ else
771
+ active.add(n)
772
+ }
773
+ store()
774
+ apply()
775
+ })
776
+
777
+ /* let a hyperlink whose target the filter hid clear the filter
778
+ before it jumps, so the browser performs the regular jump
779
+ onto the revealed target */
780
+ document.body.addEventListener("click", (event) => {
781
+ const link = event.target.closest("a[href^='#']")
782
+ if (link === null || active.size === 0)
783
+ return
784
+ const target = document.getElementById(
785
+ decodeURIComponent((link.getAttribute("href") ?? "").slice(1)))
786
+ if (target === null || target.closest(".filter-hide") === null)
787
+ return
788
+ active.clear()
789
+ store()
790
+ apply()
791
+ })
792
+ apply()
793
+ })(@LINKS@)
794
+ `;
795
+ /* inject the reference table of the artifact filter into its
796
+ client-side script ("<" escaped, so no embedded text can close the
797
+ surrounding <script> element) */
798
+ export const filterScript = (links) => artifactFilterScript.replace("@LINKS@", () => JSON.stringify(links).replace(/</g, "\\u003c"));
599
799
  /* inject the description popup tables into their client-side script ("<"
600
800
  escaped, so no embedded HTML can close the surrounding <script>
601
801
  element, and the trailing table first, so a placeholder text