@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.
- package/LICENSE.txt +201 -0
- package/README.md +288 -43
- package/dst/specbook-api.d.ts +23 -3
- package/dst/specbook-api.js +133 -59
- package/dst/specbook-cli.js +93 -26
- package/dst/specbook-cmd-describe.js +4 -4
- package/dst/specbook-cmd-export-schema.d.ts +8 -0
- package/dst/specbook-cmd-export-schema.js +441 -0
- package/dst/specbook-cmd-export.js +23 -6
- package/dst/specbook-cmd-init-sample.d.ts +4 -0
- package/dst/specbook-cmd-init-sample.js +322 -0
- package/dst/specbook-cmd-init.d.ts +1 -0
- package/dst/specbook-cmd-init.js +42 -10
- package/dst/specbook-cmd-lint.js +46 -48
- package/dst/specbook-cmd-query.d.ts +30 -0
- package/dst/specbook-cmd-query.js +165 -0
- package/dst/specbook-config.d.ts +9 -1
- package/dst/specbook-config.js +135 -10
- package/dst/specbook-coverage.js +2 -2
- package/dst/specbook-diagram.d.ts +7 -5
- package/dst/specbook-diagram.js +546 -143
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +11 -8
- package/dst/specbook-export-code.js +3 -3
- package/dst/specbook-export-common.d.ts +1 -1
- package/dst/specbook-export-common.js +3 -2
- package/dst/specbook-export-diagram.d.ts +1 -1
- package/dst/specbook-export-diagram.js +25 -7
- package/dst/specbook-export-html-fold.d.ts +16 -6
- package/dst/specbook-export-html-fold.js +65 -44
- package/dst/specbook-export-html-render.js +107 -26
- package/dst/specbook-export-html-search.js +18 -20
- package/dst/specbook-export-html-template.js +38 -9
- package/dst/specbook-export-html-widget.d.ts +2 -0
- package/dst/specbook-export-html-widget.js +219 -19
- package/dst/specbook-export-html.css +208 -38
- package/dst/specbook-export-html.js +96 -53
- package/dst/specbook-export-md.d.ts +2 -1
- package/dst/specbook-export-md.js +7 -8
- package/dst/specbook-export-pdf.js +1 -1
- package/dst/specbook-files.d.ts +2 -0
- package/dst/specbook-files.js +41 -0
- package/dst/specbook-format-meta.yaml +401 -0
- package/dst/specbook-format-pm.yaml +1240 -0
- package/dst/specbook-format-schema.d.ts +33 -3
- package/dst/specbook-format-schema.js +49 -3
- package/dst/specbook-format.md +427 -47
- package/dst/specbook-format.yaml +524 -228
- package/dst/specbook-ignore.js +2 -2
- package/dst/specbook-link.d.ts +4 -0
- package/dst/specbook-link.js +61 -6
- package/dst/specbook-mcp.js +103 -19
- package/dst/specbook-parse-semantic.d.ts +1 -0
- package/dst/specbook-parse-semantic.js +11 -4
- package/dst/specbook-parse-syntax.d.ts +1 -0
- package/dst/specbook-parse-syntax.js +1 -1
- package/dst/specbook-parse.d.ts +1 -1
- package/dst/specbook-parse.js +3 -3
- package/dst/specbook-project.d.ts +2 -0
- package/dst/specbook-project.js +15 -5
- package/dst/specbook-remote.d.ts +28 -0
- package/dst/specbook-remote.js +429 -0
- 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 (
|
|
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",
|
|
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">×</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-
|
|
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.
|
|
62
|
-
{% if Document.
|
|
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">— <span class="keyword">BECAUSE</span> {{ Description.rationale }}</p>{% endif %}</div>
|
|
214
|
+
<p class="rationale">— <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">— <span class="keyword">BECAUSE</span> {{ Description.rationale }}</span>{% endif %}</p>
|
|
217
|
+
<span class="rationale">— <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.
|
|
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">⌘</span>{% endif %}{% if row.paren %} <span class="anchor-paren">({{ row.paren }})</span>{% endif %} <a href="#{{ row.id }}"><span class="anchor-symbol">⚓︎</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
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
content.replaceChildren(
|
|
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",
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
482
|
+
if (kindText !== "" && (nameText !== "" || bare)) {
|
|
484
483
|
const kind = document.createElement("span")
|
|
485
484
|
kind.className = "info-kind"
|
|
486
|
-
kind.textContent =
|
|
485
|
+
kind.textContent = nameText !== "" ? kindText + ":" : kindText
|
|
487
486
|
path.appendChild(kind)
|
|
488
|
-
if (
|
|
489
|
-
name.textContent =
|
|
487
|
+
if (nameText !== "") {
|
|
488
|
+
name.textContent = nameText
|
|
490
489
|
path.append(" ", name)
|
|
491
490
|
}
|
|
492
491
|
}
|
|
493
492
|
else {
|
|
494
|
-
name.textContent =
|
|
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(() => {
|
|
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
|