@meltemi/diagrams-edit 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 +56 -2
- package/dist/animate.d.ts +18 -0
- package/dist/animate.d.ts.map +1 -0
- package/dist/animate.js +188 -0
- package/dist/animate.js.map +1 -0
- package/dist/batch.d.ts +18 -0
- package/dist/batch.d.ts.map +1 -0
- package/dist/batch.js +120 -0
- package/dist/batch.js.map +1 -0
- package/dist/c4.d.ts +30 -0
- package/dist/c4.d.ts.map +1 -0
- package/dist/c4.js +676 -0
- package/dist/c4.js.map +1 -0
- package/dist/context.d.ts +36 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +133 -0
- package/dist/context.js.map +1 -0
- package/dist/create.d.ts +28 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +200 -0
- package/dist/create.js.map +1 -0
- package/dist/delete.d.ts +10 -0
- package/dist/delete.d.ts.map +1 -0
- package/dist/delete.js +150 -0
- package/dist/delete.js.map +1 -0
- package/dist/diagrams.d.ts +19 -0
- package/dist/diagrams.d.ts.map +1 -0
- package/dist/diagrams.js +92 -0
- package/dist/diagrams.js.map +1 -0
- package/dist/free.d.ts +54 -0
- package/dist/free.d.ts.map +1 -0
- package/dist/free.js +596 -0
- package/dist/free.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/insert.d.ts +41 -0
- package/dist/insert.d.ts.map +1 -0
- package/dist/insert.js +165 -0
- package/dist/insert.js.map +1 -0
- package/dist/labels.d.ts +8 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +186 -0
- package/dist/labels.js.map +1 -0
- package/dist/messages/en.d.ts +525 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +519 -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 +366 -0
- package/dist/messages/es.js.map +1 -0
- package/dist/messages/index.d.ts +288 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +15 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/plan.d.ts +24 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +881 -0
- package/dist/plan.js.map +1 -0
- package/dist/relations.d.ts +19 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +167 -0
- package/dist/relations.js.map +1 -0
- package/dist/routes.d.ts +21 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +37 -0
- package/dist/routes.js.map +1 -0
- package/dist/sequence.d.ts +44 -0
- package/dist/sequence.d.ts.map +1 -0
- package/dist/sequence.js +695 -0
- package/dist/sequence.js.map +1 -0
- package/dist/stories.d.ts +23 -0
- package/dist/stories.d.ts.map +1 -0
- package/dist/stories.js +238 -0
- package/dist/stories.js.map +1 -0
- package/dist/structure.d.ts +29 -0
- package/dist/structure.d.ts.map +1 -0
- package/dist/structure.js +541 -0
- package/dist/structure.js.map +1 -0
- package/dist/styles.d.ts +11 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +219 -0
- package/dist/styles.js.map +1 -0
- package/dist/text.d.ts +26 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +59 -0
- package/dist/text.js.map +1 -0
- package/dist/themes.d.ts +9 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +110 -0
- package/dist/themes.js.map +1 -0
- package/dist/types.d.ts +639 -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 +43 -3
package/dist/c4.js
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { isIdentifierText, isToken, } from "@meltemi/diagrams-language";
|
|
2
|
+
import { ENTITY_CHILDREN, ENTITY_KINDS, isSafeUrl, } from "@meltemi/diagrams-model";
|
|
3
|
+
import { insertStatement, reject } from "./context.js";
|
|
4
|
+
import { deleteStatements } from "./delete.js";
|
|
5
|
+
import { insertAfter, insertBefore, verifyNoNewErrors } from "./insert.js";
|
|
6
|
+
import { messages, reasons } from "./messages/index.js";
|
|
7
|
+
import { removeListItems } from "./plan.js";
|
|
8
|
+
import { storyDependents } from "./stories.js";
|
|
9
|
+
import { quoteString } from "./text.js";
|
|
10
|
+
// ---------------------------------------------------------------------- views
|
|
11
|
+
/** Operations a C4 view accepts; the others write declarations the view does not have. */
|
|
12
|
+
const VIEW_OPERATIONS = new Set([
|
|
13
|
+
"setPosition",
|
|
14
|
+
"setSize",
|
|
15
|
+
"setRoute",
|
|
16
|
+
"setLabelAnchor",
|
|
17
|
+
"arrange",
|
|
18
|
+
"setDiagramProperty",
|
|
19
|
+
"setStyleProperty",
|
|
20
|
+
"setAnimation",
|
|
21
|
+
"removeAnimation",
|
|
22
|
+
"createRelation",
|
|
23
|
+
"removeRelation",
|
|
24
|
+
"setRelationLabel",
|
|
25
|
+
"includeEntity",
|
|
26
|
+
"excludeEntity",
|
|
27
|
+
"setLocalLabel",
|
|
28
|
+
]);
|
|
29
|
+
/** Refuses an operation that does not apply to a C4 view, naming the ones that do. */
|
|
30
|
+
export function refuseInView(ctx, kind) {
|
|
31
|
+
if (ctx.diagram.type !== "c4" || VIEW_OPERATIONS.has(kind))
|
|
32
|
+
return;
|
|
33
|
+
reject("edit.invalid-value", reasons().c4.notInView(kind, ctx.diagram.c4?.model ?? ""));
|
|
34
|
+
}
|
|
35
|
+
function requireView(ctx) {
|
|
36
|
+
const c4 = ctx.diagram.c4;
|
|
37
|
+
if (!c4)
|
|
38
|
+
reject("edit.unsafe-target", reasons().c4.notView(ctx.diagram.id));
|
|
39
|
+
return c4;
|
|
40
|
+
}
|
|
41
|
+
/** Relations of the view that are combinations written in it, not model relations. */
|
|
42
|
+
export function isCombination(diagram, key) {
|
|
43
|
+
return diagram.c4?.combined.includes(key) ?? false;
|
|
44
|
+
}
|
|
45
|
+
/** The block of the view's diagram. */
|
|
46
|
+
function viewBody(ctx) {
|
|
47
|
+
const block = ctx.diagram.block;
|
|
48
|
+
if (!block)
|
|
49
|
+
reject("edit.unsafe-target", reasons().c4.viewNoBody(ctx.diagram.id));
|
|
50
|
+
return block;
|
|
51
|
+
}
|
|
52
|
+
/** The statement after which a view statement goes: the last of level, scope, include, exclude. */
|
|
53
|
+
function viewAnchor(ctx, words) {
|
|
54
|
+
const body = viewBody(ctx);
|
|
55
|
+
let anchor;
|
|
56
|
+
for (const stmt of body.statements) {
|
|
57
|
+
if (stmt.kind === "KeywordStatement" && words.includes(stmt.keyword.text))
|
|
58
|
+
anchor = stmt;
|
|
59
|
+
}
|
|
60
|
+
return anchor;
|
|
61
|
+
}
|
|
62
|
+
function insertViewStatement(ctx, text, after) {
|
|
63
|
+
const body = viewBody(ctx);
|
|
64
|
+
const anchor = viewAnchor(ctx, after);
|
|
65
|
+
if (anchor)
|
|
66
|
+
return insertAfter(ctx, body, anchor, () => text);
|
|
67
|
+
const first = body.statements[0];
|
|
68
|
+
return first
|
|
69
|
+
? insertBefore(ctx, first, () => text)
|
|
70
|
+
: insertStatement(ctx, body, ctx.diagram.node.start, () => text);
|
|
71
|
+
}
|
|
72
|
+
function requireEntity(ctx, path) {
|
|
73
|
+
const model = requireView(ctx).resolved?.definition;
|
|
74
|
+
if (!model)
|
|
75
|
+
reject("edit.unsafe-target", reasons().c4.unresolved);
|
|
76
|
+
const entity = model.entitiesByPath.get(path);
|
|
77
|
+
if (!entity)
|
|
78
|
+
reject("edit.unsafe-target", reasons().c4.noEntity(model.id, path));
|
|
79
|
+
return entity;
|
|
80
|
+
}
|
|
81
|
+
export function includeEntity(ctx, op) {
|
|
82
|
+
const c4 = requireView(ctx);
|
|
83
|
+
requireEntity(ctx, op.entity);
|
|
84
|
+
if (ctx.diagram.elementsById.has(op.entity))
|
|
85
|
+
return [];
|
|
86
|
+
let edits;
|
|
87
|
+
const excluded = c4.excludes.findIndex((e) => e.path === op.entity);
|
|
88
|
+
if (excluded >= 0 && c4.excludeNode) {
|
|
89
|
+
edits =
|
|
90
|
+
c4.excludes.length === 1
|
|
91
|
+
? deleteStatements(ctx.text, ctx.compilation.parse.root, [c4.excludeNode])
|
|
92
|
+
: removeListItems(c4.excludeNode.args, new Set([excluded]));
|
|
93
|
+
}
|
|
94
|
+
else if (c4.includes !== null && c4.includeNode) {
|
|
95
|
+
const last = c4.includeNode.args[c4.includeNode.args.length - 1];
|
|
96
|
+
edits = [{ start: last.end, end: last.end, newText: `, ${op.entity}` }];
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// The default set becomes an explicit list, so that nothing else changes.
|
|
100
|
+
const drawn = ctx.diagram.elements.filter((e) => e.kind !== "group").map((e) => e.id);
|
|
101
|
+
edits = [
|
|
102
|
+
insertViewStatement(ctx, `include ${[...drawn, op.entity].join(", ")}`, ["level", "scope"]),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
verifyNoNewErrors(ctx, edits);
|
|
106
|
+
return edits;
|
|
107
|
+
}
|
|
108
|
+
/** The view statements that only target an element, and edits that drop it from shared ones. */
|
|
109
|
+
function viewDependents(ctx, id) {
|
|
110
|
+
const diagram = ctx.diagram;
|
|
111
|
+
const lines = ctx.compilation.parse.lines;
|
|
112
|
+
const statements = [];
|
|
113
|
+
const edits = [];
|
|
114
|
+
const dependents = [];
|
|
115
|
+
const depend = (kind, name, node) => {
|
|
116
|
+
statements.push(node);
|
|
117
|
+
dependents.push({
|
|
118
|
+
kind,
|
|
119
|
+
name,
|
|
120
|
+
description: messages().dependents.statement(kind, name, lines.location(node.start).line),
|
|
121
|
+
range: lines.range(node),
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
const gone = new Set(diagram.relations.filter((r) => r.from === id || r.to === id).map((r) => r.key));
|
|
125
|
+
for (const entry of diagram.placement) {
|
|
126
|
+
if (entry.target === id || gone.has(entry.target))
|
|
127
|
+
depend(entry.statement, entry.target, entry.node);
|
|
128
|
+
}
|
|
129
|
+
for (const label of diagram.c4?.labels ?? []) {
|
|
130
|
+
if (label.target === id || gone.has(label.target))
|
|
131
|
+
depend("label", label.target, label.node);
|
|
132
|
+
}
|
|
133
|
+
for (const relation of diagram.relations) {
|
|
134
|
+
if (gone.has(relation.key) && isCombination(diagram, relation.key))
|
|
135
|
+
depend("relation", relation.key, relation.node);
|
|
136
|
+
}
|
|
137
|
+
const partial = new Map();
|
|
138
|
+
for (const entry of [...diagram.styles, ...diagram.animations]) {
|
|
139
|
+
if (entry.target !== id && !gone.has(entry.target))
|
|
140
|
+
continue;
|
|
141
|
+
const index = entry.node.args.findIndex((a) => a.start === entry.targetRange.start.offset);
|
|
142
|
+
const set = partial.get(entry.node) ?? new Set();
|
|
143
|
+
if (index >= 0)
|
|
144
|
+
set.add(index);
|
|
145
|
+
partial.set(entry.node, set);
|
|
146
|
+
dependents.push({
|
|
147
|
+
kind: "properties" in entry ? "style" : "animate",
|
|
148
|
+
name: entry.target,
|
|
149
|
+
description: messages().dependents.statement("properties" in entry ? "style" : "animate", entry.target, lines.location(entry.node.start).line),
|
|
150
|
+
range: lines.range(entry.node),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
for (const [node, indices] of partial) {
|
|
154
|
+
if (indices.size >= node.args.length)
|
|
155
|
+
statements.push(node);
|
|
156
|
+
else
|
|
157
|
+
edits.push(...removeListItems(node.args, indices));
|
|
158
|
+
}
|
|
159
|
+
for (const constraint of diagram.constraints) {
|
|
160
|
+
const index = constraint.targets.indexOf(id);
|
|
161
|
+
if (index < 0)
|
|
162
|
+
continue;
|
|
163
|
+
if (constraint.kind === "distance" || constraint.targets.length <= 2)
|
|
164
|
+
depend("constraint", constraint.kind, constraint.node);
|
|
165
|
+
else
|
|
166
|
+
edits.push(...removeListItems(constraint.node.args.slice(0, constraint.targets.length), new Set([index])));
|
|
167
|
+
}
|
|
168
|
+
return { statements, edits, dependents };
|
|
169
|
+
}
|
|
170
|
+
export function excludeEntity(ctx, op) {
|
|
171
|
+
const c4 = requireView(ctx);
|
|
172
|
+
requireEntity(ctx, op.entity);
|
|
173
|
+
const element = ctx.diagram.elementsById.get(op.entity);
|
|
174
|
+
if (!element)
|
|
175
|
+
return [];
|
|
176
|
+
if (element.kind === "group") {
|
|
177
|
+
reject("edit.unsafe-target", reasons().c4.isScope(op.entity));
|
|
178
|
+
}
|
|
179
|
+
const dependents = viewDependents(ctx, op.entity);
|
|
180
|
+
if (!op.cascade && dependents.dependents.length > 0) {
|
|
181
|
+
reject("edit.has-dependents", reasons().c4.removingFromView(op.entity, dependents.dependents.length), dependents.dependents);
|
|
182
|
+
}
|
|
183
|
+
const edits = [...dependents.edits];
|
|
184
|
+
const deleted = [...dependents.statements];
|
|
185
|
+
const included = c4.includes?.findIndex((e) => e.path === op.entity) ?? -1;
|
|
186
|
+
if (included >= 0 && c4.includeNode) {
|
|
187
|
+
if (c4.includes.length === 1)
|
|
188
|
+
deleted.push(c4.includeNode);
|
|
189
|
+
else
|
|
190
|
+
edits.push(...removeListItems(c4.includeNode.args, new Set([included])));
|
|
191
|
+
}
|
|
192
|
+
else if (c4.excludeNode) {
|
|
193
|
+
const last = c4.excludeNode.args[c4.excludeNode.args.length - 1];
|
|
194
|
+
edits.push({ start: last.end, end: last.end, newText: `, ${op.entity}` });
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
edits.push(insertViewStatement(ctx, `exclude ${op.entity}`, ["level", "scope", "include"]));
|
|
198
|
+
}
|
|
199
|
+
edits.push(...deleteStatements(ctx.text, ctx.compilation.parse.root, deleted));
|
|
200
|
+
verifyNoNewErrors(ctx, edits);
|
|
201
|
+
return edits;
|
|
202
|
+
}
|
|
203
|
+
export function setLocalLabel(ctx, op) {
|
|
204
|
+
const c4 = requireView(ctx);
|
|
205
|
+
if (!ctx.diagram.elementsById.has(op.target) && !ctx.diagram.relationsByKey.has(op.target)) {
|
|
206
|
+
reject("edit.unsafe-target", reasons().c4.notDrawn(op.target, ctx.diagram.id));
|
|
207
|
+
}
|
|
208
|
+
const entry = c4.labels.find((l) => l.target === op.target);
|
|
209
|
+
let edits;
|
|
210
|
+
if (op.label === null || op.label === "") {
|
|
211
|
+
if (!entry)
|
|
212
|
+
return [];
|
|
213
|
+
edits = deleteStatements(ctx.text, ctx.compilation.parse.root, [entry.node]);
|
|
214
|
+
}
|
|
215
|
+
else if (entry) {
|
|
216
|
+
const arg = entry.node.args[1];
|
|
217
|
+
const quoted = quoteString(op.label);
|
|
218
|
+
if (arg.kind === "StringLiteral" && arg.token.text === quoted)
|
|
219
|
+
return [];
|
|
220
|
+
edits = [{ start: arg.start, end: arg.end, newText: quoted }];
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const relation = ctx.diagram.relationsByKey.get(op.target);
|
|
224
|
+
if (relation && relation.id === null) {
|
|
225
|
+
reject("edit.anonymous-relation", reasons().c4.nameRelation);
|
|
226
|
+
}
|
|
227
|
+
edits = [
|
|
228
|
+
insertViewStatement(ctx, `label ${op.target} ${quoteString(op.label)}`, [
|
|
229
|
+
"level",
|
|
230
|
+
"scope",
|
|
231
|
+
"include",
|
|
232
|
+
"exclude",
|
|
233
|
+
"label",
|
|
234
|
+
]),
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
verifyNoNewErrors(ctx, edits);
|
|
238
|
+
return edits;
|
|
239
|
+
}
|
|
240
|
+
export function createModelContext(ctx, id) {
|
|
241
|
+
const model = ctx.compilation.document.models.find((m) => m.id === id);
|
|
242
|
+
if (!model)
|
|
243
|
+
reject("edit.unsafe-target", reasons().c4.noModel(id));
|
|
244
|
+
if (model.node.hasError)
|
|
245
|
+
reject("edit.unsafe-target", reasons().c4.modelErrors(id));
|
|
246
|
+
return { ...ctx, model };
|
|
247
|
+
}
|
|
248
|
+
function requireModelEntity(ctx, path) {
|
|
249
|
+
const entity = ctx.model.entitiesByPath.get(path);
|
|
250
|
+
if (!entity)
|
|
251
|
+
reject("edit.unsafe-target", reasons().c4.noEntity(ctx.model.id, path));
|
|
252
|
+
return entity;
|
|
253
|
+
}
|
|
254
|
+
function requireModelRelation(ctx, key) {
|
|
255
|
+
const relation = ctx.model.relationsByKey.get(key);
|
|
256
|
+
if (!relation)
|
|
257
|
+
reject("edit.unsafe-target", reasons().c4.noRelation(ctx.model.id, key));
|
|
258
|
+
return relation;
|
|
259
|
+
}
|
|
260
|
+
function modelBody(ctx) {
|
|
261
|
+
const block = ctx.model.node.block;
|
|
262
|
+
if (block.kind !== "Block")
|
|
263
|
+
reject("edit.unsafe-target", reasons().c4.modelNoBody);
|
|
264
|
+
return block;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Every reference of the file to entities of the model: in the model, its views, links and the
|
|
268
|
+
* stories of its views.
|
|
269
|
+
*/
|
|
270
|
+
export function entityReferences(compilation, model) {
|
|
271
|
+
const found = [...model.references];
|
|
272
|
+
const local = (diagram) => diagram?.c4 !== null &&
|
|
273
|
+
diagram?.c4.resolved?.file === null &&
|
|
274
|
+
diagram.c4.resolved.definition.id === model.id;
|
|
275
|
+
for (const diagram of compilation.document.diagrams) {
|
|
276
|
+
for (const reference of diagram.entityReferences) {
|
|
277
|
+
if (reference.role === "link" ? reference.model === model.id : local(diagram)) {
|
|
278
|
+
found.push(reference);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const story of compilation.document.stories) {
|
|
283
|
+
for (const reference of story.entityReferences) {
|
|
284
|
+
if (reference.view.file === null && local(reference.view.diagram))
|
|
285
|
+
found.push(reference);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return found;
|
|
289
|
+
}
|
|
290
|
+
export function renameEntity(ctx, op) {
|
|
291
|
+
const entity = requireModelEntity(ctx, op.entity);
|
|
292
|
+
if (op.newId === entity.id)
|
|
293
|
+
return [];
|
|
294
|
+
if (!isIdentifierText(op.newId)) {
|
|
295
|
+
reject("edit.invalid-id", reasons().ids.invalidEntity(op.newId));
|
|
296
|
+
}
|
|
297
|
+
const sibling = entity.parent ? `${entity.parent}.${op.newId}` : op.newId;
|
|
298
|
+
if (ctx.model.entitiesByPath.has(sibling)) {
|
|
299
|
+
reject("edit.id-conflict", reasons().c4.entityExists(sibling, ctx.model.id));
|
|
300
|
+
}
|
|
301
|
+
const tokens = new Map();
|
|
302
|
+
for (const reference of entityReferences(ctx.compilation, ctx.model)) {
|
|
303
|
+
if (reference.path === entity.path)
|
|
304
|
+
tokens.set(reference.token.start, reference.token);
|
|
305
|
+
}
|
|
306
|
+
const edits = [...tokens.values()].map((t) => ({
|
|
307
|
+
start: t.start,
|
|
308
|
+
end: t.end,
|
|
309
|
+
newText: op.newId,
|
|
310
|
+
}));
|
|
311
|
+
verifyNoNewErrors(ctx, edits);
|
|
312
|
+
return edits;
|
|
313
|
+
}
|
|
314
|
+
/** The entities nested in an entity, at any depth. */
|
|
315
|
+
function descendants(model, path) {
|
|
316
|
+
return model.entities.filter((e) => e.path.startsWith(`${path}.`));
|
|
317
|
+
}
|
|
318
|
+
export function removeEntity(ctx, op) {
|
|
319
|
+
const entity = requireModelEntity(ctx, op.entity);
|
|
320
|
+
const model = ctx.model;
|
|
321
|
+
const removed = new Set([entity.path, ...descendants(model, entity.path).map((e) => e.path)]);
|
|
322
|
+
const lines = ctx.compilation.parse.lines;
|
|
323
|
+
const statements = [entity.node];
|
|
324
|
+
const edits = [];
|
|
325
|
+
const dependents = [];
|
|
326
|
+
const depend = (kind, name, node) => {
|
|
327
|
+
statements.push(node);
|
|
328
|
+
dependents.push({
|
|
329
|
+
kind,
|
|
330
|
+
name,
|
|
331
|
+
description: messages().dependents.statement(kind, name, lines.location(node.start).line),
|
|
332
|
+
range: lines.range(node),
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
for (const child of descendants(model, entity.path)) {
|
|
336
|
+
dependents.push({
|
|
337
|
+
kind: "element",
|
|
338
|
+
name: child.path,
|
|
339
|
+
description: messages().dependents.statement(child.kind, child.path, child.range.start.line),
|
|
340
|
+
range: child.range,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
for (const relation of model.relations) {
|
|
344
|
+
if (removed.has(relation.from) || removed.has(relation.to))
|
|
345
|
+
depend("relation", relation.key, relation.node);
|
|
346
|
+
}
|
|
347
|
+
const relationKeys = new Set(model.relations.filter((r) => removed.has(r.from) || removed.has(r.to)).map((r) => r.key));
|
|
348
|
+
// Views of this model in the file: list entries, labels, placement, styles and combinations.
|
|
349
|
+
for (const diagram of ctx.compilation.document.diagrams) {
|
|
350
|
+
const c4 = diagram.c4;
|
|
351
|
+
if (!c4 || c4.resolved?.file !== null || c4.resolved.definition.id !== model.id)
|
|
352
|
+
continue;
|
|
353
|
+
if (c4.scope !== null && removed.has(c4.scope)) {
|
|
354
|
+
reject("edit.unsafe-target", reasons().c4.opensScope(diagram.id, c4.scope));
|
|
355
|
+
}
|
|
356
|
+
for (const [list, node] of [
|
|
357
|
+
[c4.includes ?? [], c4.includeNode],
|
|
358
|
+
[c4.excludes, c4.excludeNode],
|
|
359
|
+
]) {
|
|
360
|
+
if (!node)
|
|
361
|
+
continue;
|
|
362
|
+
const indices = new Set(list.flatMap((e, i) => (removed.has(e.path) ? [i] : [])));
|
|
363
|
+
if (indices.size === 0)
|
|
364
|
+
continue;
|
|
365
|
+
if (indices.size === node.args.length)
|
|
366
|
+
depend("element", node.keyword.text, node);
|
|
367
|
+
else {
|
|
368
|
+
edits.push(...removeListItems(node.args, indices));
|
|
369
|
+
dependents.push({
|
|
370
|
+
kind: "element",
|
|
371
|
+
name: node.keyword.text,
|
|
372
|
+
description: messages().dependents.viewList(node.keyword.text, diagram.id, lines.location(node.start).line),
|
|
373
|
+
range: lines.range(node),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const label of c4.labels) {
|
|
378
|
+
if (removed.has(label.target) || relationKeys.has(label.target))
|
|
379
|
+
depend("label", label.target, label.node);
|
|
380
|
+
}
|
|
381
|
+
for (const entry of diagram.placement) {
|
|
382
|
+
const target = entry.target;
|
|
383
|
+
const relation = diagram.relationsByKey.get(target);
|
|
384
|
+
const touches = removed.has(target) || (relation && relation.sources.some((s) => relationKeys.has(s)));
|
|
385
|
+
if (touches)
|
|
386
|
+
depend(entry.statement, target, entry.node);
|
|
387
|
+
}
|
|
388
|
+
for (const entry of [...diagram.styles, ...diagram.animations]) {
|
|
389
|
+
if (!removed.has(entry.target) && !relationKeys.has(entry.target))
|
|
390
|
+
continue;
|
|
391
|
+
depend("properties" in entry ? "style" : "animate", entry.target, entry.node);
|
|
392
|
+
}
|
|
393
|
+
for (const reference of diagram.entityReferences) {
|
|
394
|
+
if (reference.role !== "relation" || !removed.has(reference.path))
|
|
395
|
+
continue;
|
|
396
|
+
const combined = diagram.relations.find((r) => isCombination(diagram, r.key) &&
|
|
397
|
+
r.node.start <= reference.token.start &&
|
|
398
|
+
reference.token.end <= r.node.end);
|
|
399
|
+
if (combined)
|
|
400
|
+
depend("relation", combined.key, combined.node);
|
|
401
|
+
}
|
|
402
|
+
const stories = storyDependents(ctx.compilation, diagram.id, removed);
|
|
403
|
+
statements.push(...stories.statements);
|
|
404
|
+
edits.push(...stories.edits);
|
|
405
|
+
dependents.push(...stories.dependents);
|
|
406
|
+
}
|
|
407
|
+
// Links of other diagrams to the removed entities.
|
|
408
|
+
for (const diagram of ctx.compilation.document.diagrams) {
|
|
409
|
+
for (const link of diagram.links) {
|
|
410
|
+
if (link.target === "model" &&
|
|
411
|
+
link.binding === null &&
|
|
412
|
+
link.diagram === model.id &&
|
|
413
|
+
removed.has(link.element)) {
|
|
414
|
+
depend("link", link.text, link.node);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (!op.cascade && dependents.length > 0) {
|
|
419
|
+
reject("edit.has-dependents", reasons().c4.removingEntity(entity.path, dependents.length), dependents);
|
|
420
|
+
}
|
|
421
|
+
edits.push(...deleteStatements(ctx.text, ctx.compilation.parse.root, statements));
|
|
422
|
+
verifyNoNewErrors(ctx, edits);
|
|
423
|
+
return edits;
|
|
424
|
+
}
|
|
425
|
+
/** The written string argument after the id of an entity, or undefined. */
|
|
426
|
+
function labelArg(entity) {
|
|
427
|
+
const arg = entity.node.args[1];
|
|
428
|
+
return arg?.kind === "StringLiteral" ? arg : undefined;
|
|
429
|
+
}
|
|
430
|
+
/** Writes, replaces or (with null) removes one statement of a block, adding the block if needed. */
|
|
431
|
+
function setBlockStatement(ctx, owner, block, keyword, text) {
|
|
432
|
+
const existing = block?.statements.find((s) => s.kind === "KeywordStatement" && s.keyword.text === keyword);
|
|
433
|
+
if (text === null) {
|
|
434
|
+
if (!existing)
|
|
435
|
+
return [];
|
|
436
|
+
// A block left empty goes too.
|
|
437
|
+
if (block && block.statements.length === 1)
|
|
438
|
+
return [{ start: block.fullStart, end: block.end, newText: "" }];
|
|
439
|
+
return deleteStatements(ctx.text, ctx.compilation.parse.root, [existing]);
|
|
440
|
+
}
|
|
441
|
+
if (existing) {
|
|
442
|
+
if (ctx.text.slice(existing.start, existing.end) === text)
|
|
443
|
+
return [];
|
|
444
|
+
return [{ start: existing.start, end: existing.end, newText: text }];
|
|
445
|
+
}
|
|
446
|
+
if (block) {
|
|
447
|
+
// Properties go before child entities and relations.
|
|
448
|
+
const first = block.statements[0];
|
|
449
|
+
return [
|
|
450
|
+
first
|
|
451
|
+
? insertBefore(ctx, first, () => text)
|
|
452
|
+
: insertStatement(ctx, block, owner.start, () => text),
|
|
453
|
+
];
|
|
454
|
+
}
|
|
455
|
+
return [{ start: owner.end, end: owner.end, newText: ` { ${text} }` }];
|
|
456
|
+
}
|
|
457
|
+
export function setEntityProperty(ctx, op) {
|
|
458
|
+
const entity = requireModelEntity(ctx, op.entity);
|
|
459
|
+
const node = entity.node;
|
|
460
|
+
let edits;
|
|
461
|
+
switch (op.property) {
|
|
462
|
+
case "label": {
|
|
463
|
+
const current = labelArg(entity);
|
|
464
|
+
const value = op.value === null || op.value === "" ? null : String(op.value);
|
|
465
|
+
if (value === null || value === entity.id) {
|
|
466
|
+
edits = current ? [{ start: node.args[0].end, end: current.end, newText: "" }] : [];
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
const quoted = quoteString(value);
|
|
470
|
+
edits = current
|
|
471
|
+
? current.token.text === quoted
|
|
472
|
+
? []
|
|
473
|
+
: [{ start: current.start, end: current.end, newText: quoted }]
|
|
474
|
+
: [{ start: node.args[0].end, end: node.args[0].end, newText: ` ${quoted}` }];
|
|
475
|
+
}
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
case "url":
|
|
479
|
+
case "description":
|
|
480
|
+
case "technology":
|
|
481
|
+
if (op.property === "url" &&
|
|
482
|
+
op.value !== null &&
|
|
483
|
+
op.value !== "" &&
|
|
484
|
+
!isSafeUrl(String(op.value))) {
|
|
485
|
+
reject("edit.invalid-value", reasons().values.unsafeUrl(String(op.value)));
|
|
486
|
+
}
|
|
487
|
+
edits = setBlockStatement(ctx, node, node.block, op.property, op.value === null || op.value === ""
|
|
488
|
+
? null
|
|
489
|
+
: `${op.property} ${quoteString(String(op.value))}`);
|
|
490
|
+
break;
|
|
491
|
+
case "external": {
|
|
492
|
+
if (entity.kind !== "person" && entity.kind !== "system") {
|
|
493
|
+
reject("edit.invalid-value", reasons().c4.external);
|
|
494
|
+
}
|
|
495
|
+
const on = op.value === true || op.value === "true";
|
|
496
|
+
edits = setBlockStatement(ctx, node, node.block, "external", on ? "external" : null);
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
verifyNoNewErrors(ctx, edits);
|
|
501
|
+
return edits;
|
|
502
|
+
}
|
|
503
|
+
/** A free id among the siblings of a new entity. */
|
|
504
|
+
function freshEntityId(ctx, parent, kind) {
|
|
505
|
+
for (let n = 1;; n++) {
|
|
506
|
+
const id = `${kind}${n}`;
|
|
507
|
+
if (!ctx.model.entitiesByPath.has(parent ? `${parent}.${id}` : id))
|
|
508
|
+
return id;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
export function createEntity(ctx, op) {
|
|
512
|
+
if (!ENTITY_KINDS.includes(op.entityKind)) {
|
|
513
|
+
reject("edit.invalid-value", reasons().c4.notEntityKind(op.entityKind, ENTITY_KINDS));
|
|
514
|
+
}
|
|
515
|
+
const parent = op.parent ? requireModelEntity(ctx, op.parent) : null;
|
|
516
|
+
const allowed = ENTITY_CHILDREN[parent?.kind ?? "model"];
|
|
517
|
+
if (!allowed.includes(op.entityKind)) {
|
|
518
|
+
reject("edit.invalid-value", reasons().c4.misplaced(op.entityKind, parent));
|
|
519
|
+
}
|
|
520
|
+
const id = op.id ?? freshEntityId(ctx, parent?.path ?? null, op.entityKind);
|
|
521
|
+
if (!isIdentifierText(id))
|
|
522
|
+
reject("edit.invalid-id", reasons().ids.notValid(id));
|
|
523
|
+
const path = parent ? `${parent.path}.${id}` : id;
|
|
524
|
+
if (ctx.model.entitiesByPath.has(path))
|
|
525
|
+
reject("edit.id-conflict", reasons().c4.pathExists(path));
|
|
526
|
+
const label = op.label ?? id[0].toUpperCase() + id.slice(1);
|
|
527
|
+
const text = `${op.entityKind} ${id} ${quoteString(label)}`;
|
|
528
|
+
let edits;
|
|
529
|
+
const block = parent ? parent.node.block : modelBody(ctx);
|
|
530
|
+
if (!block) {
|
|
531
|
+
edits = [
|
|
532
|
+
{
|
|
533
|
+
start: parent.node.end,
|
|
534
|
+
end: parent.node.end,
|
|
535
|
+
newText: ` {${ctx.eol}${indentOf(ctx, parent.node)}${ctx.unit}${text}${ctx.eol}${indentOf(ctx, parent.node)}}`,
|
|
536
|
+
},
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// After the last entity of the block, else first.
|
|
541
|
+
const entities = block.statements.filter((s) => s.kind === "KeywordStatement" &&
|
|
542
|
+
ENTITY_KINDS.includes(s.keyword.text));
|
|
543
|
+
const last = entities[entities.length - 1];
|
|
544
|
+
edits = [
|
|
545
|
+
last
|
|
546
|
+
? insertAfter(ctx, block, last, () => text)
|
|
547
|
+
: insertStatement(ctx, block, (parent?.node ?? ctx.model.node).start, () => text),
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
verifyNoNewErrors(ctx, edits);
|
|
551
|
+
return edits;
|
|
552
|
+
}
|
|
553
|
+
function indentOf(ctx, node) {
|
|
554
|
+
const start = ctx.text.lastIndexOf("\n", node.start - 1) + 1;
|
|
555
|
+
return /^[ \t]*/.exec(ctx.text.slice(start))[0];
|
|
556
|
+
}
|
|
557
|
+
export function createModelRelation(ctx, op) {
|
|
558
|
+
requireModelEntity(ctx, op.from);
|
|
559
|
+
requireModelEntity(ctx, op.to);
|
|
560
|
+
if (op.id !== undefined && !isIdentifierText(op.id))
|
|
561
|
+
reject("edit.invalid-id", reasons().ids.notValid(op.id));
|
|
562
|
+
if (op.id !== undefined && ctx.model.relationsByKey.has(op.id)) {
|
|
563
|
+
reject("edit.id-conflict", reasons().c4.relationExists(op.id));
|
|
564
|
+
}
|
|
565
|
+
const label = op.label ? `: ${quoteString(op.label)}` : "";
|
|
566
|
+
const id = op.id ? ` as ${op.id}` : "";
|
|
567
|
+
const block = op.technology ? ` { technology ${quoteString(op.technology)} }` : "";
|
|
568
|
+
const text = `${op.from} -> ${op.to}${label}${id}${block}`;
|
|
569
|
+
const body = modelBody(ctx);
|
|
570
|
+
// After the last relation at the root, else after the last statement.
|
|
571
|
+
const relations = body.statements.filter((s) => s.kind === "RelationStatement");
|
|
572
|
+
const last = relations[relations.length - 1] ?? body.statements[body.statements.length - 1];
|
|
573
|
+
const edits = [
|
|
574
|
+
last
|
|
575
|
+
? insertAfter(ctx, body, last, () => text)
|
|
576
|
+
: insertStatement(ctx, body, ctx.model.node.start, () => text),
|
|
577
|
+
];
|
|
578
|
+
verifyNoNewErrors(ctx, edits);
|
|
579
|
+
return edits;
|
|
580
|
+
}
|
|
581
|
+
export function removeModelRelation(ctx, op) {
|
|
582
|
+
const relation = requireModelRelation(ctx, op.relation);
|
|
583
|
+
const statements = [relation.node];
|
|
584
|
+
// View statements that name the relation by its key.
|
|
585
|
+
for (const diagram of ctx.compilation.document.diagrams) {
|
|
586
|
+
const c4 = diagram.c4;
|
|
587
|
+
if (!c4 || c4.resolved?.file !== null || c4.resolved.definition.id !== ctx.model.id)
|
|
588
|
+
continue;
|
|
589
|
+
// Combinations that stand for this relation only go too, with what names them.
|
|
590
|
+
const keys = new Set([relation.key]);
|
|
591
|
+
for (const drawn of diagram.relations) {
|
|
592
|
+
if (isCombination(diagram, drawn.key) && drawn.sources.every((k) => k === relation.key)) {
|
|
593
|
+
keys.add(drawn.key);
|
|
594
|
+
statements.push(drawn.node);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (const entry of diagram.placement)
|
|
598
|
+
if (keys.has(entry.target))
|
|
599
|
+
statements.push(entry.node);
|
|
600
|
+
for (const label of c4.labels)
|
|
601
|
+
if (keys.has(label.target))
|
|
602
|
+
statements.push(label.node);
|
|
603
|
+
for (const entry of [...diagram.styles, ...diagram.animations]) {
|
|
604
|
+
if (keys.has(entry.target))
|
|
605
|
+
statements.push(entry.node);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const edits = deleteStatements(ctx.text, ctx.compilation.parse.root, statements);
|
|
609
|
+
verifyNoNewErrors(ctx, edits);
|
|
610
|
+
return edits;
|
|
611
|
+
}
|
|
612
|
+
export function setModelRelationProperty(ctx, op) {
|
|
613
|
+
const relation = requireModelRelation(ctx, op.relation);
|
|
614
|
+
const node = relation.node;
|
|
615
|
+
const value = op.value === null || op.value === "" ? null : op.value;
|
|
616
|
+
let edits;
|
|
617
|
+
switch (op.property) {
|
|
618
|
+
case "label": {
|
|
619
|
+
const label = node.label && isToken(node.label) ? node.label : undefined;
|
|
620
|
+
if (value === null) {
|
|
621
|
+
edits =
|
|
622
|
+
label && node.colon ? [{ start: node.colon.start, end: label.end, newText: "" }] : [];
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const quoted = quoteString(value);
|
|
626
|
+
edits = label
|
|
627
|
+
? label.text === quoted
|
|
628
|
+
? []
|
|
629
|
+
: [{ start: label.start, end: label.end, newText: quoted }]
|
|
630
|
+
: [{ start: node.target.end, end: node.target.end, newText: `: ${quoted}` }];
|
|
631
|
+
}
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
case "id": {
|
|
635
|
+
if (value === null)
|
|
636
|
+
reject("edit.invalid-value", reasons().c4.keepsId);
|
|
637
|
+
if (!isIdentifierText(value))
|
|
638
|
+
reject("edit.invalid-id", reasons().ids.notValid(value));
|
|
639
|
+
if (value !== relation.id && ctx.model.relationsByKey.has(value)) {
|
|
640
|
+
reject("edit.id-conflict", reasons().c4.relationExists(value));
|
|
641
|
+
}
|
|
642
|
+
if (relation.id !== null) {
|
|
643
|
+
// The id and the view statements that use it.
|
|
644
|
+
const tokens = [
|
|
645
|
+
{ start: relation.idRange.start.offset, end: relation.idRange.end.offset },
|
|
646
|
+
];
|
|
647
|
+
for (const diagram of ctx.compilation.document.diagrams) {
|
|
648
|
+
const c4 = diagram.c4;
|
|
649
|
+
if (!c4 || c4.resolved?.file !== null || c4.resolved.definition.id !== ctx.model.id)
|
|
650
|
+
continue;
|
|
651
|
+
for (const reference of diagram.references) {
|
|
652
|
+
if (reference.targetKind === "relation" && reference.target === relation.id)
|
|
653
|
+
tokens.push(reference.token);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
edits = [...new Map(tokens.map((t) => [t.start, t])).values()].map((t) => ({
|
|
657
|
+
start: t.start,
|
|
658
|
+
end: t.end,
|
|
659
|
+
newText: value,
|
|
660
|
+
}));
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
const anchor = node.label && isToken(node.label) ? node.label.end : node.target.end;
|
|
664
|
+
edits = [{ start: anchor, end: anchor, newText: ` as ${value}` }];
|
|
665
|
+
}
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case "technology":
|
|
669
|
+
case "description":
|
|
670
|
+
edits = setBlockStatement(ctx, node, node.block, op.property, value === null ? null : `${op.property} ${quoteString(value)}`);
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
verifyNoNewErrors(ctx, edits);
|
|
674
|
+
return edits;
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=c4.js.map
|