@meltemi/diagrams-model 0.0.0-stage → 1.9.0
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 +21 -0
- package/README.md +48 -2
- package/dist/c4.d.ts +44 -0
- package/dist/c4.d.ts.map +1 -0
- package/dist/c4.js +803 -0
- package/dist/c4.js.map +1 -0
- package/dist/compile.d.ts +20 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +525 -0
- package/dist/compile.js.map +1 -0
- package/dist/diagram-types.d.ts +67 -0
- package/dist/diagram-types.d.ts.map +1 -0
- package/dist/diagram-types.js +282 -0
- package/dist/diagram-types.js.map +1 -0
- package/dist/diff.d.ts +91 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +575 -0
- package/dist/diff.js.map +1 -0
- package/dist/export.d.ts +22 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +243 -0
- package/dist/export.js.map +1 -0
- package/dist/flow.d.ts +247 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +1693 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +400 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +433 -0
- package/dist/json.js.map +1 -0
- package/dist/messages/changes.d.ts +64 -0
- package/dist/messages/changes.d.ts.map +1 -0
- package/dist/messages/changes.js +117 -0
- package/dist/messages/changes.js.map +1 -0
- package/dist/messages/en.d.ts +554 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +566 -0
- package/dist/messages/en.js.map +1 -0
- package/dist/messages/es.d.ts +3 -0
- package/dist/messages/es.d.ts.map +1 -0
- package/dist/messages/es.js +551 -0
- package/dist/messages/es.js.map +1 -0
- package/dist/messages/index.d.ts +449 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +11 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/model.d.ts +11 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +363 -0
- package/dist/model.js.map +1 -0
- package/dist/scenario.d.ts +11 -0
- package/dist/scenario.d.ts.map +1 -0
- package/dist/scenario.js +91 -0
- package/dist/scenario.js.map +1 -0
- package/dist/schema.d.ts +311 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +658 -0
- package/dist/schema.js.map +1 -0
- package/dist/sequence.d.ts +8 -0
- package/dist/sequence.d.ts.map +1 -0
- package/dist/sequence.js +722 -0
- package/dist/sequence.js.map +1 -0
- package/dist/story.d.ts +18 -0
- package/dist/story.d.ts.map +1 -0
- package/dist/story.js +406 -0
- package/dist/story.js.map +1 -0
- package/dist/style-values.d.ts +107 -0
- package/dist/style-values.d.ts.map +1 -0
- package/dist/style-values.js +558 -0
- package/dist/style-values.js.map +1 -0
- package/dist/theme-block.d.ts +10 -0
- package/dist/theme-block.d.ts.map +1 -0
- package/dist/theme-block.js +135 -0
- package/dist/theme-block.js.map +1 -0
- package/dist/types.d.ts +785 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -3
package/dist/c4.js
ADDED
|
@@ -0,0 +1,803 @@
|
|
|
1
|
+
import { isToken, } from "@meltemi/diagrams-language";
|
|
2
|
+
import { FlowAnalyzer } from "./flow.js";
|
|
3
|
+
import { messages } from "./messages/index.js";
|
|
4
|
+
import { closePaths, isAncestorPath } from "./model.js";
|
|
5
|
+
import { C4_LEVELS, C4_PROFILE, PORTS } from "./schema.js";
|
|
6
|
+
import { headerHasError, singleIdent } from "./style-values.js";
|
|
7
|
+
/** Analyzes `diagram <id>: c4 using <model> { }` into a view. */
|
|
8
|
+
export function analyzeC4View(node, id, lines, bag, model) {
|
|
9
|
+
const analyzer = new C4Analyzer(node, id, lines, bag, model);
|
|
10
|
+
return { diagram: analyzer.run(), errors: analyzer.errors };
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The entities a C4 view of a level and scope may draw: persons and systems at the context level;
|
|
14
|
+
* the scope's containers and datastores, persons and other systems at the container level; the
|
|
15
|
+
* scope's components, the other containers of its system, persons and other systems at the
|
|
16
|
+
* component level.
|
|
17
|
+
*/
|
|
18
|
+
export function allowedEntities(model, level, scopePath) {
|
|
19
|
+
const allowed = new Set();
|
|
20
|
+
const scope = scopePath === null ? null : (model.entitiesByPath.get(scopePath) ?? null);
|
|
21
|
+
for (const entity of model.entities) {
|
|
22
|
+
const kind = entity.kind;
|
|
23
|
+
switch (level) {
|
|
24
|
+
case "context":
|
|
25
|
+
if (kind === "person" || kind === "system")
|
|
26
|
+
allowed.add(entity.path);
|
|
27
|
+
break;
|
|
28
|
+
case "container":
|
|
29
|
+
if (kind === "person")
|
|
30
|
+
allowed.add(entity.path);
|
|
31
|
+
else if (kind === "system" && entity.path !== scope?.path)
|
|
32
|
+
allowed.add(entity.path);
|
|
33
|
+
else if ((kind === "container" || kind === "datastore") && entity.parent === scope?.path)
|
|
34
|
+
allowed.add(entity.path);
|
|
35
|
+
break;
|
|
36
|
+
case "component": {
|
|
37
|
+
const system = scope?.parent ?? null;
|
|
38
|
+
if (kind === "person")
|
|
39
|
+
allowed.add(entity.path);
|
|
40
|
+
else if (kind === "system" && entity.path !== system)
|
|
41
|
+
allowed.add(entity.path);
|
|
42
|
+
else if ((kind === "container" || kind === "datastore") &&
|
|
43
|
+
entity.parent === system &&
|
|
44
|
+
entity.path !== scope?.path)
|
|
45
|
+
allowed.add(entity.path);
|
|
46
|
+
else if (kind === "component" && entity.parent === scope?.path)
|
|
47
|
+
allowed.add(entity.path);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return allowed;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The level of the view that opens an entity of a kind: a system opens a container view scoped
|
|
56
|
+
* to it, a container a component view scoped to it; persons, datastores and components open
|
|
57
|
+
* nothing.
|
|
58
|
+
*/
|
|
59
|
+
export function childLevel(kind) {
|
|
60
|
+
if (kind === "system")
|
|
61
|
+
return "container";
|
|
62
|
+
if (kind === "container")
|
|
63
|
+
return "component";
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether a compiled C4 view is the child view of an entity: it uses the same model (the model's
|
|
68
|
+
* file and id; `owner` is the view's own file, standing for a model resolved in that file), has
|
|
69
|
+
* the level `childLevel` gives the entity's kind, and is scoped to the entity. A context view
|
|
70
|
+
* scoped to a system is not its child view.
|
|
71
|
+
*/
|
|
72
|
+
export function isChildView(view, owner, model, entity) {
|
|
73
|
+
const level = childLevel(entity.kind);
|
|
74
|
+
if (level === null || view.level !== level || view.scope !== entity.path)
|
|
75
|
+
return false;
|
|
76
|
+
const resolved = view.resolved;
|
|
77
|
+
if (!resolved || resolved.definition.id !== model.id)
|
|
78
|
+
return false;
|
|
79
|
+
return (resolved.file ?? owner) === model.file;
|
|
80
|
+
}
|
|
81
|
+
/** A token standing for a whole qualified reference: its text is the joined path. */
|
|
82
|
+
function wholeToken(ref) {
|
|
83
|
+
const first = ref.parts[0];
|
|
84
|
+
return {
|
|
85
|
+
kind: first.kind,
|
|
86
|
+
text: ref.parts.map((p) => p.text).join("."),
|
|
87
|
+
start: ref.start,
|
|
88
|
+
end: ref.end,
|
|
89
|
+
fullStart: first.fullStart,
|
|
90
|
+
leadingTrivia: first.leadingTrivia,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const ENTITY_TYPES = {
|
|
94
|
+
person: "person",
|
|
95
|
+
system: "system",
|
|
96
|
+
container: "container",
|
|
97
|
+
datastore: "datastore",
|
|
98
|
+
component: "component",
|
|
99
|
+
};
|
|
100
|
+
/** The statements of C4 views, as their hint lists them. */
|
|
101
|
+
const VIEW_STATEMENTS = [
|
|
102
|
+
"level",
|
|
103
|
+
"scope",
|
|
104
|
+
"include",
|
|
105
|
+
"exclude",
|
|
106
|
+
"label",
|
|
107
|
+
"direction",
|
|
108
|
+
"layout",
|
|
109
|
+
"routing",
|
|
110
|
+
"theme",
|
|
111
|
+
"title",
|
|
112
|
+
"description",
|
|
113
|
+
"preset",
|
|
114
|
+
"canvas",
|
|
115
|
+
"placement",
|
|
116
|
+
"style",
|
|
117
|
+
"animate",
|
|
118
|
+
"class",
|
|
119
|
+
"rule",
|
|
120
|
+
];
|
|
121
|
+
class C4Analyzer extends FlowAnalyzer {
|
|
122
|
+
model;
|
|
123
|
+
definition;
|
|
124
|
+
level = "context";
|
|
125
|
+
levelToken;
|
|
126
|
+
scope = null;
|
|
127
|
+
labels = [];
|
|
128
|
+
labelStatements = [];
|
|
129
|
+
includes = null;
|
|
130
|
+
excludes = [];
|
|
131
|
+
includeNode = null;
|
|
132
|
+
excludeNode = null;
|
|
133
|
+
combined = [];
|
|
134
|
+
/** Tokens of `include` entries by path, which stand for the elements in this file. */
|
|
135
|
+
includeTokens = new Map();
|
|
136
|
+
constructor(node, id, lines, bag, model) {
|
|
137
|
+
super(node, id, lines, bag, C4_PROFILE);
|
|
138
|
+
this.model = model;
|
|
139
|
+
this.definition = model.resolved?.definition ?? null;
|
|
140
|
+
}
|
|
141
|
+
// ---------------------------------------------------------------- entity references
|
|
142
|
+
entityRef(token, path, role) {
|
|
143
|
+
this.entityReferences.push({ token, model: this.model.name, path, role });
|
|
144
|
+
}
|
|
145
|
+
/** The entity a qualified reference names, from the model's root, with a reference per part. */
|
|
146
|
+
entity(ref, role, report = true) {
|
|
147
|
+
const model = this.definition;
|
|
148
|
+
const path = ref.parts.map((p) => p.text).join(".");
|
|
149
|
+
const entity = model.entitiesByPath.get(path);
|
|
150
|
+
if (!entity) {
|
|
151
|
+
if (report) {
|
|
152
|
+
const close = closePaths(path, model.entitiesByPath.keys());
|
|
153
|
+
const m = messages();
|
|
154
|
+
this.error("ref.unknown-entity", m.diagnostics.common.unknownEntity(path, model.id), ref, {
|
|
155
|
+
data: { candidates: close },
|
|
156
|
+
hints: [close.length > 0 ? m.hints.didYouMean(close) : m.hints.c4.entityPath],
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
ref.parts.forEach((part, i) => {
|
|
162
|
+
this.entityRef(part, ref.parts
|
|
163
|
+
.slice(0, i + 1)
|
|
164
|
+
.map((p) => p.text)
|
|
165
|
+
.join("."), role);
|
|
166
|
+
});
|
|
167
|
+
return entity;
|
|
168
|
+
}
|
|
169
|
+
targetToken(arg, role) {
|
|
170
|
+
if (arg?.kind !== "QualifiedRef")
|
|
171
|
+
return undefined;
|
|
172
|
+
const path = arg.parts.map((p) => p.text).join(".");
|
|
173
|
+
// Relation ids are single words; entity paths may have several parts.
|
|
174
|
+
if (this.elementsById.has(path) && this.definition)
|
|
175
|
+
this.entity(arg, role, false);
|
|
176
|
+
return arg.parts.length === 1 ? arg.parts[0] : wholeToken(arg);
|
|
177
|
+
}
|
|
178
|
+
readEndpoint(ref) {
|
|
179
|
+
// The longest prefix that is an element of the view; one more part is a port.
|
|
180
|
+
for (let n = ref.parts.length; n >= 1; n--) {
|
|
181
|
+
const path = ref.parts
|
|
182
|
+
.slice(0, n)
|
|
183
|
+
.map((p) => p.text)
|
|
184
|
+
.join(".");
|
|
185
|
+
const element = this.elementsById.get(path);
|
|
186
|
+
if (!element)
|
|
187
|
+
continue;
|
|
188
|
+
const rest = ref.parts.slice(n);
|
|
189
|
+
if (rest.length > 1)
|
|
190
|
+
break;
|
|
191
|
+
const portToken = rest[0];
|
|
192
|
+
let port = "auto";
|
|
193
|
+
let named = false;
|
|
194
|
+
if (portToken) {
|
|
195
|
+
if (PORTS.includes(portToken.text))
|
|
196
|
+
port = portToken.text;
|
|
197
|
+
else if (element.ports.some((p) => p.name === portToken.text)) {
|
|
198
|
+
port = portToken.text;
|
|
199
|
+
named = true;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const m = messages();
|
|
203
|
+
this.error("placement.unknown-port", m.diagnostics.common.unknownPort(path, portToken.text), portToken, { hints: [m.hints.c4.ports(PORTS)], data: { ids: [path, portToken.text] } });
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const partial = { ...ref, parts: ref.parts.slice(0, n) };
|
|
208
|
+
if (this.definition)
|
|
209
|
+
this.entity(partial, "placement", false);
|
|
210
|
+
return {
|
|
211
|
+
endpoint: { element: path, port, named, explicitPort: portToken !== undefined },
|
|
212
|
+
token: n === 1 ? ref.parts[0] : wholeToken(partial),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const m = messages();
|
|
216
|
+
this.error("ref.unknown-element", m.diagnostics.c4.notDrawn(ref.text), ref, {
|
|
217
|
+
hints: [m.hints.c4.routeEnds],
|
|
218
|
+
data: { ids: [ref.text] },
|
|
219
|
+
});
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
resolveElement(token, statement, stmt) {
|
|
223
|
+
if (this.elementsById.has(token.text) || this.relationsByKey.has(token.text)) {
|
|
224
|
+
return super.resolveElement(token, statement, stmt);
|
|
225
|
+
}
|
|
226
|
+
this.error("ref.unknown-element", messages().diagnostics.c4.notDrawn(token.text), token, {
|
|
227
|
+
hints: [this.notDrawnHint(token.text)],
|
|
228
|
+
data: { ids: [token.text] },
|
|
229
|
+
});
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
/** Why a path is not an element of the view, as a hint in the interface language. */
|
|
233
|
+
notDrawnHint(path) {
|
|
234
|
+
const hints = messages().hints.c4;
|
|
235
|
+
const entity = this.definition?.entitiesByPath.get(path);
|
|
236
|
+
if (!entity)
|
|
237
|
+
return hints.nameEntity;
|
|
238
|
+
if (this.allowed().has(path))
|
|
239
|
+
return hints.include(path);
|
|
240
|
+
return hints.notAtLevel(path, entity.kind, this.level);
|
|
241
|
+
}
|
|
242
|
+
// ---------------------------------------------------------------- levels and sets
|
|
243
|
+
/** The entities a view of this level and scope may draw. */
|
|
244
|
+
allowed() {
|
|
245
|
+
return allowedEntities(this.definition, this.level, this.scope?.path ?? null);
|
|
246
|
+
}
|
|
247
|
+
/** The nearest ancestor-or-self of a path in a set. */
|
|
248
|
+
lift(path, set) {
|
|
249
|
+
let current = path;
|
|
250
|
+
const model = this.definition;
|
|
251
|
+
while (current !== null) {
|
|
252
|
+
if (set.has(current))
|
|
253
|
+
return current;
|
|
254
|
+
current = model.entitiesByPath.get(current)?.parent ?? null;
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
/** The default set: everything at context without scope; else the core and its neighbours. */
|
|
259
|
+
defaults(allowed) {
|
|
260
|
+
const model = this.definition;
|
|
261
|
+
const scope = this.scope;
|
|
262
|
+
if (this.level === "context" && !scope)
|
|
263
|
+
return new Set(allowed);
|
|
264
|
+
// Without its scope a container or component view has no core; its error is reported.
|
|
265
|
+
if (!scope)
|
|
266
|
+
return new Set();
|
|
267
|
+
const core = new Set();
|
|
268
|
+
if (this.level === "context")
|
|
269
|
+
core.add(scope.path);
|
|
270
|
+
else
|
|
271
|
+
for (const path of allowed)
|
|
272
|
+
if (model.entitiesByPath.get(path).parent === scope.path)
|
|
273
|
+
core.add(path);
|
|
274
|
+
const set = new Set(core);
|
|
275
|
+
for (const relation of model.relations) {
|
|
276
|
+
const a = this.lift(relation.from, allowed);
|
|
277
|
+
const b = this.lift(relation.to, allowed);
|
|
278
|
+
if (!a || !b || a === b)
|
|
279
|
+
continue;
|
|
280
|
+
if (core.has(a))
|
|
281
|
+
set.add(b);
|
|
282
|
+
if (core.has(b))
|
|
283
|
+
set.add(a);
|
|
284
|
+
}
|
|
285
|
+
return set;
|
|
286
|
+
}
|
|
287
|
+
// ---------------------------------------------------------------- statements
|
|
288
|
+
readLevel(stmt) {
|
|
289
|
+
const word = singleIdent(stmt.args[0]);
|
|
290
|
+
if (!word ||
|
|
291
|
+
stmt.args.length > 1 ||
|
|
292
|
+
stmt.block ||
|
|
293
|
+
!C4_LEVELS.includes(word.text)) {
|
|
294
|
+
this.error("c4.invalid-level", messages().diagnostics.c4.invalidLevel, stmt.args[0] ?? stmt.keyword);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.level = word.text;
|
|
298
|
+
this.levelToken = stmt.keyword;
|
|
299
|
+
}
|
|
300
|
+
readList(stmt, which, allowed) {
|
|
301
|
+
const node = which === "include" ? this.includeNode : this.excludeNode;
|
|
302
|
+
if (node) {
|
|
303
|
+
const m = messages();
|
|
304
|
+
this.error("prop.duplicate", m.diagnostics.c4.listTwice(which), stmt.keyword, {
|
|
305
|
+
related: [{ message: m.related.firstWritten, at: node.keyword }],
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (which === "include")
|
|
310
|
+
this.includeNode = stmt;
|
|
311
|
+
else
|
|
312
|
+
this.excludeNode = stmt;
|
|
313
|
+
if (stmt.block || stmt.chain || stmt.args.length === 0) {
|
|
314
|
+
this.invalid(messages().diagnostics.c4.listShape(which), stmt.block ?? stmt.chain ?? stmt.keyword);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const entries = [];
|
|
318
|
+
const seen = new Set();
|
|
319
|
+
for (const arg of stmt.args) {
|
|
320
|
+
if (arg.kind !== "QualifiedRef") {
|
|
321
|
+
this.invalid(messages().diagnostics.c4.listByPath(which), arg);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const entity = this.entity(arg, which);
|
|
325
|
+
if (!entity)
|
|
326
|
+
continue;
|
|
327
|
+
if (seen.has(entity.path)) {
|
|
328
|
+
this.invalid(messages().diagnostics.c4.listedTwice(entity.path, which), arg);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
seen.add(entity.path);
|
|
332
|
+
if (which === "include" && !allowed.has(entity.path)) {
|
|
333
|
+
const boundary = entity.path === this.scope?.path;
|
|
334
|
+
const m = messages();
|
|
335
|
+
this.error("c4.out-of-level", boundary
|
|
336
|
+
? m.diagnostics.c4.scopeIncluded(entity.path, this.level)
|
|
337
|
+
: m.diagnostics.c4.outOfLevel(this.level, entity.kind, entity.path), arg, { hints: [boundary ? m.hints.c4.removeScope(entity.path) : this.levelHint()] });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
entries.push({ path: entity.path, range: this.range(arg) });
|
|
341
|
+
if (which === "include") {
|
|
342
|
+
this.includeTokens.set(entity.path, arg.parts.length === 1 ? arg.parts[0] : wholeToken(arg));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (which === "include")
|
|
346
|
+
this.includes = entries;
|
|
347
|
+
else
|
|
348
|
+
this.excludes.push(...entries);
|
|
349
|
+
}
|
|
350
|
+
/** What a view of this level draws, as a hint in the interface language. */
|
|
351
|
+
levelHint() {
|
|
352
|
+
const hints = messages().hints.c4;
|
|
353
|
+
const scope = this.scope?.path ?? null;
|
|
354
|
+
switch (this.level) {
|
|
355
|
+
case "context":
|
|
356
|
+
return hints.contextLevel;
|
|
357
|
+
case "container":
|
|
358
|
+
return hints.containerLevel(scope);
|
|
359
|
+
case "component":
|
|
360
|
+
return hints.componentLevel(scope);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// ---------------------------------------------------------------- run
|
|
364
|
+
run() {
|
|
365
|
+
const block = this.node.block.kind === "Block" ? this.node.block : undefined;
|
|
366
|
+
const statements = block?.statements ?? [];
|
|
367
|
+
const properties = [];
|
|
368
|
+
const blocks = [];
|
|
369
|
+
const lists = [];
|
|
370
|
+
const relationStatements = [];
|
|
371
|
+
let scopeStmt;
|
|
372
|
+
let levelStmt;
|
|
373
|
+
for (const stmt of statements) {
|
|
374
|
+
if (stmt.kind === "Error" || headerHasError(stmt))
|
|
375
|
+
continue;
|
|
376
|
+
if (stmt.kind === "RelationStatement") {
|
|
377
|
+
relationStatements.push(stmt);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (stmt.kind === "ShorthandDeclaration") {
|
|
381
|
+
const m = messages();
|
|
382
|
+
this.error("prop.unknown", m.diagnostics.c4.viewDeclares, stmt.id, {
|
|
383
|
+
hints: [m.hints.c4.declareInModel(stmt.id.text, this.model.name)],
|
|
384
|
+
});
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
const keyword = stmt.keyword.text;
|
|
388
|
+
if (keyword === "level") {
|
|
389
|
+
if (levelStmt) {
|
|
390
|
+
this.error("prop.duplicate", messages().diagnostics.common.setMoreThanOnce("level"), stmt.keyword);
|
|
391
|
+
}
|
|
392
|
+
else
|
|
393
|
+
levelStmt = stmt;
|
|
394
|
+
}
|
|
395
|
+
else if (keyword === "scope") {
|
|
396
|
+
if (scopeStmt) {
|
|
397
|
+
this.error("prop.duplicate", messages().diagnostics.common.setMoreThanOnce("scope"), stmt.keyword);
|
|
398
|
+
}
|
|
399
|
+
else
|
|
400
|
+
scopeStmt = stmt;
|
|
401
|
+
}
|
|
402
|
+
else if (keyword === "include" || keyword === "exclude")
|
|
403
|
+
lists.push(stmt);
|
|
404
|
+
else if (keyword === "label")
|
|
405
|
+
this.labelStatements.push(stmt);
|
|
406
|
+
else if (keyword in this.profile.diagramProperties)
|
|
407
|
+
properties.push(stmt);
|
|
408
|
+
else if (this.profile.diagramBlocks.includes(keyword))
|
|
409
|
+
blocks.push(stmt);
|
|
410
|
+
else {
|
|
411
|
+
const m = messages();
|
|
412
|
+
this.error("prop.unknown", m.diagnostics.c4.unknownInView(keyword), stmt.keyword, {
|
|
413
|
+
data: { candidates: ENTITY_TYPES[keyword] ? [] : VIEW_STATEMENTS },
|
|
414
|
+
hints: [
|
|
415
|
+
ENTITY_TYPES[keyword]
|
|
416
|
+
? m.hints.c4.entitiesInModel(this.model.name)
|
|
417
|
+
: m.hints.c4.viewStatements,
|
|
418
|
+
],
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const stmt of properties)
|
|
423
|
+
this.diagramProperty(stmt);
|
|
424
|
+
if (levelStmt)
|
|
425
|
+
this.readLevel(levelStmt);
|
|
426
|
+
else {
|
|
427
|
+
const m = messages();
|
|
428
|
+
this.error("c4.invalid-level", m.diagnostics.c4.missingLevel, this.node.keyword, {
|
|
429
|
+
hints: [m.hints.c4.addLevel],
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (this.definition) {
|
|
433
|
+
this.readScope(scopeStmt, levelStmt);
|
|
434
|
+
const allowed = this.allowed();
|
|
435
|
+
for (const stmt of lists)
|
|
436
|
+
this.readList(stmt, stmt.keyword.text, allowed);
|
|
437
|
+
const visible = this.includes
|
|
438
|
+
? new Set(this.includes.map((e) => e.path))
|
|
439
|
+
: this.defaults(allowed);
|
|
440
|
+
for (const entry of this.excludes)
|
|
441
|
+
visible.delete(entry.path);
|
|
442
|
+
this.buildElements(visible);
|
|
443
|
+
this.buildRelations(relationStatements);
|
|
444
|
+
this.readLabels();
|
|
445
|
+
}
|
|
446
|
+
// Placement and styles name the elements and relations built above; a view of a model that
|
|
447
|
+
// is not resolved has none, and stays silent.
|
|
448
|
+
for (const stmt of blocks) {
|
|
449
|
+
const keyword = stmt.keyword.text;
|
|
450
|
+
if (!this.definition && keyword !== "class" && keyword !== "rule")
|
|
451
|
+
continue;
|
|
452
|
+
if (keyword === "placement")
|
|
453
|
+
this.placementBlock(stmt);
|
|
454
|
+
else if (keyword === "class")
|
|
455
|
+
this.definitions.addClass(stmt);
|
|
456
|
+
else if (keyword === "rule")
|
|
457
|
+
this.definitions.addRule(stmt);
|
|
458
|
+
else
|
|
459
|
+
this.styleOrAnimate(stmt, keyword);
|
|
460
|
+
}
|
|
461
|
+
const base = this.diagram(block);
|
|
462
|
+
return {
|
|
463
|
+
...base,
|
|
464
|
+
c4: {
|
|
465
|
+
model: this.model.name,
|
|
466
|
+
modelRange: this.range(this.model.ref),
|
|
467
|
+
resolved: this.model.resolved,
|
|
468
|
+
level: this.level,
|
|
469
|
+
scope: this.scope?.path ?? null,
|
|
470
|
+
includes: this.includes,
|
|
471
|
+
excludes: this.excludes,
|
|
472
|
+
includeNode: this.includeNode,
|
|
473
|
+
excludeNode: this.excludeNode,
|
|
474
|
+
labels: this.labels,
|
|
475
|
+
combined: this.combined,
|
|
476
|
+
},
|
|
477
|
+
// A view of a model that is not resolved draws nothing; the project reports its import.
|
|
478
|
+
hasErrors: base.hasErrors || this.definition === null,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/** The diagram the base analyser would return, from the state built here. */
|
|
482
|
+
diagram(block) {
|
|
483
|
+
return {
|
|
484
|
+
type: "c4",
|
|
485
|
+
id: this.id.text,
|
|
486
|
+
range: this.range(this.node),
|
|
487
|
+
idRange: this.range(this.id),
|
|
488
|
+
node: this.node,
|
|
489
|
+
block,
|
|
490
|
+
properties: { ...this.properties, layers: [...this.properties.layers] },
|
|
491
|
+
elements: this.elements,
|
|
492
|
+
relations: this.relations,
|
|
493
|
+
placement: this.placement,
|
|
494
|
+
constraints: this.constraints,
|
|
495
|
+
styles: this.styles,
|
|
496
|
+
animations: this.animations,
|
|
497
|
+
classes: this.definitions.classes,
|
|
498
|
+
rules: this.definitions.rules,
|
|
499
|
+
icons: [...this.icons, ...this.definitions.icons].sort((a, b) => a.range.start.offset - b.range.start.offset),
|
|
500
|
+
images: [],
|
|
501
|
+
links: [],
|
|
502
|
+
layerRanges: this.layerRanges,
|
|
503
|
+
events: [],
|
|
504
|
+
eventsByKey: new Map(),
|
|
505
|
+
scenarios: [],
|
|
506
|
+
references: this.references,
|
|
507
|
+
c4: null,
|
|
508
|
+
entityReferences: this.entityReferences,
|
|
509
|
+
placementBlocks: this.placementBlocks,
|
|
510
|
+
elementsById: this.elementsById,
|
|
511
|
+
relationsByKey: this.relationsByKey,
|
|
512
|
+
definition: null,
|
|
513
|
+
hasSyntaxErrors: this.node.hasError,
|
|
514
|
+
hasErrors: this.node.hasError || this.errors > 0,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
readScope(stmt, levelStmt) {
|
|
518
|
+
if (!stmt) {
|
|
519
|
+
if (this.level !== "context" && levelStmt && this.levelToken) {
|
|
520
|
+
const m = messages();
|
|
521
|
+
this.error("c4.scope-required", m.diagnostics.c4.scopeRequired(this.level), levelStmt, {
|
|
522
|
+
hints: [this.level === "container" ? m.hints.c4.systemScope : m.hints.c4.containerScope],
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const [arg, extra] = stmt.args;
|
|
528
|
+
if (arg?.kind !== "QualifiedRef" || extra || stmt.block) {
|
|
529
|
+
this.invalid(messages().diagnostics.c4.scopeShape, extra ?? arg ?? stmt.keyword);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const entity = this.entity(arg, "scope");
|
|
533
|
+
if (!entity)
|
|
534
|
+
return;
|
|
535
|
+
const expected = this.level === "component" ? "container" : "system";
|
|
536
|
+
if (entity.kind !== expected) {
|
|
537
|
+
const m = messages().diagnostics.c4;
|
|
538
|
+
this.error("c4.invalid-scope", entity.kind === "datastore" && this.level === "component"
|
|
539
|
+
? m.datastoreScope(entity.path)
|
|
540
|
+
: m.invalidScope(this.level, expected, entity.path, entity.kind), arg);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
this.scope = entity;
|
|
544
|
+
}
|
|
545
|
+
/** The token that stands for an element in this file. */
|
|
546
|
+
tokenFor(entity) {
|
|
547
|
+
const included = this.includeTokens.get(entity.path);
|
|
548
|
+
if (included)
|
|
549
|
+
return included;
|
|
550
|
+
if (this.model.resolved?.file === null)
|
|
551
|
+
return entity.idToken;
|
|
552
|
+
return wholeToken(this.model.ref);
|
|
553
|
+
}
|
|
554
|
+
buildElements(visible) {
|
|
555
|
+
const model = this.definition;
|
|
556
|
+
const scope = this.scope;
|
|
557
|
+
const boundary = scope !== null &&
|
|
558
|
+
this.level !== "context" &&
|
|
559
|
+
[...visible].some((p) => model.entitiesByPath.get(p).parent === scope.path);
|
|
560
|
+
const local = this.model.resolved?.file === null;
|
|
561
|
+
const add = (entity, kind, groupPath) => {
|
|
562
|
+
const token = this.tokenFor(entity);
|
|
563
|
+
const inFile = this.includeTokens.has(entity.path) || local;
|
|
564
|
+
this.addElement({
|
|
565
|
+
id: entity.path,
|
|
566
|
+
kind: kind,
|
|
567
|
+
label: entity.label,
|
|
568
|
+
implicit: false,
|
|
569
|
+
description: entity.description,
|
|
570
|
+
url: entity.url,
|
|
571
|
+
classes: [],
|
|
572
|
+
ports: [],
|
|
573
|
+
layer: null,
|
|
574
|
+
layerRange: null,
|
|
575
|
+
link: null,
|
|
576
|
+
source: null,
|
|
577
|
+
sourceRange: null,
|
|
578
|
+
labelWritten: true,
|
|
579
|
+
entity: { model: model.id, path: entity.path, file: this.model.resolved?.file ?? null },
|
|
580
|
+
technology: entity.technology,
|
|
581
|
+
external: entity.external,
|
|
582
|
+
shape: null,
|
|
583
|
+
typeKind: null,
|
|
584
|
+
data: {},
|
|
585
|
+
groupPath,
|
|
586
|
+
range: inFile && this.includeTokens.has(entity.path)
|
|
587
|
+
? this.range(token)
|
|
588
|
+
: local
|
|
589
|
+
? entity.range
|
|
590
|
+
: this.range(token),
|
|
591
|
+
idRange: this.range(token),
|
|
592
|
+
idToken: token,
|
|
593
|
+
node: undefined,
|
|
594
|
+
});
|
|
595
|
+
if (this.includeTokens.has(entity.path)) {
|
|
596
|
+
this.reference(token, entity.path, "element", "declaration", this.includeNode);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
if (boundary)
|
|
600
|
+
add(scope, "group", []);
|
|
601
|
+
for (const entity of model.entities) {
|
|
602
|
+
if (!visible.has(entity.path) || (boundary && entity.path === scope.path))
|
|
603
|
+
continue;
|
|
604
|
+
add(entity, entity.kind, boundary && entity.parent === scope.path ? [scope.path] : []);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
buildRelations(statements) {
|
|
608
|
+
const model = this.definition;
|
|
609
|
+
// The boundary stands for its children; relations land on drawn entities only.
|
|
610
|
+
const visible = new Set(this.elements.filter((e) => e.kind !== "group").map((e) => e.id));
|
|
611
|
+
// Explicit combinations first: they claim their pairs.
|
|
612
|
+
const claims = new Map();
|
|
613
|
+
const ids = new Map();
|
|
614
|
+
for (const stmt of statements) {
|
|
615
|
+
if (stmt.target.kind !== "QualifiedRef")
|
|
616
|
+
continue;
|
|
617
|
+
const ends = [];
|
|
618
|
+
for (const ref of [stmt.source, stmt.target]) {
|
|
619
|
+
const path = ref.parts.map((p) => p.text).join(".");
|
|
620
|
+
if (!visible.has(path)) {
|
|
621
|
+
this.error("ref.unknown-element", messages().diagnostics.c4.notDrawn(path), ref, {
|
|
622
|
+
hints: [this.notDrawnHint(path)],
|
|
623
|
+
data: { ids: [path] },
|
|
624
|
+
});
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
this.entity(ref, "relation", false);
|
|
628
|
+
this.reference(ref.parts.length === 1 ? ref.parts[0] : wholeToken(ref), path, "element", "relation-endpoint", stmt);
|
|
629
|
+
ends.push(path);
|
|
630
|
+
}
|
|
631
|
+
if (ends.length !== 2)
|
|
632
|
+
continue;
|
|
633
|
+
const [from, to] = ends;
|
|
634
|
+
const pair = `${from}->${to}`;
|
|
635
|
+
if (claims.has(pair)) {
|
|
636
|
+
this.invalid(messages().diagnostics.c4.alreadyCombined(from, to), stmt);
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const idToken = stmt.id && isToken(stmt.id) ? stmt.id : undefined;
|
|
640
|
+
let id = null;
|
|
641
|
+
if (idToken) {
|
|
642
|
+
if (ids.has(idToken.text) || model.relationsByKey.has(idToken.text)) {
|
|
643
|
+
this.error("ref.duplicate-id", messages().diagnostics.c4.relationIdUsed(idToken.text), idToken);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
ids.set(idToken.text, idToken);
|
|
647
|
+
id = idToken.text;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const { description, values } = this.elementBlock(stmt.block, this.profile.relationProperties, "Relation", undefined);
|
|
651
|
+
const relation = {
|
|
652
|
+
key: "",
|
|
653
|
+
id,
|
|
654
|
+
from,
|
|
655
|
+
to,
|
|
656
|
+
line: "solid",
|
|
657
|
+
twoWay: false,
|
|
658
|
+
async: false,
|
|
659
|
+
technology: typeof values.technology === "string" ? values.technology : null,
|
|
660
|
+
sources: [],
|
|
661
|
+
derived: true,
|
|
662
|
+
label: stmt.label && isToken(stmt.label) ? String(stmt.label.value ?? "") : null,
|
|
663
|
+
description,
|
|
664
|
+
assumption: typeof values.assumption === "string" ? values.assumption : null,
|
|
665
|
+
data: {},
|
|
666
|
+
classes: [],
|
|
667
|
+
layer: null,
|
|
668
|
+
layerRange: null,
|
|
669
|
+
groupPath: [],
|
|
670
|
+
range: this.range(stmt),
|
|
671
|
+
idRange: id !== null && idToken ? this.range(idToken) : null,
|
|
672
|
+
node: stmt,
|
|
673
|
+
};
|
|
674
|
+
claims.set(pair, relation);
|
|
675
|
+
if (id !== null && idToken)
|
|
676
|
+
this.reference(idToken, id, "relation", "relation-id", stmt);
|
|
677
|
+
}
|
|
678
|
+
const local = this.model.resolved?.file === null;
|
|
679
|
+
const usingRange = this.range(this.model.ref);
|
|
680
|
+
for (const relation of model.relations) {
|
|
681
|
+
const a = this.lift(relation.from, visible);
|
|
682
|
+
const b = this.lift(relation.to, visible);
|
|
683
|
+
if (!a || !b || a === b)
|
|
684
|
+
continue;
|
|
685
|
+
const claim = claims.get(`${a}->${b}`);
|
|
686
|
+
if (claim) {
|
|
687
|
+
claim.sources.push(relation.key);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
this.relations.push({
|
|
691
|
+
key: relation.key,
|
|
692
|
+
id: relation.id,
|
|
693
|
+
from: a,
|
|
694
|
+
to: b,
|
|
695
|
+
line: "solid",
|
|
696
|
+
twoWay: false,
|
|
697
|
+
async: false,
|
|
698
|
+
technology: relation.technology,
|
|
699
|
+
sources: [relation.key],
|
|
700
|
+
derived: a !== relation.from || b !== relation.to,
|
|
701
|
+
label: relation.label,
|
|
702
|
+
description: relation.description,
|
|
703
|
+
assumption: relation.assumption,
|
|
704
|
+
data: {},
|
|
705
|
+
classes: [],
|
|
706
|
+
layer: null,
|
|
707
|
+
layerRange: null,
|
|
708
|
+
groupPath: [],
|
|
709
|
+
range: local ? relation.range : usingRange,
|
|
710
|
+
idRange: local ? relation.idRange : null,
|
|
711
|
+
node: relation.node,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
const counts = new Map();
|
|
715
|
+
for (const [pair, claim] of claims) {
|
|
716
|
+
if (claim.sources.length === 0) {
|
|
717
|
+
const m = messages();
|
|
718
|
+
this.error("c4.unsupported-relation", m.diagnostics.c4.unsupportedRelation(claim.from, claim.to), claim.node, { hints: [m.hints.c4.addToModel] });
|
|
719
|
+
if (claim.id)
|
|
720
|
+
this.brokenRelationIds.add(claim.id);
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const n = (counts.get(pair) ?? 0) + 1;
|
|
724
|
+
counts.set(pair, n);
|
|
725
|
+
claim.key = claim.id ?? `${pair}#${n}`;
|
|
726
|
+
// A relation that only combines assumptions is one itself.
|
|
727
|
+
if (claim.assumption === null) {
|
|
728
|
+
const combined = claim.sources.map((key) => model.relationsByKey.get(key)?.assumption ?? null);
|
|
729
|
+
if (combined.every((a) => a !== null))
|
|
730
|
+
claim.assumption = combined[0] ?? null;
|
|
731
|
+
}
|
|
732
|
+
this.relations.push(claim);
|
|
733
|
+
this.combined.push(claim.key);
|
|
734
|
+
}
|
|
735
|
+
for (const relation of this.relations) {
|
|
736
|
+
if (!this.relationsByKey.has(relation.key))
|
|
737
|
+
this.relationsByKey.set(relation.key, relation);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
readLabels() {
|
|
741
|
+
const seen = new Map();
|
|
742
|
+
for (const stmt of this.labelStatements) {
|
|
743
|
+
const [target, text, extra] = stmt.args;
|
|
744
|
+
if (target?.kind !== "QualifiedRef" ||
|
|
745
|
+
text?.kind !== "StringLiteral" ||
|
|
746
|
+
extra ||
|
|
747
|
+
stmt.block) {
|
|
748
|
+
this.invalid(messages().diagnostics.c4.labelShape, extra ?? text ?? target ?? stmt.keyword);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const path = target.parts.map((p) => p.text).join(".");
|
|
752
|
+
const token = target.parts.length === 1 ? target.parts[0] : wholeToken(target);
|
|
753
|
+
const element = this.elementsById.get(path);
|
|
754
|
+
const relation = element ? undefined : this.relationsByKey.get(path);
|
|
755
|
+
if (!element && !relation) {
|
|
756
|
+
const known = this.definition.entitiesByPath.has(path) || this.definition.relationsByKey.has(path);
|
|
757
|
+
this.error(known && this.definition.relationsByKey.has(path)
|
|
758
|
+
? "ref.unknown-relation"
|
|
759
|
+
: "ref.unknown-element", messages().diagnostics.c4.notDrawn(path), target, {
|
|
760
|
+
data: { ids: [path] },
|
|
761
|
+
hints: [
|
|
762
|
+
this.definition.entitiesByPath.has(path)
|
|
763
|
+
? this.notDrawnHint(path)
|
|
764
|
+
: messages().hints.c4.localLabel,
|
|
765
|
+
],
|
|
766
|
+
});
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const first = seen.get(path);
|
|
770
|
+
if (first) {
|
|
771
|
+
const m = messages();
|
|
772
|
+
this.error("prop.duplicate", m.diagnostics.c4.labelTwice(path), stmt.keyword, {
|
|
773
|
+
related: [{ message: m.related.firstSet, at: first }],
|
|
774
|
+
});
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
seen.set(path, stmt.keyword);
|
|
778
|
+
if (element) {
|
|
779
|
+
this.entity(target, "label", false);
|
|
780
|
+
this.reference(token, path, "element", "label", stmt);
|
|
781
|
+
const index = this.elements.indexOf(element);
|
|
782
|
+
const updated = { ...element, label: text.value };
|
|
783
|
+
this.elements[index] = updated;
|
|
784
|
+
this.elementsById.set(path, updated);
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
this.reference(token, relation.key, "relation", "label", stmt);
|
|
788
|
+
relation.label = text.value;
|
|
789
|
+
}
|
|
790
|
+
this.labels.push({
|
|
791
|
+
target: path,
|
|
792
|
+
targetKind: element ? "element" : "relation",
|
|
793
|
+
text: text.value,
|
|
794
|
+
range: this.range(stmt),
|
|
795
|
+
targetRange: this.range(target),
|
|
796
|
+
node: stmt,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
classify(_statements, _groupPath, _context, _out) { }
|
|
801
|
+
}
|
|
802
|
+
export { isAncestorPath };
|
|
803
|
+
//# sourceMappingURL=c4.js.map
|