@rse/specbook 0.9.6 → 0.9.7
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 +60 -10
- package/dst/specbook-api.js +19 -18
- package/dst/specbook-cli.js +9 -6
- package/dst/specbook-cmd-describe.js +1 -3
- package/dst/specbook-cmd-export.js +19 -16
- package/dst/specbook-cmd-init.d.ts +1 -1
- package/dst/specbook-cmd-init.js +5 -4
- package/dst/specbook-cmd-lint.d.ts +0 -1
- package/dst/specbook-cmd-lint.js +29 -5
- package/dst/specbook-config.d.ts +1 -1
- package/dst/specbook-config.js +80 -71
- package/dst/specbook-diagnostic.js +4 -1
- package/dst/specbook-diagram.d.ts +3 -2
- package/dst/specbook-diagram.js +75 -91
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +1 -1
- package/dst/specbook-export-common.d.ts +2 -1
- package/dst/specbook-export-common.js +12 -14
- package/dst/specbook-export-html-search.js +10 -14
- package/dst/specbook-export-html.css +24 -29
- package/dst/specbook-export-html.js +100 -83
- package/dst/specbook-export-md.d.ts +1 -1
- package/dst/specbook-export-md.js +50 -24
- package/dst/specbook-export-pdf.d.ts +4 -2
- package/dst/specbook-export-pdf.js +48 -38
- package/dst/specbook-format-schema.js +6 -3
- package/dst/specbook-format.md +34 -18
- package/dst/specbook-link.d.ts +1 -0
- package/dst/specbook-link.js +7 -4
- package/dst/specbook-mcp.js +17 -10
- package/dst/specbook-parse-common.d.ts +1 -1
- package/dst/specbook-parse-semantic.d.ts +4 -2
- package/dst/specbook-parse-semantic.js +95 -42
- package/dst/specbook-parse-syntax.js +163 -136
- package/dst/specbook-parse-value.d.ts +1 -1
- package/dst/specbook-parse-value.js +6 -3
- package/dst/specbook-parse.d.ts +2 -5
- package/dst/specbook-parse.js +27 -34
- package/dst/specbook-verbose.js +2 -1
- package/package.json +1 -1
|
@@ -9,7 +9,8 @@ import sourceCodeError from "source-code-error";
|
|
|
9
9
|
export const renderDiagnostic = (diagnostic) => `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}: ${diagnostic.message}`;
|
|
10
10
|
/* render a diagnostic as a multi-line message with the affected
|
|
11
11
|
source snippet, falling back to the single-line message when the
|
|
12
|
-
source file is unreadable (e.g. the file is a directory)
|
|
12
|
+
source file is unreadable (e.g. the file is a directory) or empty
|
|
13
|
+
(as there is no snippet to show and sourceCodeError rejects it) */
|
|
13
14
|
export const renderDiagnosticVerbose = (diagnostic, colors = false) => {
|
|
14
15
|
let code;
|
|
15
16
|
try {
|
|
@@ -18,6 +19,8 @@ export const renderDiagnosticVerbose = (diagnostic, colors = false) => {
|
|
|
18
19
|
catch {
|
|
19
20
|
return `${renderDiagnostic(diagnostic)}\n`;
|
|
20
21
|
}
|
|
22
|
+
if (code === "")
|
|
23
|
+
return `${renderDiagnostic(diagnostic)}\n`;
|
|
21
24
|
return sourceCodeError({ message: diagnostic.message, filename: diagnostic.file,
|
|
22
25
|
code, line: diagnostic.line, column: diagnostic.column, colors });
|
|
23
26
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Spec, SpecObject } from "./specbook-format-spec.js";
|
|
2
|
-
import type { Schema } from "./specbook-format-schema.js";
|
|
3
|
-
import {
|
|
2
|
+
import type { Schema, SchemaDiagram } from "./specbook-format-schema.js";
|
|
3
|
+
import type { ParseContext } from "./specbook-parse-common.js";
|
|
4
4
|
export interface DiagramResult {
|
|
5
5
|
spec?: string;
|
|
6
|
+
config?: SchemaDiagram["config"];
|
|
6
7
|
errors: string[];
|
|
7
8
|
}
|
|
8
9
|
export declare const specDiagrams: (specification: Spec, config: Schema) => Map<SpecObject, DiagramResult>;
|
package/dst/specbook-diagram.js
CHANGED
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
-
import { referenceRegex, buildLinkIndex, resolveUnique, resolveSet, anchorPaths } from "./specbook-link.js";
|
|
6
|
+
import { referenceRegex, buildLinkIndex, resolveUnique, resolveSet, anchorPaths, expandReferences, plainText } from "./specbook-link.js";
|
|
7
|
+
import { collectSchemas } from "./specbook-parse-semantic.js";
|
|
7
8
|
/* the single Wiki-style reference match (the non-global sibling of
|
|
8
9
|
the imported, global "referenceRegex") */
|
|
9
|
-
const referenceOnce =
|
|
10
|
-
/* strip the inline code markup of a name or property value
|
|
11
|
-
(preserved in the AST for rendering) for labeling purposes */
|
|
12
|
-
const plainText = (text) => text.replace(/`/g, "");
|
|
10
|
+
const referenceOnce = new RegExp(referenceRegex.source);
|
|
13
11
|
/* render a text as a Gradia atom: a bareword where possible
|
|
14
12
|
(no whitespace, no special characters, no "--"), a quoted
|
|
15
13
|
string otherwise */
|
|
@@ -37,11 +35,9 @@ const configValue = (value) => {
|
|
|
37
35
|
const renderSpec = (diagram, type, center, nodes, edges, index, anchors) => {
|
|
38
36
|
const lines = [`#type ${type}`];
|
|
39
37
|
const config = diagram.config ?? {};
|
|
40
|
-
for (const key of Object.
|
|
41
|
-
const value = config[key];
|
|
38
|
+
for (const [key, value] of Object.entries(config))
|
|
42
39
|
if (value !== undefined)
|
|
43
40
|
lines.push(`#config ${key} ${configValue(value)}`);
|
|
44
|
-
}
|
|
45
41
|
lines.push("");
|
|
46
42
|
for (const node of nodes) {
|
|
47
43
|
const anchor = anchors.get(node) ?? node.id;
|
|
@@ -57,10 +53,7 @@ const renderSpec = (diagram, type, center, nodes, edges, index, anchors) => {
|
|
|
57
53
|
for (const key of diagram.properties ?? []) {
|
|
58
54
|
const value = node.properties.find((property) => property.key === key)?.value;
|
|
59
55
|
if (value !== undefined) {
|
|
60
|
-
const text = value
|
|
61
|
-
const target = resolveUnique(index, reference.trim()).target;
|
|
62
|
-
return target !== undefined ? target.name : reference.trim();
|
|
63
|
-
});
|
|
56
|
+
const text = expandReferences(value, (reference) => resolveUnique(index, reference).target?.name ?? reference);
|
|
64
57
|
attrs.push(`${atom(key)}: ${atom(plainText(text))}`);
|
|
65
58
|
}
|
|
66
59
|
}
|
|
@@ -78,56 +71,14 @@ const renderSpec = (diagram, type, center, nodes, edges, index, anchors) => {
|
|
|
78
71
|
}
|
|
79
72
|
return lines.join("\n") + "\n";
|
|
80
73
|
};
|
|
81
|
-
/* derive the
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const resolvePatterns = (value, field) => {
|
|
90
|
-
const matches = new Array();
|
|
91
|
-
const seen = new Set();
|
|
92
|
-
for (const m of value.matchAll(referenceRegex)) {
|
|
93
|
-
const pattern = m[1].trim();
|
|
94
|
-
const set = resolveSet(index, pattern);
|
|
95
|
-
if (set.length === 0)
|
|
96
|
-
errors.push(`unresolvable diagram "${field}" pattern "[[${pattern}]]"`);
|
|
97
|
-
for (const match of set) {
|
|
98
|
-
if (!seen.has(match)) {
|
|
99
|
-
seen.add(match);
|
|
100
|
-
matches.push(match);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return matches;
|
|
105
|
-
};
|
|
106
|
-
/* determine the node objects (default: the current object plus
|
|
107
|
-
all objects below it) and the edge objects (default: none),
|
|
108
|
-
with the edge objects never acting as nodes themselves */
|
|
109
|
-
let nodes;
|
|
110
|
-
if (diagram.nodes !== undefined)
|
|
111
|
-
nodes = resolvePatterns(diagram.nodes, "nodes");
|
|
112
|
-
else {
|
|
113
|
-
nodes = new Array();
|
|
114
|
-
const walk = (o) => {
|
|
115
|
-
nodes.push(o);
|
|
116
|
-
o.childs.forEach(walk);
|
|
117
|
-
};
|
|
118
|
-
walk(object);
|
|
119
|
-
}
|
|
120
|
-
const edgeObjects = diagram.edges !== undefined ? resolvePatterns(diagram.edges, "edges") : [];
|
|
121
|
-
const edgeSet = new Set(edgeObjects);
|
|
122
|
-
nodes = nodes.filter((node) => !edgeSet.has(node));
|
|
123
|
-
const nodeSet = new Set(nodes);
|
|
124
|
-
/* derive the edges: from the "[[...]]" references of the node
|
|
125
|
-
objects (per the "links" selection) and from the edge objects by
|
|
126
|
-
convention (source: parent object, target: first reference in the
|
|
127
|
-
property values, name: object name, arity: "ARITY" property),
|
|
128
|
-
overridable via "edgeSource"/"edgeTarget"/"edgeArity" (a "grid"
|
|
129
|
-
diagram is edge-less by definition, so no edges are derived at all) */
|
|
130
|
-
let edges = new Array();
|
|
74
|
+
/* derive the edges of a diagram: from the "[[...]]" references of the
|
|
75
|
+
node objects (per the "links" selection) and from the edge objects by
|
|
76
|
+
convention (source: parent object, target: first reference in the
|
|
77
|
+
property values, name: object name, arity: "ARITY" property),
|
|
78
|
+
overridable via "edgeSource"/"edgeTarget"/"edgeArity" (a "grid"
|
|
79
|
+
diagram is edge-less by definition, so no edges are derived at all) */
|
|
80
|
+
const deriveEdges = (diagram, type, nodes, nodeSet, edgeObjects, index, anchors, parents, errors) => {
|
|
81
|
+
const edges = new Array();
|
|
131
82
|
if (type !== "grid") {
|
|
132
83
|
for (const node of nodes) {
|
|
133
84
|
const texts = node.properties.map((property) => property.value);
|
|
@@ -188,11 +139,13 @@ const deriveDiagram = (object, diagram, index, anchors, parents) => {
|
|
|
188
139
|
errors.push("\"grid\" diagram cannot carry an \"edges\" configuration");
|
|
189
140
|
if (diagram.hierarchy === true)
|
|
190
141
|
errors.push("\"grid\" diagram cannot carry a \"hierarchy\" configuration");
|
|
142
|
+
if (diagram.onlyConnected === true)
|
|
143
|
+
errors.push("\"grid\" diagram cannot carry an \"onlyConnected\" configuration");
|
|
191
144
|
}
|
|
192
145
|
/* deduplicate the edges (the same reference can occur in
|
|
193
146
|
multiple texts of the same node object) */
|
|
194
147
|
const seenEdges = new Set();
|
|
195
|
-
|
|
148
|
+
return edges.filter((edge) => {
|
|
196
149
|
const key = `${anchors.get(edge.source) ?? edge.source.id}` +
|
|
197
150
|
`\u0000${anchors.get(edge.target) ?? edge.target.id}` +
|
|
198
151
|
`\u0000${edge.name ?? ""}\u0000${edge.arity ?? ""}`;
|
|
@@ -201,15 +154,68 @@ const deriveDiagram = (object, diagram, index, anchors, parents) => {
|
|
|
201
154
|
seenEdges.add(key);
|
|
202
155
|
return true;
|
|
203
156
|
});
|
|
157
|
+
};
|
|
158
|
+
/* resolve a comma-separated "[[...]]" reference pattern string into
|
|
159
|
+
its (deduplicated, order-preserving) object match set, reporting
|
|
160
|
+
every unresolvable pattern as an error */
|
|
161
|
+
const resolvePatterns = (index, errors, value, field) => {
|
|
162
|
+
const matches = new Array();
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
let found = false;
|
|
165
|
+
for (const m of value.matchAll(referenceRegex)) {
|
|
166
|
+
found = true;
|
|
167
|
+
const pattern = m[1].trim();
|
|
168
|
+
const set = resolveSet(index, pattern);
|
|
169
|
+
if (set.length === 0)
|
|
170
|
+
errors.push(`unresolvable diagram "${field}" pattern "[[${pattern}]]"`);
|
|
171
|
+
for (const match of set) {
|
|
172
|
+
if (!seen.has(match)) {
|
|
173
|
+
seen.add(match);
|
|
174
|
+
matches.push(match);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!found)
|
|
179
|
+
errors.push(`diagram "${field}" carries no "[[...]]" reference pattern`);
|
|
180
|
+
return matches;
|
|
181
|
+
};
|
|
182
|
+
/* derive the Gradia spec of a single object from its "diagram:"
|
|
183
|
+
schema configuration (the returned spec is absent whenever the
|
|
184
|
+
configured diagram situation is invalid) */
|
|
185
|
+
const deriveDiagram = (object, diagram, index, anchors, parents) => {
|
|
186
|
+
const type = diagram.type ?? "graph";
|
|
187
|
+
const errors = new Array();
|
|
188
|
+
/* determine the node objects (default: the current object plus
|
|
189
|
+
all objects below it) and the edge objects (default: none),
|
|
190
|
+
with the edge objects never acting as nodes themselves */
|
|
191
|
+
let nodes;
|
|
192
|
+
if (diagram.nodes !== undefined)
|
|
193
|
+
nodes = resolvePatterns(index, errors, diagram.nodes, "nodes");
|
|
194
|
+
else {
|
|
195
|
+
nodes = new Array();
|
|
196
|
+
const walk = (o) => {
|
|
197
|
+
nodes.push(o);
|
|
198
|
+
o.childs.forEach(walk);
|
|
199
|
+
};
|
|
200
|
+
walk(object);
|
|
201
|
+
}
|
|
202
|
+
const edgeObjects = diagram.edges !== undefined ? resolvePatterns(index, errors, diagram.edges, "edges") : [];
|
|
203
|
+
const edgeSet = new Set(edgeObjects);
|
|
204
|
+
nodes = nodes.filter((node) => !edgeSet.has(node));
|
|
205
|
+
const nodeSet = new Set(nodes);
|
|
206
|
+
/* derive the (deduplicated) edges of the diagram */
|
|
207
|
+
let edges = deriveEdges(diagram, type, nodes, nodeSet, edgeObjects, index, anchors, parents, errors);
|
|
204
208
|
/* determine the center object of a "hub" diagram: the current
|
|
205
209
|
object for the default "self" configuration, the uniquely
|
|
206
210
|
resolved object of an explicit "[[...]]" reference otherwise */
|
|
207
211
|
let center = object;
|
|
212
|
+
if (type !== "hub" && diagram.center !== undefined)
|
|
213
|
+
errors.push(`"${type}" diagram cannot carry a "center" configuration`);
|
|
208
214
|
if (type === "hub" && diagram.center !== undefined && diagram.center !== "self") {
|
|
209
215
|
const reference = diagram.center.match(referenceOnce)?.[1].trim();
|
|
210
216
|
const resolved = reference !== undefined ? resolveUnique(index, reference) : undefined;
|
|
211
217
|
if (resolved?.target === undefined) {
|
|
212
|
-
errors.push(
|
|
218
|
+
errors.push((resolved?.ambiguous === true ? "ambiguous" : "unresolvable") +
|
|
213
219
|
` diagram "center" reference "${diagram.center}"`);
|
|
214
220
|
return { errors };
|
|
215
221
|
}
|
|
@@ -248,34 +254,11 @@ const deriveDiagram = (object, diagram, index, anchors, parents) => {
|
|
|
248
254
|
if (errors.length > 0)
|
|
249
255
|
return { errors };
|
|
250
256
|
/* the "collapse" handling (enabled by default) silently omits a
|
|
251
|
-
|
|
257
|
+
degenerate diagram, consisting of a single node only, as such a
|
|
252
258
|
diagram carries no information beyond the object itself */
|
|
253
259
|
if (diagram.collapse !== false && nodes.length === 1 && edges.length === 0)
|
|
254
260
|
return { errors };
|
|
255
|
-
return { spec: renderSpec(diagram, type, center, nodes, edges, index, anchors), errors };
|
|
256
|
-
};
|
|
257
|
-
/* collect the objects whose schema configuration carries a "diagram:"
|
|
258
|
-
field (the schema resolution mirrors the semantic validation) */
|
|
259
|
-
const collectDiagrams = (specification, config) => {
|
|
260
|
-
const diagrams = new Map();
|
|
261
|
-
const walk = (object, schema) => {
|
|
262
|
-
if (schema.diagram !== undefined)
|
|
263
|
-
diagrams.set(object, schema.diagram);
|
|
264
|
-
for (const child of object.childs) {
|
|
265
|
-
const childSchema = (schema.childs ?? []).find((c) => c.kind === child.kind);
|
|
266
|
-
if (childSchema !== undefined)
|
|
267
|
-
walk(child, childSchema);
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
for (const artifact of specification.artifacts) {
|
|
271
|
-
for (const object of artifact.objects) {
|
|
272
|
-
const schema = config.find((s) => (s.kind === object.kind && s.id === object.id) || `${s.kind}-${s.id}` === object.id) ??
|
|
273
|
-
config.find((s) => (s.name ?? "").toUpperCase() === plainText(object.name).toUpperCase());
|
|
274
|
-
if (schema !== undefined)
|
|
275
|
-
walk(object, schema);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return diagrams;
|
|
261
|
+
return { spec: renderSpec(diagram, type, center, nodes, edges, index, anchors), config: diagram.config, errors };
|
|
279
262
|
};
|
|
280
263
|
/* derive the Gradia specs of all diagram-configured objects
|
|
281
264
|
of a specification */
|
|
@@ -286,8 +269,9 @@ export const specDiagrams = (specification, config) => {
|
|
|
286
269
|
for (const node of index)
|
|
287
270
|
parents.set(node.object, node.parent?.object);
|
|
288
271
|
const results = new Map();
|
|
289
|
-
for (const [object,
|
|
290
|
-
|
|
272
|
+
for (const [object, schema] of collectSchemas(specification, config))
|
|
273
|
+
if (schema.diagram !== undefined)
|
|
274
|
+
results.set(object, deriveDiagram(object, schema.diagram, index, anchors, parents));
|
|
291
275
|
return results;
|
|
292
276
|
};
|
|
293
277
|
/* validate all configured diagrams of a specification, reporting
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Spec } from "./specbook-format-spec.js";
|
|
2
2
|
import type { Schema } from "./specbook-format-schema.js";
|
|
3
3
|
export type AstFormat = "json" | "json5" | "yaml" | "toon";
|
|
4
|
-
export declare const renderAst: (specification: Spec, format: AstFormat, config?: Schema) =>
|
|
4
|
+
export declare const renderAst: (specification: Spec, format: AstFormat, config?: Schema) => Buffer;
|
|
@@ -8,7 +8,7 @@ import { stringify as stringifyYaml } from "yaml";
|
|
|
8
8
|
import { encode as encodeToon } from "@toon-format/toon";
|
|
9
9
|
import { specDiagrams } from "./specbook-diagram.js";
|
|
10
10
|
/* render the specification AST into a serialization format */
|
|
11
|
-
export const renderAst =
|
|
11
|
+
export const renderAst = (specification, format, config) => {
|
|
12
12
|
/* reduce the specification to plain JSON values (ISO date strings) */
|
|
13
13
|
const plain = JSON.parse(JSON.stringify(specification));
|
|
14
14
|
/* attach the Gradia specs of the diagram-configured objects as
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { Spec, SpecObject } from "./specbook-format-spec.js";
|
|
2
|
+
import type { embeddingThemes } from "./specbook-parse-common.js";
|
|
2
3
|
export declare const escapeHtml: (text: string) => string;
|
|
3
4
|
export declare const stylesheet: () => string;
|
|
4
5
|
export declare const searchScript: () => string;
|
|
5
|
-
export declare const fallbackLogo: (theme:
|
|
6
|
+
export declare const fallbackLogo: (theme: (typeof embeddingThemes)[number]) => string;
|
|
6
7
|
export declare const isTitleObject: (object: SpecObject) => boolean;
|
|
7
8
|
export declare const titleObject: (specification: Spec) => SpecObject | undefined;
|
|
8
9
|
export declare const documentLang: (specification: Spec) => string | undefined;
|
|
@@ -3,20 +3,19 @@
|
|
|
3
3
|
** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
4
|
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
5
|
*/
|
|
6
|
-
import path from "node:path";
|
|
7
6
|
import fs from "node:fs";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
7
|
/* escape a text for embedding into template HTML (text and attributes) */
|
|
10
8
|
export const escapeHtml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
11
9
|
.replace(/"/g, """).replace(/'/g, "'");
|
|
12
10
|
/* provide the build-time pre-assembled stylesheet (with the
|
|
13
11
|
font faces already inlined as base64 data: URIs) */
|
|
14
|
-
export const stylesheet = () => fs.readFileSync(
|
|
12
|
+
export const stylesheet = () => fs.readFileSync(new URL("specbook-export-html.css", import.meta.url), "utf8");
|
|
15
13
|
/* provide the build-time bundled client-side fuzzy search script */
|
|
16
|
-
export const searchScript = () => fs.readFileSync(
|
|
14
|
+
export const searchScript = () => fs.readFileSync(new URL("specbook-export-html-search.js", import.meta.url), "utf8");
|
|
17
15
|
/* provide a theme variant of the build-time bundled fallback logo of
|
|
18
16
|
SpecBook itself (as a self-contained data: URL, to keep its styles isolated) */
|
|
19
|
-
export const fallbackLogo = (theme) => "data:image/svg+xml;base64," +
|
|
17
|
+
export const fallbackLogo = (theme) => "data:image/svg+xml;base64," +
|
|
18
|
+
fs.readFileSync(new URL(`specbook-export-logo-${theme}.svg`, import.meta.url)).toString("base64");
|
|
20
19
|
/* check whether an object is the specification title object */
|
|
21
20
|
export const isTitleObject = (object) => object.kind === "META" && object.name.toUpperCase() === "TITLE";
|
|
22
21
|
/* determine the title object of the specification, searched at any
|
|
@@ -71,14 +70,15 @@ export const documentPaperSize = (specification) => {
|
|
|
71
70
|
`(expected ${paperSizes.join(", ")})`);
|
|
72
71
|
return paper;
|
|
73
72
|
};
|
|
74
|
-
/*
|
|
75
|
-
|
|
76
|
-
print margins, the own vertical margins of the diagram, and the
|
|
77
|
-
room its introducing heading claims above it) and is never broken
|
|
78
|
-
across a page boundary; without the heading reserve a maximally
|
|
73
|
+
/* the vertical room (in rem) the introducing heading of a diagram
|
|
74
|
+
claims above it on the same page: without this reserve a maximally
|
|
79
75
|
sized diagram could not share its page with the heading, which
|
|
80
76
|
would defeat the "break-after: avoid" bundling of the stylesheet */
|
|
81
77
|
const headingReserve = 6;
|
|
78
|
+
/* provide the paper-dependent print stylesheet: a diagram is scaled
|
|
79
|
+
down to still fit onto a single page (the paper height less the
|
|
80
|
+
print margins, the own vertical margins of the diagram, and the
|
|
81
|
+
heading reserve above it) and is never broken across a page boundary */
|
|
82
82
|
export const paperStylesheet = (paper) => {
|
|
83
83
|
const setup = paperSetup(paper);
|
|
84
84
|
const avail = setup.height - setup.margin.top - setup.margin.bottom;
|
|
@@ -131,9 +131,7 @@ const typographyGlyphs = [
|
|
|
131
131
|
glyphs (no charset or a full Unicode charset keeps the fonts complete) */
|
|
132
132
|
export const subsetStylesheet = async (charset) => {
|
|
133
133
|
const css = stylesheet();
|
|
134
|
-
|
|
135
|
-
return css;
|
|
136
|
-
const codepoints = charsetCodepoints(charset);
|
|
134
|
+
const codepoints = charset !== undefined ? charsetCodepoints(charset) : undefined;
|
|
137
135
|
if (codepoints === undefined)
|
|
138
136
|
return css;
|
|
139
137
|
const { default: subsetFont } = await import("subset-font");
|
|
@@ -162,7 +160,7 @@ export const documentTitle = (specification) => ({
|
|
|
162
160
|
export const documentLogo = (specification) => {
|
|
163
161
|
const content = titleObject(specification)
|
|
164
162
|
?.properties.find((property) => property.key === "LOGO")?.embedding?.[0];
|
|
165
|
-
if (content === undefined)
|
|
163
|
+
if (content === undefined || content === "")
|
|
166
164
|
return fallbackLogo("light");
|
|
167
165
|
return content.startsWith("data:") ? content :
|
|
168
166
|
`data:image/svg+xml;base64,${Buffer.from(content, "utf8").toString("base64")}`;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
/* shortest word still eligible for fuzzy matching: below it the
|
|
20
20
|
edit distance would equate it with almost any short word */
|
|
21
|
-
const
|
|
21
|
+
const fuzzyMin = 4
|
|
22
22
|
|
|
23
23
|
/* the searchable units (paragraphs, list items, table rows, and
|
|
24
24
|
headings), the corpus vocabulary, and the per-word cache of the
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
if (result !== undefined)
|
|
102
102
|
return result
|
|
103
103
|
result = []
|
|
104
|
-
if (word.length >=
|
|
104
|
+
if (word.length >= fuzzyMin)
|
|
105
105
|
vocab.forEach((given) => {
|
|
106
106
|
if (Math.abs(word.length - given.length) <= 1
|
|
107
107
|
&& (diceCoefficient(word, given) >= 0.50
|
|
@@ -115,19 +115,21 @@
|
|
|
115
115
|
/* match a query word against a unit, as a literal substring or as
|
|
116
116
|
a fuzzy variant, remembering the variants for the highlighting */
|
|
117
117
|
const wordMatch = (word, unit, variants) => {
|
|
118
|
-
if (unit.text.
|
|
118
|
+
if (unit.text.includes(word))
|
|
119
119
|
return true
|
|
120
120
|
const near = fuzzyWords(word).filter((v) => unit.words.has(v))
|
|
121
121
|
near.forEach((v) => { variants.add(v) })
|
|
122
122
|
return near.length > 0
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/* escape a text for its literal use inside a regular expression */
|
|
126
|
+
const escapeRegex = (s) =>
|
|
127
|
+
s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
128
|
+
|
|
125
129
|
/* the alternation matching everything worth highlighting: the
|
|
126
130
|
query words as plain substrings plus the fuzzy variants as
|
|
127
131
|
whole words, longest alternative first, so a variant is never
|
|
128
132
|
cut short by a query word nested inside it */
|
|
129
|
-
const escapeRegex = (s) =>
|
|
130
|
-
s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
131
133
|
const buildRegex = (words, variants) => {
|
|
132
134
|
const alts = words.map((w) => ({ len: w.length, src: escapeRegex(w) }))
|
|
133
135
|
.concat(variants.map((v) => ({ len: v.length,
|
|
@@ -203,7 +205,7 @@
|
|
|
203
205
|
units.forEach((unit) => { unit.keep = false })
|
|
204
206
|
parts.forEach((part) => {
|
|
205
207
|
let matched = units.filter((unit) =>
|
|
206
|
-
part.every((word) => unit.text.
|
|
208
|
+
part.every((word) => unit.text.includes(word)))
|
|
207
209
|
if (matched.length === 0)
|
|
208
210
|
matched = units.filter((unit) =>
|
|
209
211
|
part.every((word) => wordMatch(word, unit, variants)))
|
|
@@ -226,7 +228,7 @@
|
|
|
226
228
|
const heading = section.firstElementChild
|
|
227
229
|
if (heading !== null && /^H[1-6]$/.test(heading.tagName))
|
|
228
230
|
heading.classList.add("search-keep")
|
|
229
|
-
section = section.parentElement
|
|
231
|
+
section = section.parentElement?.closest("section") ?? null
|
|
230
232
|
}
|
|
231
233
|
})
|
|
232
234
|
|
|
@@ -244,13 +246,7 @@
|
|
|
244
246
|
})
|
|
245
247
|
|
|
246
248
|
/* highlight every matched word within the kept units */
|
|
247
|
-
const words =
|
|
248
|
-
parts.forEach((part) => {
|
|
249
|
-
part.forEach((word) => {
|
|
250
|
-
if (!words.includes(word))
|
|
251
|
-
words.push(word)
|
|
252
|
-
})
|
|
253
|
-
})
|
|
249
|
+
const words = Array.from(new Set(parts.flat()))
|
|
254
250
|
const regex = buildRegex(words, Array.from(variants))
|
|
255
251
|
units.forEach((unit) => {
|
|
256
252
|
if (unit.el.classList.contains("search-keep"))
|