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