@rse/specbook 1.5.2 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +201 -0
- package/README.md +288 -43
- package/dst/specbook-api.d.ts +23 -3
- package/dst/specbook-api.js +133 -59
- package/dst/specbook-cli.js +93 -26
- package/dst/specbook-cmd-describe.js +4 -4
- package/dst/specbook-cmd-export-schema.d.ts +8 -0
- package/dst/specbook-cmd-export-schema.js +441 -0
- package/dst/specbook-cmd-export.js +23 -6
- package/dst/specbook-cmd-init-sample.d.ts +4 -0
- package/dst/specbook-cmd-init-sample.js +322 -0
- package/dst/specbook-cmd-init.d.ts +1 -0
- package/dst/specbook-cmd-init.js +42 -10
- package/dst/specbook-cmd-lint.js +46 -48
- package/dst/specbook-cmd-query.d.ts +30 -0
- package/dst/specbook-cmd-query.js +165 -0
- package/dst/specbook-config.d.ts +9 -1
- package/dst/specbook-config.js +135 -10
- package/dst/specbook-coverage.js +2 -2
- package/dst/specbook-diagram.d.ts +7 -5
- package/dst/specbook-diagram.js +546 -143
- package/dst/specbook-export-ast.d.ts +1 -1
- package/dst/specbook-export-ast.js +11 -8
- package/dst/specbook-export-code.js +3 -3
- package/dst/specbook-export-common.d.ts +1 -1
- package/dst/specbook-export-common.js +3 -2
- package/dst/specbook-export-diagram.d.ts +1 -1
- package/dst/specbook-export-diagram.js +25 -7
- package/dst/specbook-export-html-fold.d.ts +16 -6
- package/dst/specbook-export-html-fold.js +65 -44
- package/dst/specbook-export-html-render.js +107 -26
- package/dst/specbook-export-html-search.js +18 -20
- package/dst/specbook-export-html-template.js +38 -9
- package/dst/specbook-export-html-widget.d.ts +2 -0
- package/dst/specbook-export-html-widget.js +219 -19
- package/dst/specbook-export-html.css +208 -38
- package/dst/specbook-export-html.js +96 -53
- package/dst/specbook-export-md.d.ts +2 -1
- package/dst/specbook-export-md.js +7 -8
- package/dst/specbook-export-pdf.js +1 -1
- package/dst/specbook-files.d.ts +2 -0
- package/dst/specbook-files.js +41 -0
- package/dst/specbook-format-meta.yaml +401 -0
- package/dst/specbook-format-pm.yaml +1240 -0
- package/dst/specbook-format-schema.d.ts +33 -3
- package/dst/specbook-format-schema.js +49 -3
- package/dst/specbook-format.md +427 -47
- package/dst/specbook-format.yaml +524 -228
- package/dst/specbook-ignore.js +2 -2
- package/dst/specbook-link.d.ts +4 -0
- package/dst/specbook-link.js +61 -6
- package/dst/specbook-mcp.js +103 -19
- package/dst/specbook-parse-semantic.d.ts +1 -0
- package/dst/specbook-parse-semantic.js +11 -4
- package/dst/specbook-parse-syntax.d.ts +1 -0
- package/dst/specbook-parse-syntax.js +1 -1
- package/dst/specbook-parse.d.ts +1 -1
- package/dst/specbook-parse.js +3 -3
- package/dst/specbook-project.d.ts +2 -0
- package/dst/specbook-project.js +15 -5
- package/dst/specbook-remote.d.ts +28 -0
- package/dst/specbook-remote.js +429 -0
- package/package.json +32 -20
package/dst/specbook-diagram.js
CHANGED
|
@@ -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 {
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
163
|
-
the
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
|
211
|
-
property, or through
|
|
212
|
-
no name is configured
|
|
213
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
const
|
|
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 (
|
|
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
|
-
|
|
239
|
-
|
|
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
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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 =
|
|
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
|
|
540
|
-
|
|
541
|
-
or the diagram collapses onto a
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
549
|
-
|
|
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
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
|
602
|
-
and the containers enclosing such a node */
|
|
603
|
-
if (diagram.onlyConnected === true && type
|
|
604
|
-
const connected =
|
|
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
|
-
|
|
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
|
|
632
|
-
of a specification
|
|
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
|
-
/*
|
|
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
|
|
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
|
-
|
|
653
|
-
|
|
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
|
|
658
|
-
|
|
659
|
-
|
|
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
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
|
685
|
-
specification into embeddable SVGs
|
|
686
|
-
|
|
687
|
-
detectable invalid situations are
|
|
688
|
-
diagnostics), served from the cache where
|
|
689
|
-
afterwards swept to the diagrams of this very
|
|
690
|
-
grows beyond a single document; the omitted
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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 ===
|
|
1120
|
+
if (cached > 0 && cached === total)
|
|
718
1121
|
verbose?.(`reusing ${literal(cached)} cached diagram(s)`);
|
|
719
1122
|
else
|
|
720
|
-
verbose?.(`rendering ${literal(
|
|
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,
|
|
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
|