@rse/specbook 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +124 -204
- package/dst/specbook-cmd-export.js +4 -1
- package/dst/specbook-config.js +6 -6
- package/dst/specbook-diagram.js +154 -22
- package/dst/specbook-export-ast.js +1 -1
- package/dst/specbook-export-common.js +1 -1
- package/dst/specbook-export-html.css +178 -5
- package/dst/specbook-export-html.d.ts +1 -1
- package/dst/specbook-export-html.js +304 -42
- package/dst/specbook-export-md.js +9 -9
- package/dst/specbook-export-pdf.js +37 -13
- package/dst/specbook-format-schema.d.ts +11 -1
- package/dst/specbook-format-schema.js +12 -2
- package/dst/specbook-format-spec.d.ts +1 -1
- package/dst/specbook-format-spec.js +2 -2
- package/dst/specbook-format.md +80 -9
- package/dst/specbook-format.yaml +294 -197
- package/dst/specbook-link.js +2 -2
- package/dst/specbook-parse-semantic.js +22 -22
- package/dst/specbook-parse-syntax.js +5 -5
- package/dst/specbook-parse.js +1 -1
- package/package.json +6 -6
package/dst/specbook-diagram.js
CHANGED
|
@@ -40,26 +40,47 @@ const propValue = (parenProps, object, key) => propsOf(parenProps, object).find(
|
|
|
40
40
|
"#type" directive and the configured "#config" directives so every
|
|
41
41
|
embedded spec is self-contained: one node statement per object (the
|
|
42
42
|
unique anchor path as the node id, the object name as the displayed
|
|
43
|
-
label) and one edge statement per
|
|
44
|
-
|
|
43
|
+
label, the container node as the parent) and one edge statement per
|
|
44
|
+
derived edge */
|
|
45
|
+
const renderSpec = (diagram, type, center, centerUrl, nodes, edges, nesting, index, anchors, positions, parenProps) => {
|
|
45
46
|
const lines = [`#type ${type}`];
|
|
46
47
|
const config = diagram.config ?? {};
|
|
47
48
|
for (const [key, value] of Object.entries(config))
|
|
48
49
|
if (value !== undefined)
|
|
49
50
|
lines.push(`#config ${key} ${configValue(value)}`);
|
|
50
51
|
lines.push("");
|
|
52
|
+
/* the explicit top-down order of an "ordered" diagram: the members
|
|
53
|
+
of a container share one row (which Gradia wraps), while the
|
|
54
|
+
top-level nodes stack in document order, a synthetic container
|
|
55
|
+
taking the position of its first member */
|
|
56
|
+
const orderOf = (node) => {
|
|
57
|
+
if (nesting.parentOf.has(node))
|
|
58
|
+
return 0;
|
|
59
|
+
let order = positions.get(node) ?? Number.MAX_SAFE_INTEGER;
|
|
60
|
+
for (const [member, container] of nesting.parentOf)
|
|
61
|
+
if (container === node)
|
|
62
|
+
order = Math.min(order, positions.get(member) ?? order);
|
|
63
|
+
return order;
|
|
64
|
+
};
|
|
51
65
|
for (const node of nodes) {
|
|
52
66
|
const anchor = anchors.get(node);
|
|
53
67
|
const attrs = new Array();
|
|
54
|
-
/* a synthetic center node (the only
|
|
55
|
-
path) links to its source object, or nowhere at all */
|
|
56
|
-
const url = anchor !== undefined ? `#${anchor}` : centerUrl;
|
|
68
|
+
/* a synthetic center or container node (the only nodes without
|
|
69
|
+
an anchor path) links to its source object, or nowhere at all */
|
|
70
|
+
const url = anchor !== undefined ? `#${anchor}` : (node === center ? centerUrl : undefined);
|
|
57
71
|
if (url !== undefined)
|
|
58
72
|
attrs.push(`url: ${atom(url)}`);
|
|
59
73
|
if (diagram.qualified === true && node.kind !== "")
|
|
60
74
|
attrs.push(`type: ${atom(node.kind)}`);
|
|
61
75
|
if (type === "hub" ? node === center : node.primary === true)
|
|
62
76
|
attrs.push("primary: true");
|
|
77
|
+
const container = nesting.parentOf.get(node);
|
|
78
|
+
if (container !== undefined)
|
|
79
|
+
attrs.push(`parent: ${atom(anchors.get(container) ?? container.id)}`);
|
|
80
|
+
if (diagram.nest?.layout !== undefined && nesting.containers.has(node))
|
|
81
|
+
attrs.push(`container: ${diagram.nest.layout}`);
|
|
82
|
+
if (diagram.ordered === true)
|
|
83
|
+
attrs.push(`order: ${orderOf(node)}`);
|
|
63
84
|
/* attach the values of the configured properties as Gradia
|
|
64
85
|
key/value attributes (a node lacking a property is fine, as
|
|
65
86
|
the node set can mix objects of different kinds), with every
|
|
@@ -89,13 +110,13 @@ const renderSpec = (diagram, type, center, centerUrl, nodes, edges, index, ancho
|
|
|
89
110
|
/* derive the reference edges of a diagram from the "[[...]]" references
|
|
90
111
|
of the node objects (per the "links" selection, named with the
|
|
91
112
|
property key when "labeled", and for a "deep" diagram also of their
|
|
92
|
-
descendants, with
|
|
93
|
-
|
|
113
|
+
descendants, with the reference count as the arity), with every
|
|
114
|
+
target lifted to its nearest node */
|
|
94
115
|
const deriveReferenceEdges = (diagram, nodes, nodeSet, index, anchors, parents, parenProps) => {
|
|
95
116
|
const edges = new Array();
|
|
96
117
|
/* lift an object to its nearest ancestor-or-self within the
|
|
97
|
-
node set, as a
|
|
98
|
-
the
|
|
118
|
+
node set, as a reference onto a descendant of a node object
|
|
119
|
+
(like the scenario of a use case) is a reference onto the node */
|
|
99
120
|
const lift = (object) => {
|
|
100
121
|
while (object !== undefined && !nodeSet.has(object))
|
|
101
122
|
object = parents.get(object);
|
|
@@ -109,7 +130,7 @@ const deriveReferenceEdges = (diagram, nodes, nodeSet, index, anchors, parents,
|
|
|
109
130
|
const walk = (object) => {
|
|
110
131
|
objects.push(object);
|
|
111
132
|
if (diagram.deep === true)
|
|
112
|
-
for (const child of object.
|
|
133
|
+
for (const child of object.children)
|
|
113
134
|
if (!nodeSet.has(child))
|
|
114
135
|
walk(child);
|
|
115
136
|
};
|
|
@@ -127,9 +148,7 @@ const deriveReferenceEdges = (diagram, nodes, nodeSet, index, anchors, parents,
|
|
|
127
148
|
}
|
|
128
149
|
for (const { text, name } of texts)
|
|
129
150
|
for (const m of text.matchAll(referenceRegex)) {
|
|
130
|
-
|
|
131
|
-
if (diagram.deep === true)
|
|
132
|
-
target = lift(target);
|
|
151
|
+
const target = lift(resolveUnique(index, m[1].trim(), object).target);
|
|
133
152
|
if (target !== undefined && target !== node && nodeSet.has(target)) {
|
|
134
153
|
const key = `${anchors.get(target) ?? target.id}\u0000${name ?? ""}`;
|
|
135
154
|
const entry = counts.get(key) ?? { target, name, count: 0 };
|
|
@@ -310,7 +329,7 @@ const deriveCenter = (object, diagram, nodes, nodeSet, index, anchors, parenProp
|
|
|
310
329
|
/* inject the synthetic center node into the node set, with a
|
|
311
330
|
collision-free id ("@" never occurs in an anchor path) */
|
|
312
331
|
const center = { kind: centerCfg.kind ?? "",
|
|
313
|
-
id: `${anchors.get(object) ?? object.id}-@center`, name: label, properties: [],
|
|
332
|
+
id: `${anchors.get(object) ?? object.id}-@center`, name: label, properties: [], children: [] };
|
|
314
333
|
nodes.unshift(center);
|
|
315
334
|
nodeSet.add(center);
|
|
316
335
|
return { center, centerUrl };
|
|
@@ -347,10 +366,104 @@ const deriveCenterEdges = (cfg, nodes, center, index, parenProps, errors) => {
|
|
|
347
366
|
}
|
|
348
367
|
return edges;
|
|
349
368
|
};
|
|
369
|
+
/* derive the nesting of the nodes into container nodes: per node the
|
|
370
|
+
first configured property carrying exactly one resolvable reference
|
|
371
|
+
names its container (added to the node set when absent, and nested
|
|
372
|
+
on its own in turn), else its parent object when part of the node
|
|
373
|
+
set, else a synthetic container per object kind; a node nested into
|
|
374
|
+
itself or into a cycle stays un-nested and is reported */
|
|
375
|
+
const deriveNesting = (nest, nodes, nodeSet, index, parents, parenProps, errors) => {
|
|
376
|
+
const parentOf = new Map();
|
|
377
|
+
const byKind = new Map();
|
|
378
|
+
/* the iteration also visits the containers appended underway */
|
|
379
|
+
for (const node of nodes) {
|
|
380
|
+
let container;
|
|
381
|
+
for (const key of nest.properties ?? []) {
|
|
382
|
+
const value = propValue(parenProps, node, key);
|
|
383
|
+
const refs = value !== undefined ? Array.from(value.matchAll(referenceRegex)) : [];
|
|
384
|
+
if (refs.length !== 1)
|
|
385
|
+
continue;
|
|
386
|
+
container = resolveUnique(index, refs[0][1].trim(), node).target;
|
|
387
|
+
if (container !== undefined)
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
if (container === undefined && nest.parent === true) {
|
|
391
|
+
const parent = parents.get(node);
|
|
392
|
+
if (parent !== undefined && nodeSet.has(parent))
|
|
393
|
+
container = parent;
|
|
394
|
+
}
|
|
395
|
+
if (container === undefined && nest.kind === true && node.kind !== "") {
|
|
396
|
+
container = byKind.get(node.kind);
|
|
397
|
+
if (container === undefined) {
|
|
398
|
+
container = { kind: "", id: `@kind-${node.kind}`, name: node.kind, properties: [], children: [] };
|
|
399
|
+
byKind.set(node.kind, container);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (container === undefined)
|
|
403
|
+
continue;
|
|
404
|
+
/* a container chain leading back to the node would close a
|
|
405
|
+
cycle, which Gradia rejects exactly like a self-nesting */
|
|
406
|
+
let ancestor = container;
|
|
407
|
+
while (ancestor !== undefined && ancestor !== node)
|
|
408
|
+
ancestor = parentOf.get(ancestor);
|
|
409
|
+
if (ancestor === node) {
|
|
410
|
+
errors.push({ reason: `node "${plainText(node.name)}" is nested into itself` });
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
parentOf.set(node, container);
|
|
414
|
+
if (!nodeSet.has(container)) {
|
|
415
|
+
nodes.push(container);
|
|
416
|
+
nodeSet.add(container);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { parentOf, containers: new Set(parentOf.values()) };
|
|
420
|
+
};
|
|
421
|
+
/* lift the edges crossing a container boundary onto the containers:
|
|
422
|
+
an end is replaced by its outermost ancestor container not enclosing
|
|
423
|
+
the other end as well (the target end only, or both ends), and the
|
|
424
|
+
edges coinciding afterwards merge into one, with their arities summed
|
|
425
|
+
where numeric, so a bundle of references into a container becomes a
|
|
426
|
+
single edge onto it */
|
|
427
|
+
const liftEdges = (edges, parentOf, mode, anchors) => {
|
|
428
|
+
const ancestors = (object) => {
|
|
429
|
+
const chain = new Array();
|
|
430
|
+
for (let p = parentOf.get(object); p !== undefined; p = parentOf.get(p))
|
|
431
|
+
chain.push(p);
|
|
432
|
+
return chain;
|
|
433
|
+
};
|
|
434
|
+
const lift = (end, other) => {
|
|
435
|
+
const enclosing = new Set([other, ...ancestors(other)]);
|
|
436
|
+
let lifted = end;
|
|
437
|
+
for (const ancestor of ancestors(end)) {
|
|
438
|
+
if (enclosing.has(ancestor))
|
|
439
|
+
break;
|
|
440
|
+
lifted = ancestor;
|
|
441
|
+
}
|
|
442
|
+
return lifted;
|
|
443
|
+
};
|
|
444
|
+
const merged = new Map();
|
|
445
|
+
for (const edge of edges) {
|
|
446
|
+
const target = lift(edge.target, edge.source);
|
|
447
|
+
const source = mode === "both" ? lift(edge.source, edge.target) : edge.source;
|
|
448
|
+
if (source === target)
|
|
449
|
+
continue;
|
|
450
|
+
const key = `${anchors.get(source) ?? source.id}${anchors.get(target) ?? target.id}` +
|
|
451
|
+
`${edge.name ?? ""}`;
|
|
452
|
+
const known = merged.get(key);
|
|
453
|
+
if (known === undefined)
|
|
454
|
+
merged.set(key, { source, target, name: edge.name, arity: edge.arity });
|
|
455
|
+
else if (known.arity !== undefined && edge.arity !== undefined
|
|
456
|
+
&& /^\d+$/.test(known.arity) && /^\d+$/.test(edge.arity))
|
|
457
|
+
known.arity = String(Number(known.arity) + Number(edge.arity));
|
|
458
|
+
else if (known.arity !== edge.arity)
|
|
459
|
+
known.arity = undefined;
|
|
460
|
+
}
|
|
461
|
+
return Array.from(merged.values());
|
|
462
|
+
};
|
|
350
463
|
/* derive the Gradia spec of a single object from its "diagram:"
|
|
351
464
|
schema configuration (the returned spec is absent whenever the
|
|
352
465
|
configured diagram situation is invalid) */
|
|
353
|
-
const deriveDiagram = (object, diagram, index, anchors, parents, parenProps) => {
|
|
466
|
+
const deriveDiagram = (object, diagram, index, anchors, positions, parents, parenProps) => {
|
|
354
467
|
const type = diagram.type ?? "graph";
|
|
355
468
|
const errors = new Array();
|
|
356
469
|
/* determine the node objects (default: the current object plus
|
|
@@ -363,7 +476,7 @@ const deriveDiagram = (object, diagram, index, anchors, parents, parenProps) =>
|
|
|
363
476
|
nodes = new Array();
|
|
364
477
|
const walk = (o) => {
|
|
365
478
|
nodes.push(o);
|
|
366
|
-
o.
|
|
479
|
+
o.children.forEach(walk);
|
|
367
480
|
};
|
|
368
481
|
walk(object);
|
|
369
482
|
}
|
|
@@ -418,15 +531,32 @@ const deriveDiagram = (object, diagram, index, anchors, parents, parenProps) =>
|
|
|
418
531
|
(edges.some((edge) => edge.source === center) ? 1 : 0);
|
|
419
532
|
}
|
|
420
533
|
}
|
|
534
|
+
/* nest the nodes into container nodes (except in a "hub" diagram,
|
|
535
|
+
where every container would need a primary node of its own),
|
|
536
|
+
dropping the edges between a container and its own members,
|
|
537
|
+
which Gradia rejects, as the nesting already shows them */
|
|
538
|
+
let nesting = { parentOf: new Map(), containers: new Set() };
|
|
539
|
+
if (diagram.nest !== undefined) {
|
|
540
|
+
if (type === "hub")
|
|
541
|
+
errors.push({ reason: "\"hub\" diagram cannot carry a \"nest\" configuration" });
|
|
542
|
+
else {
|
|
543
|
+
nesting = deriveNesting(diagram.nest, nodes, nodeSet, index, parents, parenProps, errors);
|
|
544
|
+
if (diagram.nest.crossing !== undefined && diagram.nest.crossing !== "nodes")
|
|
545
|
+
edges = liftEdges(edges, nesting.parentOf, diagram.nest.crossing, anchors);
|
|
546
|
+
edges = edges.filter((edge) => nesting.parentOf.get(edge.source) !== edge.target
|
|
547
|
+
&& nesting.parentOf.get(edge.target) !== edge.source);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
421
550
|
/* the "onlyConnected" filtering keeps only the nodes with at
|
|
422
|
-
least one incident edge (sensible for "graph" diagrams only)
|
|
551
|
+
least one incident edge (sensible for "graph" diagrams only),
|
|
552
|
+
and the container nodes, whose members connect them */
|
|
423
553
|
if (diagram.onlyConnected === true && type === "graph") {
|
|
424
554
|
const connected = new Set();
|
|
425
555
|
for (const edge of edges) {
|
|
426
556
|
connected.add(edge.source);
|
|
427
557
|
connected.add(edge.target);
|
|
428
558
|
}
|
|
429
|
-
nodes = nodes.filter((node) => connected.has(node));
|
|
559
|
+
nodes = nodes.filter((node) => connected.has(node) || nesting.containers.has(node));
|
|
430
560
|
}
|
|
431
561
|
if (errors.length > 0)
|
|
432
562
|
return { errors };
|
|
@@ -438,8 +568,7 @@ const deriveDiagram = (object, diagram, index, anchors, parents, parenProps) =>
|
|
|
438
568
|
if (nodes.length === 0
|
|
439
569
|
|| (diagram.collapse !== false && nodes.length === 1 && edges.length === 0))
|
|
440
570
|
return { errors };
|
|
441
|
-
return { spec: renderSpec(diagram, type, center, centerUrl, nodes, edges, index, anchors, parenProps),
|
|
442
|
-
config: diagram.config, columns, errors };
|
|
571
|
+
return { spec: renderSpec(diagram, type, center, centerUrl, nodes, edges, nesting, index, anchors, positions, parenProps), config: diagram.config, columns, errors };
|
|
443
572
|
};
|
|
444
573
|
/* the memoized diagram derivations, keyed by specification, as the
|
|
445
574
|
lint validation and every renderer of an export derive the very
|
|
@@ -454,8 +583,11 @@ export const specDiagrams = (specification, config) => {
|
|
|
454
583
|
const index = buildLinkIndex(specification);
|
|
455
584
|
const anchors = anchorPaths(index);
|
|
456
585
|
const parents = new Map();
|
|
457
|
-
|
|
586
|
+
const positions = new Map();
|
|
587
|
+
index.forEach((node, position) => {
|
|
458
588
|
parents.set(node.object, node.parent?.object);
|
|
589
|
+
positions.set(node.object, position);
|
|
590
|
+
});
|
|
459
591
|
const schemas = collectSchemas(specification, config);
|
|
460
592
|
/* re-derive the synthetic properties of the consumed parenthesized
|
|
461
593
|
name tokens, as the derivation sees the AST alone, while the
|
|
@@ -464,7 +596,7 @@ export const specDiagrams = (specification, config) => {
|
|
|
464
596
|
const results = new Map();
|
|
465
597
|
for (const [object, schema] of schemas)
|
|
466
598
|
if (schema.diagram !== undefined)
|
|
467
|
-
results.set(object, deriveDiagram(object, schema.diagram, index, anchors, parents, parenProps));
|
|
599
|
+
results.set(object, deriveDiagram(object, schema.diagram, index, anchors, positions, parents, parenProps));
|
|
468
600
|
derivations.set(specification, { config, results });
|
|
469
601
|
return results;
|
|
470
602
|
};
|
|
@@ -23,7 +23,7 @@ export const renderAst = (specification, format, config) => {
|
|
|
23
23
|
const result = diagrams.get(object);
|
|
24
24
|
if (result?.spec !== undefined && !isTitleObject(object))
|
|
25
25
|
node.diagram = result.spec;
|
|
26
|
-
object.
|
|
26
|
+
object.children.forEach((child, i) => walk(child, node.children[i]));
|
|
27
27
|
};
|
|
28
28
|
specification.artifacts.forEach((artifact, i) => artifact.objects.forEach((object, j) => walk(object, plain.artifacts[i].objects[j])));
|
|
29
29
|
}
|
|
@@ -26,7 +26,7 @@ export const titleObject = (specification) => {
|
|
|
26
26
|
for (const object of objects) {
|
|
27
27
|
if (isTitleObject(object))
|
|
28
28
|
return object;
|
|
29
|
-
const found = search(object.
|
|
29
|
+
const found = search(object.children);
|
|
30
30
|
if (found !== undefined)
|
|
31
31
|
return found;
|
|
32
32
|
}
|
|
@@ -96,6 +96,9 @@ html.theme-light {
|
|
|
96
96
|
--gradia-color-group-name: var(--theme-color-accent-18);
|
|
97
97
|
--gradia-color-group-box: var(--theme-color-accent-32);
|
|
98
98
|
--gradia-color-group-border: var(--theme-color-accent-26);
|
|
99
|
+
--gradia-color-container-name: var(--theme-color-base-12);
|
|
100
|
+
--gradia-color-container-box: var(--theme-color-base-31);
|
|
101
|
+
--gradia-color-container-border: var(--theme-color-base-24);
|
|
99
102
|
--gradia-color-edge-line: var(--theme-color-base-22);
|
|
100
103
|
--gradia-color-edge-name: var(--theme-color-base-5);
|
|
101
104
|
--gradia-color-edge-arity: var(--theme-color-base-5);
|
|
@@ -138,6 +141,9 @@ html.theme-light {
|
|
|
138
141
|
--gradia-color-group-name: var(--theme-color-accent-15);
|
|
139
142
|
--gradia-color-group-box: var(--theme-color-accent-1);
|
|
140
143
|
--gradia-color-group-border: var(--theme-color-accent-7);
|
|
144
|
+
--gradia-color-container-name: var(--theme-color-base-24);
|
|
145
|
+
--gradia-color-container-box: var(--theme-color-base-4);
|
|
146
|
+
--gradia-color-container-border: var(--theme-color-base-16);
|
|
141
147
|
--gradia-color-edge-line: var(--theme-color-base-20);
|
|
142
148
|
--gradia-color-edge-name: var(--theme-color-base-29);
|
|
143
149
|
--gradia-color-edge-arity: var(--theme-color-base-29);
|
|
@@ -395,6 +401,173 @@ div.info-popup.spec a.link-compact::before {
|
|
|
395
401
|
display: none;
|
|
396
402
|
}
|
|
397
403
|
}
|
|
404
|
+
/* the folding tab (screen-only): the tab riding right of the brand
|
|
405
|
+
bar between the search tab and the table of contents tab (as wide
|
|
406
|
+
as those, fixed independent of the content scrolling), showing a
|
|
407
|
+
fold icon (drawn like the search icon), which slides the two
|
|
408
|
+
controls out to the right (and back in), where the tab follows the
|
|
409
|
+
bar when the table of contents panel shifts it aside; the controls
|
|
410
|
+
fold and unfold all diagrams and all over-long cell texts of the
|
|
411
|
+
content at once and carry, like the fold icon itself, the mark
|
|
412
|
+
colors in a rounded box while anything of their kind is folded */
|
|
413
|
+
div.fold-switch {
|
|
414
|
+
position: fixed;
|
|
415
|
+
top: 10.66rem;
|
|
416
|
+
left: calc(0.6rem - 1px);
|
|
417
|
+
z-index: 30;
|
|
418
|
+
display: flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
border-radius: 0 0.3rem 0.3rem 0;
|
|
421
|
+
color: var(--theme-color-specbook-background);
|
|
422
|
+
background-color: var(--theme-color-specbook-accent);
|
|
423
|
+
transition: left 0.3s ease;
|
|
424
|
+
}
|
|
425
|
+
body:has(nav.toc-panel.open) div.fold-switch {
|
|
426
|
+
left: calc(31rem - 1px);
|
|
427
|
+
box-shadow: 0.2rem 0 0.6rem rgba(0,0,0,0.3);
|
|
428
|
+
}
|
|
429
|
+
div.fold-switch div.fold-toggle {
|
|
430
|
+
padding: 0.65rem 0.1rem;
|
|
431
|
+
line-height: 0;
|
|
432
|
+
border-radius: 0 0.3rem 0.3rem 0;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
}
|
|
435
|
+
div.fold-switch div.fold-toggle:hover {
|
|
436
|
+
background-color: var(--theme-color-specbook-accent-soft);
|
|
437
|
+
}
|
|
438
|
+
div.fold-switch svg.fold-icon {
|
|
439
|
+
width: 1.1rem;
|
|
440
|
+
height: 1.1rem;
|
|
441
|
+
padding: 0.25rem;
|
|
442
|
+
border-radius: 0.3rem;
|
|
443
|
+
margin-right: 0.15rem;
|
|
444
|
+
}
|
|
445
|
+
div.fold-switch.folded svg.fold-icon {
|
|
446
|
+
color: var(--theme-color-specbook-mark-text);
|
|
447
|
+
background-color: var(--theme-color-specbook-mark-back);
|
|
448
|
+
}
|
|
449
|
+
div.fold-switch div.fold-controls {
|
|
450
|
+
display: flex;
|
|
451
|
+
align-items: center;
|
|
452
|
+
gap: 0.3rem;
|
|
453
|
+
overflow: hidden;
|
|
454
|
+
max-width: 0;
|
|
455
|
+
padding: 0;
|
|
456
|
+
transition: max-width 0.3s ease, padding 0.3s ease;
|
|
457
|
+
}
|
|
458
|
+
div.fold-switch.open div.fold-controls {
|
|
459
|
+
max-width: 6rem;
|
|
460
|
+
padding: 0 0.5rem 0 0.2rem;
|
|
461
|
+
}
|
|
462
|
+
div.fold-switch div.fold-controls > div {
|
|
463
|
+
padding: 0.25rem;
|
|
464
|
+
line-height: 0;
|
|
465
|
+
border-radius: 0.3rem;
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
-webkit-user-select: none;
|
|
468
|
+
user-select: none;
|
|
469
|
+
}
|
|
470
|
+
div.fold-switch div.fold-controls > div:hover {
|
|
471
|
+
background-color: var(--theme-color-specbook-accent-soft);
|
|
472
|
+
}
|
|
473
|
+
div.fold-switch div.fold-controls > div.folded {
|
|
474
|
+
color: var(--theme-color-specbook-mark-text);
|
|
475
|
+
background-color: var(--theme-color-specbook-mark-back);
|
|
476
|
+
}
|
|
477
|
+
div.fold-switch div.fold-controls svg {
|
|
478
|
+
width: 1.1rem;
|
|
479
|
+
height: 1.1rem;
|
|
480
|
+
}
|
|
481
|
+
@media print {
|
|
482
|
+
div.fold-switch {
|
|
483
|
+
display: none;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/* the folding of the diagrams and the over-long table cells
|
|
487
|
+
(screen-only): a diagram sits in a fold container carrying a chevron
|
|
488
|
+
mark in the muted symbol color of the anchors at its top left corner
|
|
489
|
+
-- in the page margin beside the content, and in the flow left of
|
|
490
|
+
the diagram inside a table cell, where no margin is left -- while an
|
|
491
|
+
over-long cell carries its own chevron in a rounded grey box behind
|
|
492
|
+
the last still shown word; a folded diagram leaves the icon of its
|
|
493
|
+
tab control behind in the muted color, and a running search unfolds
|
|
494
|
+
everything, so no match hides inside */
|
|
495
|
+
div.fold {
|
|
496
|
+
position: relative;
|
|
497
|
+
}
|
|
498
|
+
span.fold-mark,
|
|
499
|
+
span.fold-more {
|
|
500
|
+
color: var(--theme-color-specbook-symbol);
|
|
501
|
+
line-height: 0;
|
|
502
|
+
cursor: pointer;
|
|
503
|
+
-webkit-user-select: none;
|
|
504
|
+
user-select: none;
|
|
505
|
+
}
|
|
506
|
+
span.fold-mark:hover,
|
|
507
|
+
span.fold-more:hover {
|
|
508
|
+
color: var(--theme-color-specbook-accent);
|
|
509
|
+
}
|
|
510
|
+
span.fold-mark {
|
|
511
|
+
position: absolute;
|
|
512
|
+
top: 0.35rem;
|
|
513
|
+
left: -1.15rem;
|
|
514
|
+
}
|
|
515
|
+
span.fold-more {
|
|
516
|
+
display: inline-block;
|
|
517
|
+
margin-left: 0.2rem;
|
|
518
|
+
padding: 0.1rem 0.25rem;
|
|
519
|
+
vertical-align: -0.05em;
|
|
520
|
+
border: 1px solid var(--theme-color-specbook-border);
|
|
521
|
+
border-radius: 0.25rem;
|
|
522
|
+
}
|
|
523
|
+
span.fold-more:hover {
|
|
524
|
+
border-color: var(--theme-color-specbook-accent);
|
|
525
|
+
}
|
|
526
|
+
span.fold-mark svg,
|
|
527
|
+
span.fold-more svg {
|
|
528
|
+
width: 0.8rem;
|
|
529
|
+
height: 0.8rem;
|
|
530
|
+
}
|
|
531
|
+
div.fold.folded > span.fold-mark svg,
|
|
532
|
+
td.folded span.fold-more svg {
|
|
533
|
+
transform: rotate(-90deg);
|
|
534
|
+
}
|
|
535
|
+
span.fold-label {
|
|
536
|
+
display: none;
|
|
537
|
+
line-height: 0;
|
|
538
|
+
margin-bottom: 1rem;
|
|
539
|
+
color: var(--theme-color-specbook-muted);
|
|
540
|
+
}
|
|
541
|
+
span.fold-label svg {
|
|
542
|
+
width: 1.1rem;
|
|
543
|
+
height: 1.1rem;
|
|
544
|
+
}
|
|
545
|
+
@media screen {
|
|
546
|
+
td div.fold > span.fold-mark {
|
|
547
|
+
position: static;
|
|
548
|
+
display: inline-block;
|
|
549
|
+
vertical-align: middle;
|
|
550
|
+
margin-right: 0.3rem;
|
|
551
|
+
}
|
|
552
|
+
body:not(.searching) div.fold.folded > span.fold-label {
|
|
553
|
+
display: block;
|
|
554
|
+
}
|
|
555
|
+
body:not(.searching) td div.fold.folded > span.fold-label {
|
|
556
|
+
display: inline-block;
|
|
557
|
+
vertical-align: middle;
|
|
558
|
+
margin-bottom: 0;
|
|
559
|
+
}
|
|
560
|
+
body:not(.searching) div.fold.folded > div.diagram,
|
|
561
|
+
body:not(.searching) td.folded .fold-rest {
|
|
562
|
+
display: none;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
@media print {
|
|
566
|
+
span.fold-mark,
|
|
567
|
+
span.fold-more {
|
|
568
|
+
display: none;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
398
571
|
/* the connection status tab of the live preview (screen-only): the
|
|
399
572
|
bottommost tab riding right of the brand bar below the table of
|
|
400
573
|
contents tab (sized like the search tab, fixed independent of the
|
|
@@ -406,7 +579,7 @@ div.info-popup.spec a.link-compact::before {
|
|
|
406
579
|
for 2s (toggling every 250ms) after a content reload */
|
|
407
580
|
div.realtime-status {
|
|
408
581
|
position: fixed;
|
|
409
|
-
top:
|
|
582
|
+
top: 17.12rem;
|
|
410
583
|
left: calc(0.6rem - 2px);
|
|
411
584
|
z-index: 30;
|
|
412
585
|
padding: 0.65rem 0.1rem;
|
|
@@ -506,7 +679,7 @@ div.scroll-progress svg.scroll-icon {
|
|
|
506
679
|
left of the brand bar and to shift it aside when slid out, with its
|
|
507
680
|
tab (a menu icon drawn like the search icon, as none of the bundled
|
|
508
681
|
fonts carries the U+2630 glyph, and sized like the search tab)
|
|
509
|
-
riding right of the bar below the
|
|
682
|
+
riding right of the bar below the folding tab */
|
|
510
683
|
nav.toc-panel {
|
|
511
684
|
position: fixed;
|
|
512
685
|
top: 0;
|
|
@@ -536,7 +709,7 @@ nav.toc-panel.open div.toc-tab {
|
|
|
536
709
|
}
|
|
537
710
|
nav.toc-panel div.toc-tab {
|
|
538
711
|
position: absolute;
|
|
539
|
-
top:
|
|
712
|
+
top: 13.89rem;
|
|
540
713
|
left: calc(100% - 1px);
|
|
541
714
|
padding: 0.9rem 0.35rem;
|
|
542
715
|
line-height: 0;
|
|
@@ -1042,8 +1215,8 @@ div.titlepage table.meta td {
|
|
|
1042
1215
|
text-align: left;
|
|
1043
1216
|
}
|
|
1044
1217
|
/* the fuzzy search tab (screen-only): the middle tab riding right of
|
|
1045
|
-
the brand bar between the info tab and the
|
|
1046
|
-
|
|
1218
|
+
the brand bar between the info tab and the folding
|
|
1219
|
+
tab (as wide as those, fixed independent of the content
|
|
1047
1220
|
scrolling), showing the search icon, which slides the input field
|
|
1048
1221
|
and its clear symbol out to the right (and back in), where the tab
|
|
1049
1222
|
follows the bar when the table of contents panel shifts it aside
|
|
@@ -4,7 +4,7 @@ import type { Verbose } from "./specbook-verbose.js";
|
|
|
4
4
|
export type OutlineEntry = {
|
|
5
5
|
title: string;
|
|
6
6
|
anchor: string;
|
|
7
|
-
|
|
7
|
+
children: OutlineEntry[];
|
|
8
8
|
};
|
|
9
9
|
export declare const htmlOutline: (specification: Spec, config?: Schema) => OutlineEntry[];
|
|
10
10
|
export declare const titlePageObject: (specification: Spec) => SpecObject | undefined;
|