@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
@@ -4,8 +4,10 @@
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
6
  import { Gradia } from "@rse/gradia";
7
- import { referenceRegex, buildLinkIndex, resolveUnique, resolveSet, anchorPaths, expandReferences, plainText } from "./specbook-link.js";
7
+ import { diagramsOf } from "./specbook-format-schema.js";
8
+ import { referenceRegex, buildLinkIndex, resolveUnique, resolveSet, anchorPaths, chainOf, expandReferences, plainText, referenceText } from "./specbook-link.js";
8
9
  import { collectSchemas, collectParenProps } from "./specbook-parse-semantic.js";
10
+ import { referencedCoverage, specCoverage } from "./specbook-coverage.js";
9
11
  import { literal } from "./specbook-verbose.js";
10
12
  /* the single Wiki-style reference match (the non-global sibling of
11
13
  the imported, global "referenceRegex") */
@@ -49,7 +51,7 @@ const quoted = (text) => `"${text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"
49
51
  string otherwise */
50
52
  const atom = (text) => {
51
53
  const plain = text.replace(/\s+/g, " ").trim();
52
- return /^[^\s[\]():,">#]+$/.test(plain) && !plain.includes("--") ? plain : quoted(plain);
54
+ return /^[^\s[\]{}():,">#]+$/.test(plain) && !plain.includes("--") ? plain : quoted(plain);
53
55
  };
54
56
  /* render a text as a Gradia edge name or edge arity token
55
57
  (each with its own, more lenient character set) */
@@ -62,6 +64,12 @@ const configValue = (value) => {
62
64
  const text = String(value).replace(/\s+/g, " ").trim();
63
65
  return /^[^\s"]+$/.test(text) ? text : quoted(text);
64
66
  };
67
+ const messageKinds = {
68
+ call: "sync", sync: "sync",
69
+ signal: "async", async: "async",
70
+ return: "return", create: "create",
71
+ destroy: "destroy"
72
+ };
65
73
  /* the properties of an object, the synthetic one included */
66
74
  const propsOf = (parenProps, object) => {
67
75
  const synthetic = parenProps.get(object);
@@ -69,13 +77,43 @@ const propsOf = (parenProps, object) => {
69
77
  };
70
78
  /* the value of a single property of an object, by its key */
71
79
  const propValue = (parenProps, object, key) => propsOf(parenProps, object).find((property) => property.key === key)?.value;
80
+ /* the badge icon of an object: the fixed one of its object kind, or the
81
+ one the value of the configured property maps onto, else the default */
82
+ const badgeOf = (ctx, object) => {
83
+ const badge = ctx.schemas.get(object)?.badge;
84
+ if (badge === undefined || typeof badge === "string")
85
+ return badge;
86
+ const value = plainText(propValue(ctx.parenProps, object, badge.property) ?? "").trim();
87
+ return Object.hasOwn(badge.map, value) ? badge.map[value] : badge.default;
88
+ };
89
+ /* whether an object stands for several instances: the value of the
90
+ configured property of its object kind matches the configured pattern */
91
+ const isMultiple = (ctx, object) => {
92
+ const multiple = ctx.schemas.get(object)?.multiple;
93
+ if (multiple === undefined)
94
+ return false;
95
+ const value = propValue(ctx.parenProps, object, multiple.property);
96
+ return value !== undefined && new RegExp(multiple.pattern).test(plainText(value));
97
+ };
98
+ /* the initial tokens or the capacity of a place of a "petri" diagram,
99
+ or the duration or earliest start of a task of a "gantt" diagram:
100
+ the trimmed value of the property the option of the same name
101
+ names (if any) */
102
+ const optionValue = (ctx, diagram, node, key) => {
103
+ const name = diagram[key];
104
+ const value = name !== undefined ? propValue(ctx.parenProps, node, name) : undefined;
105
+ return value !== undefined ? plainText(value).trim() : undefined;
106
+ };
72
107
  /* generate the Gradia spec text of a derived diagram, opened by a
73
108
  "#type" directive and the configured "#config" directives so every
74
109
  embedded spec is self-contained: one node statement per object (the
75
110
  unique anchor path as the node id, the object name as the displayed
76
- label, the container node as the parent) and one edge statement per
77
- derived edge */
78
- const renderSpec = (ctx, diagram, type, center, centerUrl, nodes, edges, nesting) => {
111
+ label, the container node as the parent, band, or cluster, the badge icon
112
+ and the multiplicity of its object kind, the "incomplete" mark of an
113
+ uncovered object, the pseudo-state kind of a synthetic pseudo-state,
114
+ the place or transition kind of a Petri net node, the duration and
115
+ start of a task) and one edge statement per derived edge */
116
+ const renderSpec = (ctx, diagram, type, center, centerUrl, nodes, edges, nesting, pseudo, places) => {
79
117
  const { index, anchors, positions, parenProps } = ctx;
80
118
  const lines = [`#type ${type}`];
81
119
  const config = { ...diagramPresets, ...diagram.config };
@@ -86,9 +124,10 @@ const renderSpec = (ctx, diagram, type, center, centerUrl, nodes, edges, nesting
86
124
  /* the explicit top-down order of an "ordered" diagram: the members
87
125
  of a container share one row (which Gradia wraps), while the
88
126
  top-level nodes stack in document order, a synthetic container
89
- taking the position of its first member */
127
+ taking the position of its first member (a "matrix" or "tree"
128
+ knows no container rows, so there every node takes its own position) */
90
129
  const orderOf = (node) => {
91
- if (nesting.parentOf.has(node))
130
+ if (nesting.parentOf.has(node) && type !== "matrix" && type !== "tree")
92
131
  return 0;
93
132
  let order = positions.get(node) ?? Number.MAX_SAFE_INTEGER;
94
133
  for (const [member, container] of nesting.parentOf)
@@ -109,12 +148,61 @@ const renderSpec = (ctx, diagram, type, center, centerUrl, nodes, edges, nesting
109
148
  if (type === "hub" ? node === center : node.primary === true)
110
149
  attrs.push("primary: true");
111
150
  const container = nesting.parentOf.get(node);
112
- if (container !== undefined)
151
+ const banded = type === "layer" || type === "slice";
152
+ if (banded || type === "matrix" || type === "gantt") {
153
+ /* a "layer", "slice", "matrix", or "gantt" diagram knows no
154
+ containers: in a "layer" or "slice" one the container
155
+ names the band of the node and the containers enclosing
156
+ it the path of the clusters grouping that band, while in
157
+ a "matrix" one a container (itself a row and column there)
158
+ and all containers of a node form the path of its partition
159
+ (in a "gantt" one just the containers of a task, as a
160
+ container is no task) */
161
+ if (banded && container !== undefined)
162
+ attrs.push(`${type}: ${atom(plainText(container.name))}`);
163
+ const clusters = new Array();
164
+ for (let p = banded ? (container !== undefined ? nesting.parentOf.get(container) : undefined) :
165
+ nesting.containers.has(node) ? node : container; p !== undefined; p = nesting.parentOf.get(p))
166
+ clusters.unshift(plainText(p.name));
167
+ if (clusters.length > 0)
168
+ attrs.push(`cluster: ${atom(clusters.join("/"))}`);
169
+ }
170
+ else if (container !== undefined && type !== "tree")
113
171
  attrs.push(`parent: ${atom(anchors.get(container) ?? container.id)}`);
114
172
  if (diagram.nest?.layout !== undefined && nesting.containers.has(node))
115
173
  attrs.push(`container: ${diagram.nest.layout}`);
116
174
  if (diagram.ordered === true)
117
175
  attrs.push(`order: ${orderOf(node)}`);
176
+ const badge = badgeOf(ctx, node);
177
+ if (badge !== undefined)
178
+ attrs.push(`badge: ${badge}`);
179
+ if (isMultiple(ctx, node))
180
+ attrs.push("multiple: true");
181
+ const marking = diagram.markUncovered ?? "none";
182
+ if (((marking === "referenced" || marking === "all") && ctx.uncovered.referenced.has(node))
183
+ || ((marking === "coverage" || marking === "all") && ctx.uncovered.coverage.has(node)))
184
+ attrs.push("mark: incomplete");
185
+ const state = pseudo.get(node);
186
+ if (state !== undefined)
187
+ attrs.push(`state: ${state}`);
188
+ /* the kind of a node of a "petri" diagram, plus the initial
189
+ tokens and the capacity of a place, and the duration and the
190
+ earliest start of a task of a "gantt" diagram */
191
+ if (places !== undefined) {
192
+ attrs.push(`petri: ${places.has(node) ? "place" : "transition"}`);
193
+ if (places.has(node))
194
+ for (const key of ["tokens", "capacity"]) {
195
+ const value = optionValue(ctx, diagram, node, key);
196
+ if (value !== undefined)
197
+ attrs.push(`${key}: ${value}`);
198
+ }
199
+ }
200
+ if (type === "gantt")
201
+ for (const key of ["duration", "start"]) {
202
+ const value = optionValue(ctx, diagram, node, key);
203
+ if (value !== undefined)
204
+ attrs.push(`${key}: ${value}`);
205
+ }
118
206
  /* attach the values of the configured properties as Gradia
119
207
  key/value attributes (a node lacking a property is fine, as
120
208
  the node set can mix objects of different kinds), with every
@@ -135,17 +223,29 @@ const renderSpec = (ctx, diagram, type, center, centerUrl, nodes, edges, nesting
135
223
  const op = "--" +
136
224
  (edge.name !== undefined && edge.name !== "" ? `(${nameToken(edge.name)})--` : "") + ">" +
137
225
  (edge.arity !== undefined && edge.arity !== "" ? `[${arityToken(edge.arity)}]` : "");
226
+ const attrs = new Array();
227
+ if (edge.kind !== undefined && edge.kind !== "sync")
228
+ attrs.push(`kind: ${edge.kind}`);
229
+ if (edge.duration !== undefined && edge.duration !== "")
230
+ attrs.push(`duration: ${atom(edge.duration)}`);
231
+ if (edge.time !== undefined && edge.time !== "")
232
+ attrs.push(`time: ${atom(edge.time)}`);
138
233
  lines.push(`${atom(anchors.get(edge.source) ?? edge.source.id)} ${op} ` +
139
- atom(anchors.get(edge.target) ?? edge.target.id));
234
+ atom(anchors.get(edge.target) ?? edge.target.id) +
235
+ (attrs.length > 0 ? ` { ${attrs.join(", ")} }` : ""));
140
236
  }
141
237
  }
142
238
  return lines.join("\n") + "\n";
143
239
  };
144
240
  /* derive the reference edges of a diagram from the "[[...]]" references
145
- of the node objects (per the "links" selection, named with the
146
- property key when "labeled", and for a "deep" diagram also of their
147
- descendants, with the reference count as the arity), with every
148
- target lifted to its nearest node */
241
+ of the node objects (per the "links" selection: all property values,
242
+ also the texts, or the values of the listed properties only, named
243
+ with the property key when "labeled", for a "deep" diagram also of
244
+ their descendants, with the reference count as the arity, and also
245
+ of their descendants of the "descend" kinds, uncounted and named with
246
+ the descendant kind when "labeled"), with every target lifted to its
247
+ nearest node, and with the edges of the "reverse" properties pointing
248
+ back onto the node */
149
249
  const deriveReferenceEdges = (ctx, diagram, nodes, nodeSet) => {
150
250
  const { index, anchors, parents, parenProps } = ctx;
151
251
  const edges = new Array();
@@ -159,22 +259,34 @@ const deriveReferenceEdges = (ctx, diagram, nodes, nodeSet) => {
159
259
  };
160
260
  for (const node of nodes) {
161
261
  /* the reference-carrying objects of the node: the node itself
162
- and, for a "deep" diagram, its descendants (halting at
163
- the descendants which are nodes on their own) */
262
+ and, for a "deep" diagram, its descendants, else its
263
+ descendants of the "descend" kinds (each halting at the
264
+ descendants which are nodes on their own) */
164
265
  const objects = new Array();
266
+ const descended = new Set();
165
267
  const walk = (object) => {
166
268
  objects.push(object);
167
- if (diagram.deep === true)
168
- for (const child of object.children)
169
- if (!nodeSet.has(child))
170
- walk(child);
269
+ for (const child of object.children) {
270
+ if (nodeSet.has(child))
271
+ continue;
272
+ if (diagram.deep === true)
273
+ walk(child);
274
+ else if ((diagram.descend ?? []).includes(child.kind)) {
275
+ descended.add(child);
276
+ walk(child);
277
+ }
278
+ }
171
279
  };
172
280
  walk(node);
173
281
  const counts = new Map();
174
282
  for (const object of objects) {
175
- const texts = propsOf(parenProps, object).map((property) => ({
283
+ const texts = propsOf(parenProps, object)
284
+ .filter((property) => !Array.isArray(diagram.links) || diagram.links.includes(property.key))
285
+ .map((property) => ({
176
286
  text: property.value,
177
- name: diagram.labeled === true ? property.key.toLowerCase() : undefined
287
+ name: diagram.labeled !== true ? undefined :
288
+ descended.has(object) ? object.kind.toLowerCase() : property.key.toLowerCase(),
289
+ reversed: (diagram.reverse ?? []).includes(property.key)
178
290
  }));
179
291
  if (diagram.links === "all" && object.description !== undefined) {
180
292
  texts.push({ text: object.description.description });
@@ -183,19 +295,20 @@ const deriveReferenceEdges = (ctx, diagram, nodes, nodeSet) => {
183
295
  if (object.description.elaboration !== undefined)
184
296
  texts.push({ text: object.description.elaboration });
185
297
  }
186
- for (const { text, name } of texts)
187
- for (const m of plainText(text).matchAll(referenceRegex)) {
298
+ for (const { text, name, reversed = false } of texts)
299
+ for (const m of referenceText(text).matchAll(referenceRegex)) {
188
300
  const target = lift(resolveUnique(index, m[1].trim(), object).target);
189
301
  if (target !== undefined && target !== node) {
190
- const key = `${anchors.get(target) ?? target.id}\u0000${name ?? ""}`;
191
- const entry = counts.get(key) ?? { target, name, count: 0 };
302
+ const key = `${anchors.get(target) ?? target.id}\u0000${name ?? ""}\u0000${reversed}`;
303
+ const entry = counts.get(key) ?? { target, name, reversed, count: 0 };
192
304
  entry.count++;
193
305
  counts.set(key, entry);
194
306
  }
195
307
  }
196
308
  }
197
- for (const { target, name, count } of counts.values())
198
- edges.push({ source: node, target, name,
309
+ /* an edge of a "reverse" property points back onto the node */
310
+ for (const { target, name, reversed, count } of counts.values())
311
+ edges.push({ source: reversed ? target : node, target: reversed ? node : target, name,
199
312
  arity: diagram.deep === true ? String(count) : undefined });
200
313
  }
201
314
  return edges;
@@ -203,52 +316,195 @@ const deriveReferenceEdges = (ctx, diagram, nodes, nodeSet) => {
203
316
  /* derive the object edges of a diagram from the edge objects by
204
317
  convention (source: parent object, target: first reference in the
205
318
  property values, name: object name, arity: "ARITY" property),
206
- overridable via "edgeSource"/"edgeTarget"/"edgeArity" */
207
- const deriveObjectEdges = (ctx, diagram, nodeSet, edgeObjects, errors) => {
319
+ overridable via "edgeSource"/"edgeTarget"/"edgeArity", where a
320
+ configured target property carrying several references (a "list"
321
+ valued one) yields one edge per resolvable target, where the
322
+ "edgeGuard" property value follows the name in brackets, and where
323
+ the "edgeKind", "edgeDuration", and "edgeTime" property values make
324
+ the message kind and the timing constraints of a "sequence" diagram */
325
+ const deriveObjectEdges = (ctx, diagram, admits, edgeObjects, errors) => {
208
326
  const { index, parents, parenProps } = ctx;
209
327
  const edges = new Array();
210
- /* resolve the node an edge object references through a named
211
- property, or through its first single-reference property when
212
- no name is configured (skipping the source property) */
213
- const edgeNode = (edgeObject, key) => {
328
+ /* resolve the nodes an edge object references through a named
329
+ property (all of its references), or through the first reference
330
+ of its first single-reference property when no name is configured
331
+ (skipping the source property) */
332
+ const edgeNodes = (edgeObject, key) => {
214
333
  const value = key !== undefined ?
215
334
  propValue(parenProps, edgeObject, key) :
216
335
  propsOf(parenProps, edgeObject).filter((property) => property.key !== diagram.edgeSource)
217
- .map((property) => property.value).find((v) => referenceOnce.test(v));
218
- const reference = value !== undefined ? plainText(value).match(referenceOnce)?.[1].trim() : undefined;
219
- return reference !== undefined ? resolveUnique(index, reference, edgeObject).target : undefined;
336
+ .map((property) => property.value).find((v) => referenceOnce.test(referenceText(v)));
337
+ if (value === undefined)
338
+ return [];
339
+ const references = Array.from(referenceText(value).matchAll(referenceRegex), (m) => m[1].trim());
340
+ return (key !== undefined ? references : references.slice(0, 1))
341
+ .map((reference) => resolveUnique(index, reference, edgeObject).target)
342
+ .filter((target) => target !== undefined);
220
343
  };
221
344
  for (const edgeObject of edgeObjects) {
222
345
  const source = diagram.edgeSource !== undefined ?
223
- edgeNode(edgeObject, diagram.edgeSource) : parents.get(edgeObject);
224
- const target = edgeNode(edgeObject, diagram.edgeTarget);
346
+ edgeNodes(edgeObject, diagram.edgeSource)[0] : parents.get(edgeObject);
347
+ const targets = edgeNodes(edgeObject, diagram.edgeTarget);
225
348
  if (source === undefined) {
226
349
  errors.push({ object: edgeObject, reason: diagram.edgeSource !== undefined ?
227
350
  "carries no resolvable source reference" :
228
351
  "has no parent object as source" });
229
352
  continue;
230
353
  }
231
- if (target === undefined) {
354
+ if (targets.length === 0) {
232
355
  errors.push({ object: edgeObject, reason: "carries no resolvable target reference" });
233
356
  continue;
234
357
  }
235
- if (!nodeSet.has(source) || !nodeSet.has(target))
236
- continue;
237
358
  const arity = propValue(parenProps, edgeObject, diagram.edgeArity ?? "ARITY");
238
- edges.push({ source, target, name: plainText(edgeObject.name),
239
- arity: arity !== undefined ? plainText(arity) : undefined });
359
+ const guard = diagram.edgeGuard !== undefined ?
360
+ propValue(parenProps, edgeObject, diagram.edgeGuard) : undefined;
361
+ const name = plainText(edgeObject.name) + (guard !== undefined ?
362
+ ` [${plainText(expandReferences(guard, (reference) => resolveUnique(index, reference, edgeObject).target?.name ?? reference))}]` : "");
363
+ const text = (key) => {
364
+ const value = key !== undefined ? propValue(parenProps, edgeObject, key) : undefined;
365
+ return value !== undefined ? plainText(value).trim() : undefined;
366
+ };
367
+ const label = text(diagram.edgeKind);
368
+ const kind = label !== undefined && Object.hasOwn(messageKinds, label.toLowerCase()) ?
369
+ messageKinds[label.toLowerCase()] : undefined;
370
+ if (label !== undefined && kind === undefined) {
371
+ errors.push({ object: edgeObject, reason: `carries the unknown message kind "${label}"` });
372
+ continue;
373
+ }
374
+ for (const target of targets)
375
+ if (admits(source) && admits(target))
376
+ edges.push({ source, target, name,
377
+ arity: arity !== undefined ? plainText(arity) : undefined,
378
+ kind, duration: text(diagram.edgeDuration), time: text(diagram.edgeTime), object: edgeObject });
240
379
  }
241
380
  return edges;
242
381
  };
382
+ /* validate the messages of a "sequence" diagram exactly like Gradia
383
+ does, so an invalid message becomes a diagnostic on its edge object
384
+ instead of failing the rendering: a participant is created at most
385
+ once, and only by the first message it takes part in, takes part in
386
+ no further message once destroyed, and a duration constraint needs
387
+ a call answered by a return (the next return from its callee to its
388
+ caller, answering the innermost such call still open on the callee) */
389
+ const validateMessages = (edges, errors) => {
390
+ const seen = new Set();
391
+ const destroyed = new Set();
392
+ const pending = new Map();
393
+ const answered = new Set();
394
+ edges.forEach((edge, i) => {
395
+ const kind = edge.kind ?? "sync";
396
+ for (const party of [edge.source, edge.target])
397
+ if (destroyed.has(party))
398
+ errors.push({ object: edge.object,
399
+ reason: `involves participant "${party.name}" after its destruction` });
400
+ if (kind === "create" && (edge.source === edge.target || seen.has(edge.target)))
401
+ errors.push({ object: edge.object, reason: `cannot create participant "${edge.target.name}", ` +
402
+ "as only the first message a participant takes part in can create it" });
403
+ seen.add(edge.source);
404
+ seen.add(edge.target);
405
+ if (kind === "destroy")
406
+ destroyed.add(edge.target);
407
+ if (kind === "sync" || kind === "create")
408
+ pending.set(edge.target, [...(pending.get(edge.target) ?? []), i]);
409
+ else if (kind === "return") {
410
+ const calls = pending.get(edge.source) ?? [];
411
+ const k = calls.findLastIndex((call) => edges[call].source === edge.target);
412
+ if (k >= 0) {
413
+ answered.add(calls[k]);
414
+ calls.length = k;
415
+ }
416
+ }
417
+ });
418
+ edges.forEach((edge, i) => {
419
+ if (edge.duration !== undefined && edge.duration !== "" && !answered.has(i))
420
+ errors.push({ object: edge.object,
421
+ reason: "carries a duration, but is not a call answered by a return" });
422
+ });
423
+ };
424
+ /* validate the net of a "petri" diagram exactly like Gradia does, so
425
+ an invalid net becomes a diagnostic on the offending object instead
426
+ of failing the rendering: the initial tokens of a place are an
427
+ integer of at least 0 and its capacity one of at least 1, holding
428
+ them, and every arc connects a place with a transition */
429
+ const validatePetri = (ctx, diagram, nodes, edges, places, errors) => {
430
+ for (const node of nodes) {
431
+ if (!places.has(node))
432
+ continue;
433
+ const numbers = {};
434
+ for (const [key, min] of [["tokens", 0], ["capacity", 1]]) {
435
+ const value = optionValue(ctx, diagram, node, key);
436
+ if (value === undefined)
437
+ continue;
438
+ if (!(/^\d+$/).test(value) || Number(value) < min)
439
+ errors.push({ object: node, reason: `carries the invalid ${key} "${value}" ` +
440
+ `(expected an integer of at least ${min})` });
441
+ else
442
+ numbers[key] = Number(value);
443
+ }
444
+ if (numbers.tokens !== undefined && numbers.capacity !== undefined && numbers.tokens > numbers.capacity)
445
+ errors.push({ object: node, reason: "carries more tokens than its capacity" });
446
+ }
447
+ for (const edge of edges)
448
+ if (places.has(edge.source) === places.has(edge.target))
449
+ errors.push({ object: edge.object ?? edge.source,
450
+ reason: `connects ${places.has(edge.source) ? "place" : "transition"} "${edge.source.name}" ` +
451
+ `with ${places.has(edge.target) ? "place" : "transition"} "${edge.target.name}", ` +
452
+ "but an arc connects a place with a transition only" });
453
+ };
454
+ /* validate the schedule of a "gantt" diagram exactly like Gradia does,
455
+ so an invalid plan becomes a diagnostic on the offending object
456
+ instead of failing the rendering: the duration and the earliest
457
+ start of a task are non-negative numbers, and the dependencies form
458
+ no cycle (reported on the first task of the cycle found) */
459
+ const validateGantt = (ctx, diagram, nodes, edges, errors) => {
460
+ for (const node of nodes)
461
+ for (const key of ["duration", "start"]) {
462
+ const value = optionValue(ctx, diagram, node, key);
463
+ if (value !== undefined && !(/^\d+(?:\.\d+)?$/).test(value))
464
+ errors.push({ object: node, reason: `carries the invalid ${key} "${value}" ` +
465
+ "(expected a non-negative number)" });
466
+ }
467
+ const successors = new Map();
468
+ for (const edge of edges)
469
+ successors.set(edge.source, [...(successors.get(edge.source) ?? []), edge.target]);
470
+ const done = new Set();
471
+ const path = new Set();
472
+ const visit = (node) => {
473
+ if (path.has(node))
474
+ return true;
475
+ if (done.has(node))
476
+ return false;
477
+ path.add(node);
478
+ const cyclic = (successors.get(node) ?? []).some(visit);
479
+ path.delete(node);
480
+ done.add(node);
481
+ return cyclic;
482
+ };
483
+ for (const node of nodes)
484
+ if (visit(node)) {
485
+ errors.push({ object: node, reason: "takes part in a cycle of dependencies" });
486
+ break;
487
+ }
488
+ };
243
489
  /* derive the edges of a diagram: the reference edges of the node
244
490
  objects, the object edges of the edge objects, and the containment
245
491
  edges of the object hierarchy (a "grid" diagram is edge-less by
246
- definition, so no edges are derived at all) */
492
+ definition, so no edges are derived at all, and a "sequence" diagram
493
+ knows the object edges, its messages, only) */
247
494
  const deriveEdges = (ctx, diagram, type, nodes, nodeSet, edgeObjects, errors) => {
248
495
  const edges = new Array();
249
- if (type !== "grid") {
496
+ if (type === "sequence") {
497
+ /* a "sequence" diagram draws the edge objects as its messages
498
+ alone, as references and the object nesting carry no order,
499
+ between any objects unless "nodes" restricts its participants */
500
+ for (const field of ["links", "hierarchy", "deep", "descend", "reverse", "labeled"])
501
+ if (diagram[field] !== undefined && diagram[field] !== false)
502
+ errors.push({ reason: `"sequence" diagram cannot carry a "${field}" configuration` });
503
+ edges.push(...deriveObjectEdges(ctx, diagram, (object) => diagram.nodes === undefined || nodeSet.has(object), edgeObjects, errors));
504
+ }
505
+ else if (type !== "grid") {
250
506
  edges.push(...deriveReferenceEdges(ctx, diagram, nodes, nodeSet));
251
- edges.push(...deriveObjectEdges(ctx, diagram, nodeSet, edgeObjects, errors));
507
+ edges.push(...deriveObjectEdges(ctx, diagram, (object) => nodeSet.has(object), edgeObjects, errors));
252
508
  /* derive the containment edges from the object hierarchy, as
253
509
  the nesting of the objects carries no "[[...]]" reference */
254
510
  if (diagram.hierarchy === true)
@@ -259,7 +515,7 @@ const deriveEdges = (ctx, diagram, type, nodes, nodeSet, edgeObjects, errors) =>
259
515
  }
260
516
  }
261
517
  else {
262
- for (const field of ["edges", "hierarchy", "deep", "labeled", "onlyConnected"])
518
+ for (const field of ["edges", "hierarchy", "deep", "descend", "reverse", "labeled", "onlyConnected"])
263
519
  if (diagram[field] !== undefined && diagram[field] !== false)
264
520
  errors.push({ reason: "\"grid\" diagram cannot carry " +
265
521
  `${(/^[aeiou]/).test(field) ? "an" : "a"} "${field}" configuration` });
@@ -408,25 +664,36 @@ const deriveCenterEdges = (ctx, cfg, nodes, center, errors) => {
408
664
  };
409
665
  /* derive the nesting of the nodes into container nodes: per node the
410
666
  first configured property carrying exactly one resolvable reference
411
- names its container (added to the node set when absent, and nested
412
- on its own in turn), else its parent object when part of the node
413
- set, else a synthetic container per object kind; a node nested into
414
- itself or into a cycle stays un-nested and is reported */
415
- const deriveNesting = (ctx, nest, nodes, nodeSet, errors) => {
667
+ (or, with "multiple", at least one, the further ones collected
668
+ apart) names its container (added to the node set when absent, and
669
+ nested on its own in turn), else its parent object when part of the
670
+ node set, else a synthetic container per object kind; a node nested
671
+ into itself or into a cycle stays un-nested and is reported */
672
+ const deriveNesting = (ctx, nest, nodes, nodeSet, errors, multiple = false) => {
416
673
  const { index, parents, parenProps } = ctx;
417
674
  const parentOf = new Map();
675
+ const furtherOf = new Map();
418
676
  const byKind = new Map();
677
+ const include = (container) => {
678
+ if (!nodeSet.has(container)) {
679
+ nodes.push(container);
680
+ nodeSet.add(container);
681
+ }
682
+ };
419
683
  /* the iteration also visits the containers appended underway */
420
684
  for (const node of nodes) {
421
685
  let container;
686
+ let further = new Array();
422
687
  for (const key of nest.properties ?? []) {
423
688
  const value = propValue(parenProps, node, key);
424
- const refs = value !== undefined ? Array.from(plainText(value).matchAll(referenceRegex)) : [];
425
- if (refs.length !== 1)
426
- continue;
427
- container = resolveUnique(index, refs[0][1].trim(), node).target;
428
- if (container !== undefined)
689
+ const refs = value !== undefined ? Array.from(referenceText(value).matchAll(referenceRegex)) : [];
690
+ const targets = (multiple || refs.length === 1 ? refs : [])
691
+ .map((ref) => resolveUnique(index, ref[1].trim(), node).target)
692
+ .filter((target) => target !== undefined);
693
+ if (targets.length > 0) {
694
+ [container, ...further] = targets;
429
695
  break;
696
+ }
430
697
  }
431
698
  if (container === undefined && nest.parent === true) {
432
699
  const parent = parents.get(node);
@@ -452,12 +719,13 @@ const deriveNesting = (ctx, nest, nodes, nodeSet, errors) => {
452
719
  continue;
453
720
  }
454
721
  parentOf.set(node, container);
455
- if (!nodeSet.has(container)) {
456
- nodes.push(container);
457
- nodeSet.add(container);
722
+ include(container);
723
+ if (further.length > 0) {
724
+ furtherOf.set(node, further);
725
+ further.forEach(include);
458
726
  }
459
727
  }
460
- return { parentOf, containers: new Set(parentOf.values()) };
728
+ return { parentOf, containers: new Set(parentOf.values()), furtherOf };
461
729
  };
462
730
  /* lift the edges crossing a container boundary onto the containers:
463
731
  an end is replaced by its outermost ancestor container not enclosing
@@ -516,6 +784,8 @@ const deriveNodes = (index, object, diagram, errors) => {
516
784
  walk(object);
517
785
  return nodes;
518
786
  };
787
+ /* the end objects of a list of edges, in the order of their first occurrence */
788
+ const endsOf = (edges) => new Set(edges.flatMap((edge) => [edge.source, edge.target]));
519
789
  /* project a "hub" diagram onto its center object: only the edges
520
790
  incident to the center and only the nodes connected through them
521
791
  remain, as Gradia requires exactly this constrained topology, and
@@ -525,33 +795,45 @@ const deriveNodes = (index, object, diagram, errors) => {
525
795
  target of its self-loop, too */
526
796
  const projectHub = (nodes, edges, center) => {
527
797
  const incident = edges.filter((edge) => edge.source === center || edge.target === center);
528
- const connected = new Set([center]);
529
- for (const edge of incident) {
530
- connected.add(edge.source);
531
- connected.add(edge.target);
532
- }
798
+ const connected = endsOf(incident).add(center);
533
799
  const columns = 1 +
534
800
  (incident.some((edge) => edge.target === center && edge.source !== center) ? 1 : 0) +
535
801
  (incident.some((edge) => edge.source === center) ? 1 : 0);
536
802
  return { nodes: nodes.filter((node) => connected.has(node)), edges: incident, columns };
537
803
  };
538
804
  /* derive the Gradia spec of a single object from its "diagram:"
539
- schema configuration (the returned spec is absent whenever the
540
- configured diagram situation is invalid, the node set is empty,
541
- or the diagram collapses onto a single node) */
542
- const deriveDiagram = (ctx, object, diagram) => {
543
- const { index, anchors } = ctx;
805
+ schema configuration and its optional "automaton:" one (the
806
+ returned spec is absent whenever the configured diagram situation
807
+ is invalid, the node set is empty, or the diagram collapses onto a
808
+ single node) */
809
+ const deriveDiagram = (ctx, object, diagram, automaton) => {
810
+ const { index, anchors, parenProps } = ctx;
544
811
  const type = diagram.type ?? "graph";
545
812
  const errors = new Array();
546
813
  /* determine the node objects and the edge objects (default: none),
547
- with the edge objects never acting as nodes themselves */
548
- let nodes = deriveNodes(index, object, diagram, errors);
549
- const edgeObjects = diagram.edges !== undefined ? resolvePatterns(index, errors, diagram.edges, "edges") : [];
814
+ with the edge objects never acting as nodes themselves, where a
815
+ "local" diagram keeps only the matches of its patterns below
816
+ the object itself (as the patterns match corpus-wide), and where
817
+ a "sequence" diagram without "nodes" takes its participants from
818
+ its messages instead (see below) */
819
+ let nodes = type === "sequence" && diagram.nodes === undefined ? [] :
820
+ deriveNodes(index, object, diagram, errors);
821
+ let edgeObjects = diagram.edges !== undefined ? resolvePatterns(index, errors, diagram.edges, "edges") : [];
822
+ if (diagram.local === true) {
823
+ const below = (match) => chainOf(index, match).includes(object);
824
+ nodes = nodes.filter(below);
825
+ edgeObjects = edgeObjects.filter(below);
826
+ }
550
827
  const edgeSet = new Set(edgeObjects);
551
828
  nodes = nodes.filter((node) => !edgeSet.has(node));
552
829
  const nodeSet = new Set(nodes);
553
- /* derive the edges of the diagram */
830
+ /* derive the edges of the diagram, with the message kinds and the
831
+ timing constraints known to a "sequence" diagram only */
554
832
  let edges = deriveEdges(ctx, diagram, type, nodes, nodeSet, edgeObjects, errors);
833
+ if (type !== "sequence")
834
+ for (const field of ["edgeKind", "edgeDuration", "edgeTime"])
835
+ if (diagram[field] !== undefined)
836
+ errors.push({ reason: `"${type}" diagram cannot carry an "${field}" configuration` });
555
837
  /* determine the center of a "hub" diagram (the only shape
556
838
  carrying a center at all) */
557
839
  if (type !== "hub") {
@@ -563,15 +845,25 @@ const deriveDiagram = (ctx, object, diagram) => {
563
845
  const derived = type === "hub" ?
564
846
  deriveCenter(ctx, object, diagram, nodes, nodeSet, errors) : { center: object };
565
847
  if (derived === undefined)
566
- return { errors };
848
+ return { type, errors };
567
849
  const { center, centerUrl } = derived;
568
850
  /* synthesize the center edges of a "hub" diagram, as the nodes
569
851
  carry no "[[...]]" reference to a synthetic center, and only
570
852
  then deduplicate all edges, as a center edge of a real center
571
- can coincide with a reference edge of a node onto it */
853
+ can coincide with a reference edge of a node onto it (except
854
+ the messages of a "sequence" diagram, which may repeat) */
572
855
  if (type === "hub" && diagram.centerEdges !== undefined)
573
856
  edges.push(...deriveCenterEdges(ctx, diagram.centerEdges, nodes, center, errors));
574
- edges = dedupEdges(edges, anchors);
857
+ if (type !== "sequence")
858
+ edges = dedupEdges(edges, anchors);
859
+ /* the participants of a "sequence" diagram line up in the order
860
+ their messages first involve them, the uninvolved ones dropped */
861
+ if (type === "sequence") {
862
+ nodes = Array.from(endsOf(edges));
863
+ if (diagram.ordered === true)
864
+ errors.push({ reason: "\"sequence\" diagram cannot carry an \"ordered\" configuration" });
865
+ validateMessages(edges, errors);
866
+ }
575
867
  /* project a "hub" diagram onto its center object, which has
576
868
  to be part of the node set */
577
869
  let columns;
@@ -582,37 +874,98 @@ const deriveDiagram = (ctx, object, diagram) => {
582
874
  ({ nodes, edges, columns } = projectHub(nodes, edges, center));
583
875
  }
584
876
  /* nest the nodes into container nodes (except in a "hub" diagram,
585
- where every container would need a primary node of its own),
586
- dropping the edges between a container and its own members,
587
- which Gradia rejects, as the nesting already shows them */
588
- let nesting = { parentOf: new Map(), containers: new Set() };
877
+ where every container would need a primary node of its own, in a
878
+ "state" diagram, where every container would be a composite state
879
+ needing an initial state of its own, and in a "sequence" or "petri"
880
+ diagram, whose nodes nest not at all), dropping the edges between
881
+ a container and its own members, which Gradia rejects, as the
882
+ nesting already shows them */
883
+ let nesting = { parentOf: new Map(), containers: new Set(), furtherOf: new Map() };
589
884
  if (diagram.nest !== undefined) {
590
- if (type === "hub")
591
- errors.push({ reason: "\"hub\" diagram cannot carry a \"nest\" configuration" });
885
+ if (type === "hub" || type === "state" || type === "sequence" || type === "petri")
886
+ errors.push({ reason: `"${type}" diagram cannot carry a "nest" configuration` });
592
887
  else {
593
- nesting = deriveNesting(ctx, diagram.nest, nodes, nodeSet, errors);
594
- if (diagram.nest.crossing !== undefined && diagram.nest.crossing !== "nodes")
888
+ nesting = deriveNesting(ctx, diagram.nest, nodes, nodeSet, errors, type === "tree");
889
+ const banded = type === "layer" || type === "slice" || type === "gantt";
890
+ if ((banded || type === "tree" || type === "matrix") && diagram.nest.layout !== undefined)
891
+ errors.push({ reason: `"${type}" diagram cannot carry a "nest" layout configuration` });
892
+ if ((banded || type === "tree") && diagram.nest.crossing !== undefined && diagram.nest.crossing !== "nodes")
893
+ errors.push({ reason: `"${type}" diagram cannot carry a "nest" crossing configuration` });
894
+ if (banded) {
895
+ /* a "layer" or "slice" diagram draws its containers as
896
+ bands and cluster groups, a "gantt" one as the
897
+ partitions of its rows, instead of as nodes, so they
898
+ drop the edges ending at one */
899
+ nodes = nodes.filter((node) => !nesting.containers.has(node));
900
+ edges = edges.filter((edge) => !nesting.containers.has(edge.source)
901
+ && !nesting.containers.has(edge.target));
902
+ }
903
+ else if (type !== "tree" && diagram.nest.crossing !== undefined && diagram.nest.crossing !== "nodes")
595
904
  edges = liftEdges(edges, nesting.parentOf, diagram.nest.crossing, anchors);
596
- edges = edges.filter((edge) => nesting.parentOf.get(edge.source) !== edge.target
597
- && nesting.parentOf.get(edge.target) !== edge.source);
905
+ /* a "matrix" diagram keeps its containers as regular rows
906
+ and columns (only partitioning their members), so it
907
+ keeps their edges onto their own members, too */
908
+ if (type !== "matrix")
909
+ edges = edges.filter((edge) => nesting.parentOf.get(edge.source) !== edge.target
910
+ && nesting.parentOf.get(edge.target) !== edge.source);
911
+ /* a "tree" diagram draws the nesting as its parent-to-child
912
+ edges (the containers staying nodes), placed first, as the
913
+ first edge into a node makes its source the parent, while
914
+ every other edge (the ones from the further containers of
915
+ a node included) becomes a dashed cross link */
916
+ if (type === "tree")
917
+ edges = [
918
+ ...Array.from(nesting.parentOf, ([member, container]) => ({ source: container, target: member })),
919
+ ...Array.from(nesting.furtherOf).flatMap(([member, further]) => further.map((container) => ({ source: container, target: member }))),
920
+ ...edges
921
+ ];
598
922
  }
599
923
  }
924
+ if ((type === "layer" || type === "slice") && diagram.ordered === true)
925
+ errors.push({ reason: `"${type}" diagram cannot carry an "ordered" configuration` });
600
926
  /* the "onlyConnected" filtering keeps only the nodes with at
601
- least one incident edge (sensible for "graph" diagrams only),
602
- and the containers enclosing such a node */
603
- if (diagram.onlyConnected === true && type === "graph") {
604
- const connected = new Set();
605
- for (const edge of edges) {
606
- connected.add(edge.source);
607
- connected.add(edge.target);
608
- }
927
+ least one incident edge (sensible for all diagrams except the
928
+ "hub" and "grid" ones), and the containers enclosing such a node */
929
+ if (diagram.onlyConnected === true && type !== "hub" && type !== "grid") {
930
+ const connected = endsOf(edges);
609
931
  for (const node of Array.from(connected))
610
932
  for (let p = nesting.parentOf.get(node); p !== undefined; p = nesting.parentOf.get(p))
611
933
  connected.add(p);
612
934
  nodes = nodes.filter((node) => connected.has(node));
613
935
  }
936
+ /* a "matrix" diagram keeps a container as a row and column only
937
+ where it takes part in the relation itself, as a mere grouping
938
+ object (like a requirement group) would yield an empty row and
939
+ column, while it still names the partition of its members */
940
+ if (type === "matrix")
941
+ nodes = nodes.filter((node) => !nesting.containers.has(node)
942
+ || edges.some((edge) => edge.source === node || edge.target === node));
943
+ /* a "petri" diagram takes the objects matching its "places" patterns
944
+ as its places and all other nodes as its transitions (without
945
+ "places", Gradia infers them from the arcs), where the "tokens"
946
+ and "capacity" options name properties of the places */
947
+ let places;
948
+ if (type === "petri" && diagram.places !== undefined) {
949
+ places = new Set(resolvePatterns(index, errors, diagram.places, "places"));
950
+ validatePetri(ctx, diagram, nodes, edges, places, errors);
951
+ }
952
+ else
953
+ for (const field of ["places", "tokens", "capacity"])
954
+ if (diagram[field] !== undefined)
955
+ errors.push({ reason: type === "petri" ?
956
+ `"petri" diagram cannot carry a "${field}" configuration without a "places" one` :
957
+ `"${type}" diagram cannot carry a "${field}" configuration` });
958
+ /* a "gantt" diagram takes the nodes as its tasks (a node without a
959
+ duration being a milestone) and the edges as their dependencies,
960
+ where the "duration" and "start" options name properties of the tasks */
961
+ if (type === "gantt")
962
+ validateGantt(ctx, diagram, nodes, edges, errors);
963
+ else
964
+ for (const field of ["duration", "start"])
965
+ if (diagram[field] !== undefined)
966
+ errors.push({ reason: `"${type}" diagram cannot carry a "${field}" configuration` });
614
967
  if (errors.length > 0)
615
- return { errors };
968
+ return { type, errors };
616
969
  /* an empty node set silently omits the diagram, as a wildcard
617
970
  "nodes" pattern legitimately matches nothing when an optional
618
971
  kind is absent, and the "collapse" handling (enabled by default)
@@ -620,16 +973,41 @@ const deriveDiagram = (ctx, object, diagram) => {
620
973
  such a diagram carries no information beyond the object itself */
621
974
  if (nodes.length === 0
622
975
  || (diagram.collapse !== false && nodes.length === 1 && edges.length === 0))
623
- return { errors };
624
- return { spec: renderSpec(ctx, diagram, type, center, centerUrl, nodes, edges, nesting),
976
+ return { type, errors };
977
+ /* a "state" diagram of an object kind carrying an "automaton" draws
978
+ its flags as pseudo-states, which Gradia knows instead: a single
979
+ initial one leading into every node flagged initial, and a single
980
+ final one every node flagged final leads into */
981
+ const pseudo = new Map();
982
+ if (type === "state" && automaton !== undefined) {
983
+ for (const role of ["initial", "final"]) {
984
+ const states = nodes.filter((node) => node.kind === automaton.nodes
985
+ && plainText(propValue(parenProps, node, automaton[role]) ?? "").trim() === "true");
986
+ if (states.length === 0)
987
+ continue;
988
+ const state = { kind: "", id: `${anchors.get(object) ?? object.id}-@${role}`,
989
+ name: role, properties: [], children: [] };
990
+ pseudo.set(state, role);
991
+ if (role === "initial") {
992
+ nodes.unshift(state);
993
+ edges.push(...states.map((target) => ({ source: state, target })));
994
+ }
995
+ else {
996
+ nodes.push(state);
997
+ edges.push(...states.map((source) => ({ source, target: state })));
998
+ }
999
+ }
1000
+ }
1001
+ return { type, spec: renderSpec(ctx, diagram, type, center, centerUrl, nodes, edges, nesting, pseudo, places),
625
1002
  config: { ...diagramPresets, ...diagram.config }, columns, errors };
626
1003
  };
627
1004
  /* the memoized diagram derivations, keyed by specification, as the
628
1005
  lint validation and every renderer of an export derive the very
629
1006
  same diagrams of the very same parsed specification again */
630
1007
  const derivations = new WeakMap();
631
- /* derive the Gradia specs of all diagram-configured objects
632
- of a specification (memoized per specification and schema) */
1008
+ /* derive the Gradia specs of all diagrams of all diagram-configured
1009
+ objects of a specification, in their configured order (memoized
1010
+ per specification and schema) */
633
1011
  export const specDiagrams = (specification, config) => {
634
1012
  const derived = derivations.get(specification);
635
1013
  if (derived !== undefined && derived.config === config)
@@ -643,35 +1021,54 @@ export const specDiagrams = (specification, config) => {
643
1021
  positions.set(node.object, position);
644
1022
  });
645
1023
  const schemas = collectSchemas(specification, config);
646
- /* re-derive the synthetic properties of the consumed parenthesized
1024
+ /* determine the uncovered objects (only where a diagram marks them
1025
+ at all, as resolving all references is not for free): the ones
1026
+ lacking their demanded "referenced" coverage, and the ones a
1027
+ "coverage" report counts as unreferenced while no other report
1028
+ counts them as referenced; and
1029
+ re-derive the synthetic properties of the consumed parenthesized
647
1030
  name tokens, as the derivation sees the AST alone, while the
648
1031
  parsing context holding them is long gone by export time */
649
- const ctx = { index, anchors, positions, parents, parenProps: collectParenProps(schemas) };
1032
+ const markings = new Set(Array.from(schemas.values()).flatMap((schema) => diagramsOf(schema).map((diagram) => diagram.markUncovered ?? "none")));
1033
+ const uncovered = { referenced: new Set(), coverage: new Set() };
1034
+ if (markings.has("referenced") || markings.has("all"))
1035
+ for (const result of referencedCoverage(index, schemas))
1036
+ result.uncovered.forEach((object) => uncovered.referenced.add(object));
1037
+ if (markings.has("coverage") || markings.has("all")) {
1038
+ const reports = Array.from(specCoverage(index, schemas).values()).flat();
1039
+ const covered = new Set(reports.flatMap((report) => report.covered));
1040
+ for (const report of reports)
1041
+ for (const object of report.uncovered)
1042
+ if (!covered.has(object))
1043
+ uncovered.coverage.add(object);
1044
+ }
1045
+ const ctx = { index, anchors, positions, parents, schemas,
1046
+ parenProps: collectParenProps(schemas), uncovered };
650
1047
  const results = new Map();
651
- for (const [object, schema] of schemas)
652
- if (schema.diagram !== undefined)
653
- results.set(object, deriveDiagram(ctx, object, schema.diagram));
1048
+ for (const [object, schema] of schemas) {
1049
+ const diagrams = diagramsOf(schema);
1050
+ if (diagrams.length > 0)
1051
+ results.set(object, diagrams.map((diagram) => deriveDiagram(ctx, object, diagram, schema.automaton)));
1052
+ }
654
1053
  derivations.set(specification, { config, results });
655
1054
  return results;
656
1055
  };
657
- /* determine the diagram-configured objects whose diagrams the given
658
- aspects omit: the ones of a diagram type and the ones from an object
659
- tree nesting level on (the top-level objects being level 1) */
1056
+ /* determine the diagrams the given aspects omit: the ones of a diagram
1057
+ type and the ones from an object tree nesting level on (the top-level
1058
+ objects being level 1) */
660
1059
  export const omittedDiagrams = (specification, config, omit) => {
661
1060
  const omitted = new Set();
662
1061
  if (omit.size === 0)
663
1062
  return omitted;
664
- const schemas = collectSchemas(specification, config);
1063
+ const results = specDiagrams(specification, config);
665
1064
  for (const node of buildLinkIndex(specification)) {
666
- const diagram = schemas.get(node.object)?.diagram;
667
- if (diagram === undefined)
668
- continue;
669
1065
  let level = 1;
670
1066
  for (let parent = node.parent; parent !== undefined; parent = parent.parent)
671
1067
  level++;
672
- if (omit.has(`diagram:${diagram.type ?? "graph"}`)
673
- || [1, 2, 3].some((n) => n <= level && omit.has(`diagram:${n}`)))
674
- omitted.add(node.object);
1068
+ for (const result of results.get(node.object) ?? [])
1069
+ if (omit.has(`diagram:${result.type}`)
1070
+ || [1, 2, 3].some((n) => n <= level && omit.has(`diagram:${n}`)))
1071
+ omitted.add(result);
675
1072
  }
676
1073
  return omitted;
677
1074
  };
@@ -681,51 +1078,57 @@ export const omittedDiagrams = (specification, config, omit) => {
681
1078
  passes and formats of a single export render the very same diagrams
682
1079
  over and over again */
683
1080
  let svgCache = new Map();
684
- /* render the Gradia specs of all diagram-configured objects of a
685
- specification into embeddable SVGs (a runtime rendering failure omits
686
- the diagram and is surfaced as a notice only, as the statically
687
- detectable invalid situations are already reported as lint
688
- diagnostics), served from the cache where possible, which is
689
- afterwards swept to the diagrams of this very rendering, so it never
690
- grows beyond a single document; the omitted diagrams are skipped */
1081
+ /* render the Gradia specs of all diagrams of all diagram-configured
1082
+ objects of a specification into embeddable SVGs, in their configured
1083
+ order (a runtime rendering failure omits the diagram and is surfaced
1084
+ as a notice only, as the statically detectable invalid situations are
1085
+ already reported as lint diagnostics), served from the cache where
1086
+ possible, which is afterwards swept to the diagrams of this very
1087
+ rendering, so it never grows beyond a single document; the omitted
1088
+ diagrams are skipped */
691
1089
  export const renderDiagrams = async (specification, config, verbose, omit = new Set()) => {
692
1090
  const cache = new Map();
693
1091
  const rendered = new Map();
694
1092
  const omitted = omittedDiagrams(specification, config, omit);
695
1093
  let cached = 0;
696
- for (const [object, result] of specDiagrams(specification, config)) {
697
- if (result.spec === undefined || omitted.has(object))
698
- continue;
699
- const key = result.spec + JSON.stringify(result.config ?? {});
700
- let svg = svgCache.get(key) ?? cache.get(key);
701
- if (svg !== undefined)
702
- cached++;
703
- else {
704
- try {
705
- svg = await Gradia.render(result.spec, { format: "svg:embedded", config: result.config });
706
- }
707
- catch (err) {
708
- verbose?.(`rendering diagram of ${object.kind} "${literal(plainText(object.name))}" failed: ` +
709
- (err instanceof Error ? err.message : String(err)));
1094
+ let total = 0;
1095
+ for (const [object, results] of specDiagrams(specification, config)) {
1096
+ for (const result of results) {
1097
+ if (result.spec === undefined || omitted.has(result))
710
1098
  continue;
1099
+ const key = result.spec + JSON.stringify(result.config ?? {});
1100
+ let svg = svgCache.get(key) ?? cache.get(key);
1101
+ if (svg !== undefined)
1102
+ cached++;
1103
+ else {
1104
+ try {
1105
+ svg = await Gradia.render(result.spec, { format: "svg:embedded", config: result.config });
1106
+ }
1107
+ catch (err) {
1108
+ verbose?.(`rendering ${result.type} diagram of ${object.kind} ` +
1109
+ `"${literal(plainText(object.name))}" failed: ` +
1110
+ (err instanceof Error ? err.message : String(err)));
1111
+ continue;
1112
+ }
711
1113
  }
1114
+ cache.set(key, svg);
1115
+ rendered.set(object, [...(rendered.get(object) ?? []), { ...result, svg }]);
1116
+ total++;
712
1117
  }
713
- cache.set(key, svg);
714
- rendered.set(object, { ...result, svg });
715
1118
  }
716
1119
  svgCache = cache;
717
- if (cached > 0 && cached === rendered.size)
1120
+ if (cached > 0 && cached === total)
718
1121
  verbose?.(`reusing ${literal(cached)} cached diagram(s)`);
719
1122
  else
720
- verbose?.(`rendering ${literal(rendered.size)} diagram(s) (${literal(cached)} cached)`);
1123
+ verbose?.(`rendering ${literal(total)} diagram(s) (${literal(cached)} cached)`);
721
1124
  return rendered;
722
1125
  };
723
1126
  /* validate all configured diagrams of a specification, reporting
724
1127
  every invalid diagram situation as a file/line-precise diagnostic */
725
1128
  export const validateDiagrams = (ctx, specification, config) => {
726
1129
  const seen = new Set();
727
- for (const [object, result] of specDiagrams(specification, config))
728
- for (const error of result.errors) {
1130
+ for (const [object, results] of specDiagrams(specification, config))
1131
+ for (const error of results.flatMap((result) => result.errors)) {
729
1132
  /* attribute an edge object situation to the offending edge
730
1133
  object itself and report it just once, as a wildcard
731
1134
  "edges" pattern lets every diagram walk the very same