@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
@@ -0,0 +1,165 @@
1
+ /*
2
+ ** Specification Book (SpecBook)
3
+ ** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
+ */
6
+ import ASTQ from "astq";
7
+ import { Marked } from "marked";
8
+ import JSON5 from "json5";
9
+ import { stringify as stringifyYaml } from "yaml";
10
+ import { encode as encodeToon } from "@toon-format/toon";
11
+ import { buildLinkIndex, referenceTargets, resolveUnique, expandReferences } from "./specbook-link.js";
12
+ import { parseOutputSpec } from "./specbook-cmd-export.js";
13
+ /* the fixed attributes of an object node, which win over
14
+ the same-named property keys */
15
+ const objectFields = ["kind", "id", "anchor", "paren", "name", "primary",
16
+ "description", "rationale", "elaboration"];
17
+ /* the private Markdown renderer of the text helpers, as the
18
+ "marked" singleton is shared with the application */
19
+ const marked = new Marked();
20
+ /* render Markdown tokens into plain text: the blocks joined by the
21
+ separator, the inline markup dropped, and the code kept verbatim */
22
+ const plainOf = (tokens, separator) => tokens.map((generic) => {
23
+ const token = generic;
24
+ if (token.type === "space" || token.type === "hr" || token.type === "html" || token.type === "def")
25
+ return "";
26
+ else if (token.type === "code" || token.type === "codespan")
27
+ return token.text;
28
+ else if (token.type === "br")
29
+ return "\n";
30
+ else if (token.type === "blockquote")
31
+ return plainOf(token.tokens, "\n\n");
32
+ else if (token.type === "list")
33
+ return token.items.map((item) => plainOf(item.tokens, "\n")).join("\n");
34
+ else if (token.type === "table")
35
+ return [token.header, ...token.rows].map((row) => row.map((cell) => plainOf(cell.tokens, "")).join(" | ")).join("\n");
36
+ else if ("tokens" in token && token.tokens !== undefined)
37
+ return plainOf(token.tokens, "");
38
+ else
39
+ return "text" in token ? token.text : "";
40
+ }).filter((part) => part !== "").join(separator);
41
+ /* the serialization formats of the query results (the AST export ones) */
42
+ export const queryFormats = ["json", "json5", "yaml", "toon"];
43
+ /* determine the format and file of a query output: an explicit format
44
+ wins, else it is inferred exactly like the one of an export output
45
+ (a "<format>:" prefix, else the filename extension, "-" being JSON),
46
+ where only the serialization formats apply */
47
+ export const parseQueryOutput = (output, format) => {
48
+ const spec = format !== undefined ? { format, output } : parseOutputSpec(output);
49
+ if (!queryFormats.includes(spec.format))
50
+ throw new Error(`unsupported query format "${spec.format}" (supported: ${queryFormats.join(", ")})`);
51
+ return { format: spec.format, output: spec.output };
52
+ };
53
+ /* render query result nodes in a serialization format: their subtrees
54
+ included, the embedded file contents dropped (the bulk no consumer
55
+ can use), and the dates as ISO strings */
56
+ export const renderNodes = (nodes, format = "json") => {
57
+ const plain = JSON.parse(JSON.stringify(nodes, (key, value) => key === "embedding" ? undefined : value));
58
+ if (format === "json")
59
+ return JSON.stringify(plain, null, 4) + "\n";
60
+ else if (format === "json5")
61
+ return JSON5.stringify(plain, null, 4) + "\n";
62
+ else if (format === "yaml")
63
+ return stringifyYaml(plain);
64
+ else
65
+ return encodeToon(plain) + "\n";
66
+ };
67
+ /* a loaded specification, queryable through the XPath-like ASTq
68
+ selectors: the root node (type "specification") holds the artifacts
69
+ (type "artifact"), which hold the object trees (typed by their kind,
70
+ whitespace dashed), and the pre-registered function "refs()" yields
71
+ the objects the references of the current object resolve to */
72
+ export class SpecBookDocument {
73
+ specification;
74
+ config;
75
+ diagnostics;
76
+ astq = new ASTQ();
77
+ parents = new WeakMap();
78
+ attrs = new WeakMap();
79
+ index;
80
+ constructor(specification, config, diagnostics, origins) {
81
+ this.specification = specification;
82
+ this.config = config;
83
+ this.diagnostics = diagnostics;
84
+ this.index = buildLinkIndex(specification);
85
+ /* index the parents once, as the objects carry no parent reference */
86
+ const walk = (object, parent) => {
87
+ this.parents.set(object, parent);
88
+ for (const child of object.children)
89
+ walk(child, object);
90
+ };
91
+ for (const artifact of specification.artifacts) {
92
+ this.parents.set(artifact, specification);
93
+ for (const object of artifact.objects)
94
+ walk(object, artifact);
95
+ }
96
+ /* determine the attributes of a node once: the timestamps and
97
+ source file of an artifact, and the fixed fields of an object
98
+ followed by its properties (the first of duplicate keys winning) */
99
+ const attributesOf = (node) => {
100
+ let attrs = this.attrs.get(node);
101
+ if (attrs === undefined) {
102
+ attrs = new Map();
103
+ if ("objects" in node) {
104
+ attrs.set("created", node.created.toISOString());
105
+ attrs.set("modified", node.modified.toISOString());
106
+ attrs.set("file", origins.get(node));
107
+ }
108
+ else if ("kind" in node) {
109
+ const fields = [node.kind, node.id, node.anchor, node.paren, node.name, node.primary,
110
+ node.description?.description, node.description?.rationale, node.description?.elaboration];
111
+ for (const [i, value] of fields.entries())
112
+ if (value !== undefined)
113
+ attrs.set(objectFields[i], value);
114
+ for (const property of node.properties)
115
+ if (!objectFields.includes(property.key) && !attrs.has(property.key))
116
+ attrs.set(property.key, property.value);
117
+ }
118
+ this.attrs.set(node, attrs);
119
+ }
120
+ return attrs;
121
+ };
122
+ /* access the specification tree exclusively, as the plain AST
123
+ objects would otherwise be tasted by the built-in JSON adapter */
124
+ const adapter = {
125
+ taste: (node) => node === specification || this.parents.has(node),
126
+ getParentNode: (node) => this.parents.get(node) ?? null,
127
+ getChildNodes: (node) => "artifacts" in node ? node.artifacts :
128
+ "objects" in node ? node.objects : node.children,
129
+ getNodeType: (node) => "artifacts" in node ? "specification" :
130
+ "objects" in node ? "artifact" : node.kind.replace(/\s+/g, "-"),
131
+ getNodeAttrNames: (node) => Array.from(attributesOf(node).keys()),
132
+ getNodeAttrValue: (node, attr) => attributesOf(node).get(attr)
133
+ };
134
+ this.astq.adapter(adapter, true);
135
+ this.astq.func("refs", (_adapter, node) => "kind" in node ? referenceTargets(this.index, node) : []);
136
+ }
137
+ /* query the nodes an ASTq selector matches, starting at the root,
138
+ with the values of the "{name}" parameters of the selector */
139
+ query(selector, params) {
140
+ return this.astq.query(this.specification, selector, params);
141
+ }
142
+ /* the parent of a node (undefined for the root) */
143
+ parent(node) {
144
+ return this.parents.get(node);
145
+ }
146
+ /* register a custom query function (or replace a registered one) */
147
+ func(name, fn) {
148
+ this.astq.func(name, fn);
149
+ return this;
150
+ }
151
+ /* replace the references of a Markdown text (outside its code) by
152
+ the names of their targets, resolved nearest to the referencing
153
+ object (if given), an unresolvable one keeping its reference text */
154
+ expand(text, from) {
155
+ return expandReferences(text, (reference) => resolveUnique(this.index, reference, from).target?.name ?? reference);
156
+ }
157
+ /* render a Markdown text (a description part or property value) into plain text */
158
+ plain(text, from) {
159
+ return plainOf(marked.lexer(this.expand(text, from)), "\n\n");
160
+ }
161
+ /* render a Markdown text (a description part or property value) into an HTML fragment */
162
+ html(text, from) {
163
+ return marked.parse(this.expand(text, from), { async: false });
164
+ }
165
+ }
@@ -1,11 +1,17 @@
1
1
  import { LineCounter, type Document } from "yaml";
2
- import { Schema } from "./specbook-format-schema.js";
2
+ import { Schema, type SchemaObject, type SchemaProperty } from "./specbook-format-schema.js";
3
3
  import { type Diagnostic } from "./specbook-diagnostic.js";
4
4
  type YamlPath = (string | number)[];
5
5
  export declare const lineColOfPath: (doc: Document, lines: LineCounter, path: YamlPath) => {
6
6
  line: number;
7
7
  column: number;
8
8
  };
9
+ export interface ConfigOverlap {
10
+ artifact: string;
11
+ files: string[];
12
+ }
13
+ export type ConfigOrigins = Map<string, string[]>;
14
+ export declare const originKey: (chain: SchemaObject[], prop?: SchemaProperty) => string;
9
15
  export declare const configStatistics: (config: Schema) => {
10
16
  objects: number;
11
17
  links: number;
@@ -13,5 +19,7 @@ export declare const configStatistics: (config: Schema) => {
13
19
  export declare const loadConfig: (files: string[]) => {
14
20
  config?: Schema;
15
21
  diagnostics: Diagnostic[];
22
+ overlaps: ConfigOverlap[];
23
+ origins: ConfigOrigins;
16
24
  };
17
25
  export {};
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
7
7
  import { parseDocument, isNode, LineCounter } from "yaml";
8
8
  import * as v from "valibot";
9
9
  import { mergeWith, isPlainObject } from "es-toolkit";
10
- import { Schema } from "./specbook-format-schema.js";
10
+ import { Schema, diagramsOf, reservedAttributes } from "./specbook-format-schema.js";
11
11
  import { compileValueExpr, anchored, admitsReferences } from "./specbook-parse-value.js";
12
12
  /* determine line/column of a YAML document path via the node ranges
13
13
  of the parsed document, falling back to the closest ancestor node */
@@ -25,14 +25,16 @@ export const lineColOfPath = (doc, lines, path) => {
25
25
  /* the identity by which the elements of the lists of the configuration
26
26
  are matched on merging: the artifacts by their kind plus id or name
27
27
  and the nested objects by their kind alone (exactly like the sibling
28
- resolution), the properties by their name, and the scalar entries
29
- by their value */
28
+ resolution), the properties by their name, the diagrams by their
29
+ type, and the scalar entries by their value */
30
30
  const identityOf = (item, list) => {
31
31
  if (!isPlainObject(item))
32
32
  return String(item);
33
33
  const object = item;
34
34
  if (list === "props")
35
35
  return String(object.name ?? "");
36
+ else if (list === "diagram")
37
+ return String(object.type ?? "graph");
36
38
  else if (list === "children")
37
39
  return String(object.kind ?? "");
38
40
  else
@@ -60,7 +62,15 @@ const mergeConfig = (target, source, list) => {
60
62
  return items;
61
63
  }
62
64
  else if (isPlainObject(target) && isPlainObject(source))
63
- return mergeWith(target, source, (t, s, key) => Array.isArray(t) && Array.isArray(s) ? mergeConfig(t, s, key) : undefined);
65
+ return mergeWith(target, source, (t, s, key) => {
66
+ if (Array.isArray(t) && Array.isArray(s))
67
+ return mergeConfig(t, s, key);
68
+ /* a single diagram and a list of diagrams merge as lists */
69
+ if (key === "diagram" && (Array.isArray(t) || Array.isArray(s))
70
+ && t !== undefined && t !== null && s !== undefined && s !== null)
71
+ return mergeConfig(Array.isArray(t) ? t : [t], Array.isArray(s) ? s : [s], key);
72
+ return undefined;
73
+ });
64
74
  else
65
75
  return source;
66
76
  };
@@ -75,6 +85,59 @@ const unshared = (value) => {
75
85
  else
76
86
  return value;
77
87
  };
88
+ /* the key of a node of the merged configuration in its origins: the
89
+ identities of the object kinds along its path (the artifact first),
90
+ plus the identity of a property of the last one */
91
+ export const originKey = (chain, prop) => chain.map((object, i) => identityOf(object, i > 0 ? "children" : undefined)).join("/") +
92
+ (prop !== undefined ? `#${identityOf(prop, "props")}` : "");
93
+ /* determine the configuration files contributing every object kind
94
+ and property, by walking the raw documents along the very identities
95
+ the merge matches the list elements with */
96
+ const originsOf = (docs) => {
97
+ const origins = new Map();
98
+ const add = (key, file) => {
99
+ const files = origins.get(key) ?? [];
100
+ if (!files.includes(file))
101
+ files.push(file);
102
+ origins.set(key, files);
103
+ };
104
+ const walk = (items, prefix, depth, file) => {
105
+ if (!Array.isArray(items))
106
+ return;
107
+ for (const item of items) {
108
+ if (!isPlainObject(item))
109
+ continue;
110
+ const object = item;
111
+ const key = (prefix !== "" ? `${prefix}/` : "") + identityOf(item, depth > 1 ? "children" : undefined);
112
+ add(key, file);
113
+ if (Array.isArray(object.props))
114
+ for (const prop of object.props)
115
+ if (isPlainObject(prop))
116
+ add(`${key}#${identityOf(prop, "props")}`, file);
117
+ walk(object.children, key, depth + 1, file);
118
+ }
119
+ };
120
+ for (const { file, raw } of docs)
121
+ walk(raw, "", 1, file);
122
+ return origins;
123
+ };
124
+ /* determine the artifacts several configuration files define */
125
+ const overlapsOf = (docs) => {
126
+ const defined = new Map();
127
+ for (const { file, raw } of docs)
128
+ if (Array.isArray(raw))
129
+ for (const item of raw) {
130
+ if (!isPlainObject(item))
131
+ continue;
132
+ const object = item;
133
+ const kind = String(object.kind ?? "");
134
+ const label = object.id !== undefined ? `${kind} (${String(object.id)})` : `${kind}: ${String(object.name ?? "")}`;
135
+ const overlap = defined.get(identityOf(item)) ?? { artifact: label, files: [] };
136
+ overlap.files.push(file);
137
+ defined.set(identityOf(item), overlap);
138
+ }
139
+ return Array.from(defined.values()).filter((overlap) => overlap.files.length > 1);
140
+ };
78
141
  /* determine file and line/column of a path into the merged
79
142
  configuration by translating the path into every document (from the
80
143
  last to the first one, as a later document overrides an earlier one),
@@ -190,6 +253,58 @@ const checkAutomaton = (object, at, diagnose) => {
190
253
  if (nodes !== undefined && propOf(nodes, automaton[field]) === undefined)
191
254
  diagnose([...at, "automaton", field], `"automaton" ${field} property "${automaton[field]}" is not a property of "${automaton.nodes}"`);
192
255
  };
256
+ /* check the diagrams of an object (if any): a displayed property must
257
+ not be named like a node attribute Gradia consumes instead of
258
+ displaying it (e.g. "mark" or "layer"), as it would silently alter
259
+ the node instead of annotating it */
260
+ const checkDiagrams = (object, at, diagnose) => {
261
+ for (const [i, diagram] of diagramsOf(object).entries()) {
262
+ const base = Array.isArray(object.diagram) ? [...at, "diagram", i] : [...at, "diagram"];
263
+ for (const [j, key] of (diagram.properties ?? []).entries())
264
+ if (reservedAttributes.includes(key))
265
+ diagnose([...base, "properties", j], `diagram property "${key}" collides with the reserved Gradia node attribute "${key}"`);
266
+ }
267
+ };
268
+ /* check the multiplicity of an object (if any): it names a property
269
+ of the object kind and a valid regular expression */
270
+ const checkMultiple = (object, at, diagnose) => {
271
+ const multiple = object.multiple;
272
+ if (multiple === undefined)
273
+ return;
274
+ if (!(object.props ?? []).some((p) => p.name === multiple.property))
275
+ diagnose([...at, "multiple", "property"], `"multiple" property "${multiple.property}" is not a property of "${object.kind}"`);
276
+ try {
277
+ RegExp(multiple.pattern);
278
+ }
279
+ catch (err) {
280
+ diagnose([...at, "multiple", "pattern"], `invalid "multiple" pattern "${multiple.pattern}": ` +
281
+ (err instanceof Error ? err.message : String(err)));
282
+ }
283
+ };
284
+ /* check the property-driven badge of an object (if any): it names a
285
+ property of the object kind, and for an "enum" valued one each mapped
286
+ value is a member of it (else the mapping would never apply) */
287
+ const checkBadge = (object, at, diagnose) => {
288
+ const badge = object.badge;
289
+ if (badge === undefined || typeof badge === "string")
290
+ return;
291
+ const prop = (object.props ?? []).find((p) => p.name === badge.property);
292
+ if (prop === undefined) {
293
+ diagnose([...at, "badge", "property"], `"badge" property "${badge.property}" is not a property of "${object.kind}"`);
294
+ return;
295
+ }
296
+ let expr;
297
+ try {
298
+ expr = prop.value !== undefined ? compileValueExpr(prop.value) : undefined;
299
+ }
300
+ catch {
301
+ /* an invalid value expression is reported on its own */
302
+ }
303
+ if (expr?.kind === "enum")
304
+ for (const value of Object.keys(badge.map))
305
+ if (!expr.members.includes(value))
306
+ diagnose([...at, "badge", "map", value], `"badge" map value "${value}" is not a member of "${prop.value}"`);
307
+ };
193
308
  /* check the constraints of a structurally valid configuration which
194
309
  are beyond its schema: sibling objects have to stay distinctly
195
310
  resolvable, "file" fields are allowed on the first (artifact) level
@@ -197,8 +312,12 @@ const checkAutomaton = (object, at, diagnose) => {
197
312
  property value and marker expressions have to be syntactically
198
313
  valid, "referenced" and "coverage" entries have to be reference
199
314
  expressions, an "automaton" has to name existing child kinds and
200
- properties, and the names of non-artifact objects have to be valid
201
- patterns (as they are compiled into regular expressions) */
315
+ properties, a "badge" has to name an existing property and its
316
+ members, a "multiple" an existing property and a valid regular
317
+ expression, a diagram must not display a property named like a
318
+ reserved Gradia node attribute, and the names of non-artifact
319
+ objects have to be valid patterns (as they are compiled into
320
+ regular expressions) */
202
321
  const checkConstraints = (config, posOfPath) => {
203
322
  const diagnostics = new Array();
204
323
  const diagnose = (path, message) => {
@@ -226,6 +345,9 @@ const checkConstraints = (config, posOfPath) => {
226
345
  diagnose([...at, "file"], `"file" field "${object.file}" is not a relative path below the base directory`);
227
346
  checkProperties(object.props ?? [], at, diagnose);
228
347
  checkAutomaton(object, at, diagnose);
348
+ checkBadge(object, at, diagnose);
349
+ checkMultiple(object, at, diagnose);
350
+ checkDiagrams(object, at, diagnose);
229
351
  for (const field of ["referenced", "coverage"])
230
352
  for (const [j, entry] of (object[field] ?? []).entries()) {
231
353
  /* a reference coverage entry has to be a single
@@ -279,7 +401,8 @@ export const configStatistics = (config) => {
279
401
  /* load and validate the YAML schema configuration, merged in order out
280
402
  of its files (the later ones into the earlier ones), where every file
281
403
  has to be readable and syntactically valid on its own, while the
282
- merged result alone is validated */
404
+ merged result alone is validated, and report the artifacts several
405
+ files define plus the files contributing every node */
283
406
  export const loadConfig = (files) => {
284
407
  if (files.length === 0)
285
408
  throw new Error("no configuration files given");
@@ -314,8 +437,10 @@ export const loadConfig = (files) => {
314
437
  if (doc.errors.length === 0)
315
438
  docs.push({ file, doc, lines, raw: doc.toJS() });
316
439
  }
440
+ const overlaps = overlapsOf(docs);
441
+ const origins = originsOf(docs);
317
442
  if (diagnostics.length > 0)
318
- return { diagnostics };
443
+ return { diagnostics, overlaps, origins };
319
444
  /* merge the documents in order, out of unshared deep copies of
320
445
  the conversions, as the merge mutates its target while the raw
321
446
  ones have to stay intact for the path translation */
@@ -332,9 +457,9 @@ export const loadConfig = (files) => {
332
457
  diagnostics.push({ file, line, column, severity: "error",
333
458
  message: `invalid configuration: ${where}${issue.message}` });
334
459
  }
335
- return { diagnostics };
460
+ return { diagnostics, overlaps, origins };
336
461
  }
337
462
  /* check the constraints beyond the structural schema */
338
463
  diagnostics.push(...checkConstraints(result.output, posOfPath));
339
- return { config: diagnostics.length === 0 ? result.output : undefined, diagnostics };
464
+ return { config: diagnostics.length === 0 ? result.output : undefined, diagnostics, overlaps, origins };
340
465
  };
@@ -3,7 +3,7 @@
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, resolveUnique, resolveSet, chainOf, plainText } from "./specbook-link.js";
6
+ import { referenceRegex, resolveUnique, resolveSet, chainOf, referenceText } from "./specbook-link.js";
7
7
  import { compileValueExpr } from "./specbook-parse-value.js";
8
8
  /* the referencing objects of every referenced object, resolved from
9
9
  the names, property values, descriptions, and rationales of all
@@ -19,7 +19,7 @@ const referrerIndex = (index) => {
19
19
  if (object.description !== undefined)
20
20
  texts.push(object.description.description, object.description.rationale ?? "", object.description.elaboration ?? "");
21
21
  for (const text of texts)
22
- for (const m of plainText(text).matchAll(referenceRegex)) {
22
+ for (const m of referenceText(text).matchAll(referenceRegex)) {
23
23
  const target = resolveUnique(index, m[1].trim(), object).target;
24
24
  if (target !== undefined && !chain.includes(target))
25
25
  referrers.set(target, (referrers.get(target) ?? new Set()).add(object));
@@ -1,5 +1,5 @@
1
1
  import type { Spec, SpecObject } from "./specbook-format-spec.js";
2
- import type { Schema, SchemaDiagram } from "./specbook-format-schema.js";
2
+ import { type Schema, type SchemaDiagram } from "./specbook-format-schema.js";
3
3
  import type { ParseContext } from "./specbook-parse-common.js";
4
4
  import type { OmitAspect } from "./specbook-export-common.js";
5
5
  import { type Verbose } from "./specbook-verbose.js";
@@ -8,14 +8,16 @@ export interface DiagramError {
8
8
  object?: SpecObject;
9
9
  }
10
10
  export interface DiagramResult {
11
+ type: DiagramType;
11
12
  spec?: string;
12
13
  config?: SchemaDiagram["config"];
13
14
  columns?: number;
14
15
  errors: DiagramError[];
15
16
  }
16
- export declare const specDiagrams: (specification: Spec, config: Schema) => Map<SpecObject, DiagramResult>;
17
- export declare const omittedDiagrams: (specification: Spec, config: Schema, omit: Set<OmitAspect>) => Set<SpecObject>;
18
- export declare const renderDiagrams: (specification: Spec, config: Schema, verbose?: Verbose, omit?: Set<"diagram:graph" | "diagram:hub" | "diagram:grid" | "diagram:code" | "diagram:image" | "diagram:listing" | "diagram:1" | "diagram:2" | "diagram:3" | "text:long">) => Promise<Map<SpecObject, DiagramResult & {
17
+ export type DiagramType = NonNullable<SchemaDiagram["type"]>;
18
+ export declare const specDiagrams: (specification: Spec, config: Schema) => Map<SpecObject, DiagramResult[]>;
19
+ export declare const omittedDiagrams: (specification: Spec, config: Schema, omit: Set<OmitAspect>) => Set<DiagramResult>;
20
+ export declare const renderDiagrams: (specification: Spec, config: Schema, verbose?: Verbose, omit?: Set<"diagram:graph" | "diagram:hub" | "diagram:grid" | "diagram:state" | "diagram:layer" | "diagram:slice" | "diagram:tree" | "diagram:matrix" | "diagram:sequence" | "diagram:petri" | "diagram:gantt" | "diagram:code" | "diagram:image" | "diagram:listing" | "diagram:1" | "diagram:2" | "diagram:3" | "text:long">) => Promise<Map<SpecObject, (DiagramResult & {
19
21
  svg: string;
20
- }>>;
22
+ })[]>>;
21
23
  export declare const validateDiagrams: (ctx: ParseContext, specification: Spec, config: Schema) => void;