@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/flow.js
ADDED
|
@@ -0,0 +1,1693 @@
|
|
|
1
|
+
import { isToken, } from "@meltemi/diagrams-language";
|
|
2
|
+
import { DIRECTIONS, LAYOUT_MODES, ALIGN_EDGES, ANIMATION_EFFECT_PROPERTIES, AXES, ELEMENT_ONLY_PROPERTIES, FLOW_PROFILE, FREE_PROFILE, MAIN_LAYER, PORTS, PORT_SIDES, RELATION_ONLY_PROPERTIES, diagramStatements, profileTakesShape, takesShape, profileTakesRotation, takesRotation, } from "./schema.js";
|
|
3
|
+
import { relationAllowed, } from "./diagram-types.js";
|
|
4
|
+
import { messages } from "./messages/index.js";
|
|
5
|
+
import { StyleDefinitions, checkArg, describeArg, headerHasError, readClasses, readPropertyBlock, readStatementValue, refuseFreeProperties, singleIdent, spanOf, targetHint, } from "./style-values.js";
|
|
6
|
+
/** Element kinds drawn without an outline, which a moving dash cannot follow. */
|
|
7
|
+
const NO_OUTLINE_KINDS = new Set([
|
|
8
|
+
"note",
|
|
9
|
+
"text",
|
|
10
|
+
"icon",
|
|
11
|
+
"image",
|
|
12
|
+
"actor",
|
|
13
|
+
"boundary",
|
|
14
|
+
"control",
|
|
15
|
+
"entity",
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Analyzes one `diagram <id>: flow { }` or `diagram <id>: free { }` block into a `FlowDiagram`,
|
|
19
|
+
* reporting diagnostics. Same-file links are resolved later by `resolveLinks`.
|
|
20
|
+
*/
|
|
21
|
+
export function analyzeFlowDiagram(node, id, lines, bag, type = "flow", declared = null) {
|
|
22
|
+
const analyzer = new FlowAnalyzer(node, id, lines, bag, declared ? declaredProfile(declared) : type === "free" ? FREE_PROFILE : FLOW_PROFILE, declared);
|
|
23
|
+
return { diagram: analyzer.run(), errors: analyzer.errors };
|
|
24
|
+
}
|
|
25
|
+
/** The value spec of a declared property. */
|
|
26
|
+
function specOf(property) {
|
|
27
|
+
switch (property.kind) {
|
|
28
|
+
case "string":
|
|
29
|
+
return { kind: "string" };
|
|
30
|
+
case "number":
|
|
31
|
+
return { kind: "number", min: -1e12, max: 1e12 };
|
|
32
|
+
case "boolean":
|
|
33
|
+
return { kind: "enum", values: ["true", "false"] };
|
|
34
|
+
case "enum":
|
|
35
|
+
return { kind: "enum", values: property.values };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** The properties of a declared kind, as a table. */
|
|
39
|
+
function tableOf(properties) {
|
|
40
|
+
return Object.fromEntries(Object.entries(properties).map(([name, p]) => [name, specOf(p)]));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The profile of a declared diagram type: the free profile, whose kinds it adds, with the type's
|
|
44
|
+
* layout defaults and relation properties (decision 0030).
|
|
45
|
+
*/
|
|
46
|
+
export function declaredProfile(definition) {
|
|
47
|
+
const kinds = Object.keys(definition.elements);
|
|
48
|
+
return {
|
|
49
|
+
...FREE_PROFILE,
|
|
50
|
+
diagramProperties: {
|
|
51
|
+
...FREE_PROFILE.diagramProperties,
|
|
52
|
+
direction: { kind: "enum", values: DIRECTIONS, default: definition.layout.direction },
|
|
53
|
+
layout: { kind: "enum", values: LAYOUT_MODES, default: definition.layout.mode },
|
|
54
|
+
},
|
|
55
|
+
elementKinds: [...FREE_PROFILE.elementKinds, ...kinds],
|
|
56
|
+
relationProperties: {
|
|
57
|
+
...FREE_PROFILE.relationProperties,
|
|
58
|
+
...tableOf(definition.relations.properties),
|
|
59
|
+
},
|
|
60
|
+
ruleKinds: [...FREE_PROFILE.ruleKinds, ...kinds],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The declared values of a block: booleans as booleans. */
|
|
64
|
+
function dataOf(values, properties) {
|
|
65
|
+
const data = {};
|
|
66
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
67
|
+
const value = values[name];
|
|
68
|
+
if (value === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
data[name] = property.kind === "boolean" ? value === "true" : value;
|
|
71
|
+
}
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* What is wrong with a path written as a URL, an absolute path or with backslashes, in the
|
|
76
|
+
* interface language; undefined for a usable path.
|
|
77
|
+
*/
|
|
78
|
+
function unsafeImagePath(path) {
|
|
79
|
+
const m = messages().diagnostics.flow;
|
|
80
|
+
if (path.trim() === "")
|
|
81
|
+
return m.imageEmpty;
|
|
82
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(path)) {
|
|
83
|
+
return /^[A-Za-z]:/.test(path) ? m.imageAbsolute(path) : m.imageUrl(path);
|
|
84
|
+
}
|
|
85
|
+
if (path.startsWith("/"))
|
|
86
|
+
return m.imageAbsolute(path);
|
|
87
|
+
if (path.includes("\\"))
|
|
88
|
+
return m.imageBackslash(path);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
/** The analyser of flow and free diagrams; the sequence analyser extends it. */
|
|
92
|
+
export class FlowAnalyzer {
|
|
93
|
+
node;
|
|
94
|
+
id;
|
|
95
|
+
lines;
|
|
96
|
+
bag;
|
|
97
|
+
profile;
|
|
98
|
+
declared;
|
|
99
|
+
errors = 0;
|
|
100
|
+
elements = [];
|
|
101
|
+
elementsById = new Map();
|
|
102
|
+
relations = [];
|
|
103
|
+
relationsByKey = new Map();
|
|
104
|
+
/** Every `as <id>` written in the diagram, collected before relations are resolved. */
|
|
105
|
+
relationIdTokens = new Map();
|
|
106
|
+
/** Ids of relations that could not be built; references to them stay silent. */
|
|
107
|
+
brokenRelationIds = new Set();
|
|
108
|
+
placement = [];
|
|
109
|
+
constraints = [];
|
|
110
|
+
styles = [];
|
|
111
|
+
icons = [];
|
|
112
|
+
images = [];
|
|
113
|
+
links = [];
|
|
114
|
+
/** Layer names used by blocks, checked once `layers` is read. */
|
|
115
|
+
layerUses = [];
|
|
116
|
+
layerRanges = new Map();
|
|
117
|
+
animations = [];
|
|
118
|
+
references = [];
|
|
119
|
+
entityReferences = [];
|
|
120
|
+
placementBlocks = [];
|
|
121
|
+
placementSeen = new Map();
|
|
122
|
+
styleSeen = new Map();
|
|
123
|
+
/** `<target>:<effect>` of every animation, for `animate.conflict`. */
|
|
124
|
+
animationSeen = new Map();
|
|
125
|
+
properties;
|
|
126
|
+
definitions;
|
|
127
|
+
report = (code, message, at, options = {}) => this.error(code, message, at, options);
|
|
128
|
+
constructor(node, id, lines, bag, profile,
|
|
129
|
+
/** The definition of a declared type, or null for the built-in profiles. */
|
|
130
|
+
declared = null) {
|
|
131
|
+
this.node = node;
|
|
132
|
+
this.id = id;
|
|
133
|
+
this.lines = lines;
|
|
134
|
+
this.bag = bag;
|
|
135
|
+
this.profile = profile;
|
|
136
|
+
this.declared = declared;
|
|
137
|
+
this.definitions = new StyleDefinitions(lines, this.report, profile);
|
|
138
|
+
const layout = profile.diagramProperties.layout;
|
|
139
|
+
const direction = profile.diagramProperties.direction;
|
|
140
|
+
this.properties = {
|
|
141
|
+
direction: (direction?.kind === "enum" ? direction.default : "LR"),
|
|
142
|
+
layout: (layout?.kind === "enum" ? layout.default : "auto"),
|
|
143
|
+
theme: null,
|
|
144
|
+
title: null,
|
|
145
|
+
description: null,
|
|
146
|
+
preset: null,
|
|
147
|
+
canvas: null,
|
|
148
|
+
routing: "auto",
|
|
149
|
+
layers: [MAIN_LAYER],
|
|
150
|
+
autonumber: null,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Sequence diagrams: the ordered events, their index and the scenarios. */
|
|
154
|
+
events = [];
|
|
155
|
+
eventsByKey = new Map();
|
|
156
|
+
scenarios = [];
|
|
157
|
+
/** Handles a statement of the event order; true when it was one. */
|
|
158
|
+
classifyEvent(_stmt, _context, _out) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
/** Runs once relations have keys: sequence diagrams build their events here. */
|
|
162
|
+
afterRelations(_classified) { }
|
|
163
|
+
/** Reads a placement statement of the profile; true when it was one. */
|
|
164
|
+
placementStatement(_stmt) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
/** Reads a `scenario` block. */
|
|
168
|
+
scenarioBlock(_stmt) { }
|
|
169
|
+
/** Runs after every block was read. */
|
|
170
|
+
finish() { }
|
|
171
|
+
/**
|
|
172
|
+
* The token that names an element or relation in placement, constraints, styles and
|
|
173
|
+
* animations: one identifier here; C4 views also read qualified entity paths.
|
|
174
|
+
*/
|
|
175
|
+
targetToken(arg, _role) {
|
|
176
|
+
return singleIdent(arg);
|
|
177
|
+
}
|
|
178
|
+
get free() {
|
|
179
|
+
return this.profile.name === "free";
|
|
180
|
+
}
|
|
181
|
+
run() {
|
|
182
|
+
const block = this.node.block.kind === "Block" ? this.node.block : undefined;
|
|
183
|
+
const classified = {
|
|
184
|
+
declarations: [],
|
|
185
|
+
relations: [],
|
|
186
|
+
properties: [],
|
|
187
|
+
blocks: [],
|
|
188
|
+
events: [],
|
|
189
|
+
};
|
|
190
|
+
if (block)
|
|
191
|
+
this.classify(block.statements, [], "diagram", classified);
|
|
192
|
+
for (const { stmt } of classified.relations) {
|
|
193
|
+
if (stmt.id && isToken(stmt.id) && !this.relationIdTokens.has(stmt.id.text)) {
|
|
194
|
+
this.relationIdTokens.set(stmt.id.text, stmt.id);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const decl of classified.declarations)
|
|
198
|
+
this.declare(decl.stmt, decl.groupPath);
|
|
199
|
+
for (const rel of classified.relations)
|
|
200
|
+
this.relation(rel.stmt, rel.groupPath);
|
|
201
|
+
this.assignKeys();
|
|
202
|
+
this.afterRelations(classified);
|
|
203
|
+
for (const stmt of classified.properties)
|
|
204
|
+
this.diagramProperty(stmt);
|
|
205
|
+
for (const stmt of classified.blocks) {
|
|
206
|
+
const keyword = stmt.keyword.text;
|
|
207
|
+
if (keyword === "placement")
|
|
208
|
+
this.placementBlock(stmt);
|
|
209
|
+
else if (keyword === "class")
|
|
210
|
+
this.definitions.addClass(stmt);
|
|
211
|
+
else if (keyword === "rule")
|
|
212
|
+
this.definitions.addRule(stmt);
|
|
213
|
+
else if (keyword === "scenario")
|
|
214
|
+
this.scenarioBlock(stmt);
|
|
215
|
+
else
|
|
216
|
+
this.styleOrAnimate(stmt, keyword);
|
|
217
|
+
}
|
|
218
|
+
this.checkLayers();
|
|
219
|
+
this.finish();
|
|
220
|
+
if (this.declared)
|
|
221
|
+
this.checkDeclared(this.declared);
|
|
222
|
+
this.elements.sort((a, b) => a.idToken.start - b.idToken.start);
|
|
223
|
+
return {
|
|
224
|
+
type: this.profile.name,
|
|
225
|
+
id: this.id.text,
|
|
226
|
+
range: this.range(this.node),
|
|
227
|
+
idRange: this.range(this.id),
|
|
228
|
+
node: this.node,
|
|
229
|
+
block,
|
|
230
|
+
properties: { ...this.properties, layers: [...this.properties.layers] },
|
|
231
|
+
elements: this.elements,
|
|
232
|
+
relations: this.relations,
|
|
233
|
+
placement: this.placement,
|
|
234
|
+
constraints: this.constraints,
|
|
235
|
+
styles: this.styles,
|
|
236
|
+
animations: this.animations,
|
|
237
|
+
classes: this.definitions.classes,
|
|
238
|
+
rules: this.definitions.rules,
|
|
239
|
+
icons: [...this.icons, ...this.definitions.icons].sort((a, b) => a.range.start.offset - b.range.start.offset),
|
|
240
|
+
images: this.images,
|
|
241
|
+
links: this.links,
|
|
242
|
+
layerRanges: this.layerRanges,
|
|
243
|
+
events: this.events,
|
|
244
|
+
eventsByKey: this.eventsByKey,
|
|
245
|
+
scenarios: this.scenarios,
|
|
246
|
+
references: this.references,
|
|
247
|
+
c4: null,
|
|
248
|
+
entityReferences: this.entityReferences,
|
|
249
|
+
placementBlocks: this.placementBlocks,
|
|
250
|
+
elementsById: this.elementsById,
|
|
251
|
+
relationsByKey: this.relationsByKey,
|
|
252
|
+
definition: this.declared,
|
|
253
|
+
hasSyntaxErrors: this.node.hasError,
|
|
254
|
+
hasErrors: this.node.hasError || this.errors > 0,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/** A declared type's rules: required properties, and the kinds relations may join. */
|
|
258
|
+
checkDeclared(definition) {
|
|
259
|
+
for (const element of this.elements) {
|
|
260
|
+
const kind = element.typeKind ? definition.elements[element.typeKind] : undefined;
|
|
261
|
+
for (const name of kind?.required ?? []) {
|
|
262
|
+
if (element.data[name] !== undefined)
|
|
263
|
+
continue;
|
|
264
|
+
const m = messages();
|
|
265
|
+
this.error("type.missing-property", m.diagnostics.flow.missingProperty(kind.label, element.id, name), element.idToken, {
|
|
266
|
+
hints: [m.hints.flow.requiredProperty(definition.title, name, element.typeKind)],
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const relation of this.relations) {
|
|
271
|
+
const from = this.elementsById.get(relation.from)?.typeKind ?? null;
|
|
272
|
+
const to = this.elementsById.get(relation.to)?.typeKind ?? null;
|
|
273
|
+
if (relationAllowed(definition, from, to))
|
|
274
|
+
continue;
|
|
275
|
+
const pairs = (definition.relations.allowed ?? []).map(([a, b]) => `${a} -> ${b}`);
|
|
276
|
+
const m = messages();
|
|
277
|
+
this.error("type.relation-not-allowed", m.diagnostics.flow.relationNotAllowed(definition.title, from ?? "box", to ?? "box"), relation.node, { hints: [m.hints.flow.allowedRelations(pairs)] });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ------------------------------------------------------------------ helpers
|
|
281
|
+
range(element) {
|
|
282
|
+
return this.lines.range(spanOf(element));
|
|
283
|
+
}
|
|
284
|
+
error(code, message, at, options = {}) {
|
|
285
|
+
this.errors++;
|
|
286
|
+
this.bag.add({
|
|
287
|
+
code,
|
|
288
|
+
message,
|
|
289
|
+
span: "kind" in at ? spanOf(at) : at,
|
|
290
|
+
hints: options.hints,
|
|
291
|
+
data: options.data,
|
|
292
|
+
related: options.related?.map((r) => ({
|
|
293
|
+
message: r.message,
|
|
294
|
+
span: "kind" in r.at ? spanOf(r.at) : r.at,
|
|
295
|
+
})),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
invalid(message, at, hints) {
|
|
299
|
+
this.error("prop.invalid-value", message, at, { hints });
|
|
300
|
+
}
|
|
301
|
+
reference(token, target, targetKind, role, statement) {
|
|
302
|
+
this.references.push({ token, target, targetKind, role, statement });
|
|
303
|
+
}
|
|
304
|
+
// ------------------------------------------------------------------ classification
|
|
305
|
+
classify(statements, groupPath, context, out) {
|
|
306
|
+
for (const stmt of statements) {
|
|
307
|
+
if (stmt.kind === "Error" || headerHasError(stmt))
|
|
308
|
+
continue;
|
|
309
|
+
if (this.classifyEvent(stmt, context, out))
|
|
310
|
+
continue;
|
|
311
|
+
if (stmt.kind === "ShorthandDeclaration") {
|
|
312
|
+
out.declarations.push({ stmt, groupPath });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (stmt.kind === "RelationStatement") {
|
|
316
|
+
out.relations.push({ stmt, groupPath });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const keyword = stmt.keyword.text;
|
|
320
|
+
if (this.profile.elementKinds.includes(keyword)) {
|
|
321
|
+
out.declarations.push({ stmt, groupPath });
|
|
322
|
+
if (keyword === "group" && stmt.block) {
|
|
323
|
+
const groupId = singleIdent(stmt.args[0]);
|
|
324
|
+
const path = groupId ? [...groupPath, groupId.text] : groupPath;
|
|
325
|
+
this.classify(stmt.block.statements, path, "group", out);
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const otherKind = this.otherProfileKind(keyword);
|
|
330
|
+
if (context === "group") {
|
|
331
|
+
// `description`, `url`, `class`, `layer` and `link` are read when the group is declared.
|
|
332
|
+
if (keyword === "description" || keyword === "url" || (keyword === "class" && !stmt.block))
|
|
333
|
+
continue;
|
|
334
|
+
if ((keyword === "layer" || keyword === "link") &&
|
|
335
|
+
keyword in this.profile.groupProperties) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const m = messages();
|
|
339
|
+
const hints = otherKind
|
|
340
|
+
? [otherKind]
|
|
341
|
+
: diagramStatements(this.profile).includes(keyword)
|
|
342
|
+
? [m.hints.flow.diagramLevel(keyword)]
|
|
343
|
+
: [m.hints.flow.groupContents];
|
|
344
|
+
const candidates = otherKind
|
|
345
|
+
? this.declarationWords()
|
|
346
|
+
: diagramStatements(this.profile).includes(keyword)
|
|
347
|
+
? []
|
|
348
|
+
: ["description", "url"];
|
|
349
|
+
this.error("prop.unknown", m.diagnostics.flow.unknownInGroup(keyword), stmt.keyword, {
|
|
350
|
+
hints,
|
|
351
|
+
data: { candidates },
|
|
352
|
+
});
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (keyword in this.profile.diagramProperties) {
|
|
356
|
+
out.properties.push(stmt);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (this.profile.diagramBlocks.includes(keyword)) {
|
|
360
|
+
out.blocks.push(stmt);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const hints = this.statementHints(keyword, stmt);
|
|
364
|
+
this.error("prop.unknown", messages().diagnostics.flow.unknownInDiagram(keyword, this.profile.name), stmt.keyword, { hints, data: { candidates: this.statementCandidates(keyword) } });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** Hints for an unknown statement at the diagram level, in the interface language. */
|
|
368
|
+
statementHints(keyword, stmt) {
|
|
369
|
+
const otherKind = this.otherProfileKind(keyword);
|
|
370
|
+
if (otherKind)
|
|
371
|
+
return [otherKind];
|
|
372
|
+
const m = messages().hints.flow;
|
|
373
|
+
const hints = [];
|
|
374
|
+
if (stmt.args.length === 0 && !stmt.block) {
|
|
375
|
+
hints.push(m.declareElement(keyword, this.profile.shorthand.box));
|
|
376
|
+
}
|
|
377
|
+
hints.push(this.free ? m.freeStatements : m.flowStatements);
|
|
378
|
+
return hints;
|
|
379
|
+
}
|
|
380
|
+
/** The statements an unknown one at the diagram level may have meant, for fixes. */
|
|
381
|
+
statementCandidates(keyword) {
|
|
382
|
+
if (this.otherProfileKind(keyword))
|
|
383
|
+
return this.declarationWords();
|
|
384
|
+
return this.free ? FREE_STATEMENTS : FLOW_STATEMENTS;
|
|
385
|
+
}
|
|
386
|
+
/** The declaration keywords of this profile, as its hints list them. */
|
|
387
|
+
declarationWords() {
|
|
388
|
+
return this.free ? FREE_DECLARATIONS : FLOW_DECLARATIONS;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The hint for a declaration keyword of the other profile, in the interface language, or
|
|
392
|
+
* undefined.
|
|
393
|
+
*/
|
|
394
|
+
otherProfileKind(keyword) {
|
|
395
|
+
if (this.profile.name === "sequence")
|
|
396
|
+
return undefined;
|
|
397
|
+
const other = this.free ? FLOW_PROFILE : FREE_PROFILE;
|
|
398
|
+
if (!other.elementKinds.includes(keyword) || this.profile.elementKinds.includes(keyword)) {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
const m = messages().hints.flow;
|
|
402
|
+
return this.free ? m.freeDeclarations : m.flowDeclarations(keyword);
|
|
403
|
+
}
|
|
404
|
+
// ------------------------------------------------------------------ elements
|
|
405
|
+
declare(stmt, groupPath) {
|
|
406
|
+
if (stmt.kind === "ShorthandDeclaration") {
|
|
407
|
+
const label = isToken(stmt.label) ? String(stmt.label.value ?? "") : stmt.id.text;
|
|
408
|
+
const block = this.elementBlock(stmt.block, this.profile.elementProperties, "Element", stmt.id);
|
|
409
|
+
const data = stmt.shape === "data";
|
|
410
|
+
this.addElement({
|
|
411
|
+
id: stmt.id.text,
|
|
412
|
+
kind: (data ? this.profile.shorthand.data : this.profile.shorthand.box),
|
|
413
|
+
label,
|
|
414
|
+
implicit: false,
|
|
415
|
+
...block,
|
|
416
|
+
source: null,
|
|
417
|
+
sourceRange: null,
|
|
418
|
+
labelWritten: isToken(stmt.label),
|
|
419
|
+
entity: null,
|
|
420
|
+
technology: null,
|
|
421
|
+
external: false,
|
|
422
|
+
shape: data ? "cylinder" : null,
|
|
423
|
+
typeKind: null,
|
|
424
|
+
data: {},
|
|
425
|
+
groupPath,
|
|
426
|
+
range: this.range(stmt),
|
|
427
|
+
idRange: this.range(stmt.id),
|
|
428
|
+
idToken: stmt.id,
|
|
429
|
+
node: stmt,
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
// A kind of a declared type is drawn as a box with the kind's style.
|
|
434
|
+
const declaredKind = this.declared?.elements[stmt.keyword.text];
|
|
435
|
+
const typeKind = declaredKind ? stmt.keyword.text : null;
|
|
436
|
+
const kind = (declaredKind ? "box" : stmt.keyword.text);
|
|
437
|
+
const sourced = kind === "text" || kind === "icon" || kind === "image";
|
|
438
|
+
const second = { text: '"Text"', icon: '"<pack>:<name>"', image: '"<path>"' }[kind];
|
|
439
|
+
const form = sourced
|
|
440
|
+
? `\`${kind} <id> ${second}\``
|
|
441
|
+
: `\`${kind} <id> ["Label"]${kind === "group" ? " { ... }" : ""}\``;
|
|
442
|
+
const example = {
|
|
443
|
+
text: 'text heading "Architecture"',
|
|
444
|
+
icon: 'icon server "tabler:server"',
|
|
445
|
+
image: 'image logo "logo.png"',
|
|
446
|
+
}[kind];
|
|
447
|
+
if (stmt.chain) {
|
|
448
|
+
this.invalid(messages().diagnostics.common.singleBlock(kind), stmt.chain.clauses[1].keyword);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const [idArg, labelArg, ...extra] = stmt.args;
|
|
452
|
+
const idToken = singleIdent(idArg);
|
|
453
|
+
if (!idToken) {
|
|
454
|
+
const m = messages();
|
|
455
|
+
this.invalid(m.diagnostics.common.expected(form), idArg ?? stmt.keyword, [
|
|
456
|
+
m.hints.example(example ?? `${kind} api "API Gateway"`),
|
|
457
|
+
]);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
let label = idToken.text;
|
|
461
|
+
let source = null;
|
|
462
|
+
let sourceRange = null;
|
|
463
|
+
if (labelArg) {
|
|
464
|
+
if (labelArg.kind !== "StringLiteral") {
|
|
465
|
+
const what = sourced && kind !== "text" ? kind : "label";
|
|
466
|
+
this.invalid(messages().diagnostics.flow.quotedSource(what, idToken.text), labelArg);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (kind === "text" || !sourced)
|
|
470
|
+
label = labelArg.value;
|
|
471
|
+
// A bad source is reported, and the object still declared so that its uses resolve.
|
|
472
|
+
let usable = true;
|
|
473
|
+
if (kind === "icon") {
|
|
474
|
+
const checked = checkArg(labelArg, { kind: "icon" }, "icon");
|
|
475
|
+
if (!checked.ok) {
|
|
476
|
+
this.invalid(checked.message, labelArg, checked.hints);
|
|
477
|
+
usable = false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (kind === "image") {
|
|
481
|
+
const problem = unsafeImagePath(labelArg.value);
|
|
482
|
+
if (problem) {
|
|
483
|
+
this.error("asset.outside-project", problem, labelArg, {
|
|
484
|
+
hints: [messages().hints.flow.imagePath],
|
|
485
|
+
});
|
|
486
|
+
usable = false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (usable && (kind === "icon" || kind === "image")) {
|
|
490
|
+
source = labelArg.value;
|
|
491
|
+
sourceRange = this.range(labelArg);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (sourced) {
|
|
495
|
+
const m = messages();
|
|
496
|
+
this.invalid(m.diagnostics.flow.needsSource(kind, idToken.text, form), stmt, [
|
|
497
|
+
m.hints.example(example),
|
|
498
|
+
]);
|
|
499
|
+
}
|
|
500
|
+
if (extra.length > 0) {
|
|
501
|
+
this.invalid(messages().diagnostics.flow.valueAfter(sourced ? kind : "label", form), extra[0]);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const block = kind === "group"
|
|
505
|
+
? { ...this.groupBlock(stmt.block, idToken), ports: [], values: {} }
|
|
506
|
+
: this.elementBlock(stmt.block, declaredKind
|
|
507
|
+
? { ...this.profile.elementProperties, ...tableOf(declaredKind.properties) }
|
|
508
|
+
: this.profile.elementProperties, "Element", idToken);
|
|
509
|
+
const { values: blockValues, ...blockFields } = block;
|
|
510
|
+
const added = this.addElement({
|
|
511
|
+
id: idToken.text,
|
|
512
|
+
kind,
|
|
513
|
+
label,
|
|
514
|
+
implicit: false,
|
|
515
|
+
...blockFields,
|
|
516
|
+
source,
|
|
517
|
+
sourceRange,
|
|
518
|
+
labelWritten: labelArg !== undefined && (kind === "text" || !sourced),
|
|
519
|
+
entity: null,
|
|
520
|
+
technology: null,
|
|
521
|
+
external: false,
|
|
522
|
+
shape: null,
|
|
523
|
+
typeKind,
|
|
524
|
+
data: declaredKind ? dataOf(blockValues, declaredKind.properties) : {},
|
|
525
|
+
groupPath,
|
|
526
|
+
range: this.range(stmt),
|
|
527
|
+
idRange: this.range(idToken),
|
|
528
|
+
idToken,
|
|
529
|
+
node: stmt,
|
|
530
|
+
});
|
|
531
|
+
if (!added || source === null || !sourceRange)
|
|
532
|
+
return;
|
|
533
|
+
if (kind === "icon")
|
|
534
|
+
this.icons.push({ reference: source, range: sourceRange });
|
|
535
|
+
else
|
|
536
|
+
this.images.push({ element: idToken.text, path: source, range: sourceRange });
|
|
537
|
+
}
|
|
538
|
+
addElement(element) {
|
|
539
|
+
const existing = this.elementsById.get(element.id);
|
|
540
|
+
if (existing) {
|
|
541
|
+
const m = messages();
|
|
542
|
+
this.error("ref.duplicate-id", m.diagnostics.flow.duplicateElement(element.id), element.idToken, {
|
|
543
|
+
related: [{ message: m.related.firstDeclared, at: existing.idToken }],
|
|
544
|
+
hints: [m.hints.flow.sharedNamespace],
|
|
545
|
+
});
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
this.elements.push(element);
|
|
549
|
+
this.elementsById.set(element.id, element);
|
|
550
|
+
if (element.node) {
|
|
551
|
+
this.reference(element.idToken, element.id, "element", "declaration", element.node);
|
|
552
|
+
}
|
|
553
|
+
if (element.link)
|
|
554
|
+
this.links.push(element.link);
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Reads an element or relation block: its description, applied classes, named ports, and the
|
|
559
|
+
* `layer` and `link` statements the profile accepts.
|
|
560
|
+
*/
|
|
561
|
+
elementBlock(block, table, owner, ownerToken) {
|
|
562
|
+
const empty = { layer: null, layerRange: null, link: null };
|
|
563
|
+
if (!block)
|
|
564
|
+
return { description: null, url: null, classes: [], ports: [], values: {}, ...empty };
|
|
565
|
+
const ports = [];
|
|
566
|
+
const extras = { ...empty };
|
|
567
|
+
const seen = new Map();
|
|
568
|
+
const rest = block.statements.filter((stmt) => {
|
|
569
|
+
if (stmt.kind !== "KeywordStatement")
|
|
570
|
+
return true;
|
|
571
|
+
const keyword = stmt.keyword.text;
|
|
572
|
+
if (owner === "Element" && keyword === "port" && this.profile.ports.length > 0) {
|
|
573
|
+
if (!headerHasError(stmt))
|
|
574
|
+
this.port(stmt, ports);
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
if ((keyword === "layer" || keyword === "link") && keyword in table) {
|
|
578
|
+
if (!headerHasError(stmt))
|
|
579
|
+
this.extra(stmt, extras, seen, ownerToken);
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
return true;
|
|
583
|
+
});
|
|
584
|
+
const words = Object.keys(table).concat(owner === "Element" && this.profile.ports.length > 0 ? ["port"] : []);
|
|
585
|
+
const { values, classes } = this.readProperties({ ...block, statements: rest }, table, () => {
|
|
586
|
+
const m = messages().hints.flow;
|
|
587
|
+
return owner === "Element" ? m.elementBlock(words) : m.relationBlock(words);
|
|
588
|
+
}, owner.toLowerCase(), words);
|
|
589
|
+
return {
|
|
590
|
+
description: typeof values.description === "string" ? values.description : null,
|
|
591
|
+
url: typeof values.url === "string" ? values.url : null,
|
|
592
|
+
classes,
|
|
593
|
+
ports,
|
|
594
|
+
values,
|
|
595
|
+
...extras,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/** Reads `layer <name>` or `link <diagram>.<element>` into the extras of a block. */
|
|
599
|
+
extra(stmt, extras, seen, ownerToken) {
|
|
600
|
+
const keyword = stmt.keyword.text;
|
|
601
|
+
const first = seen.get(keyword);
|
|
602
|
+
if (first) {
|
|
603
|
+
const m = messages();
|
|
604
|
+
this.error("prop.duplicate", m.diagnostics.common.setMoreThanOnce(keyword), stmt.keyword, {
|
|
605
|
+
related: [{ message: m.related.firstSet, at: first }],
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
seen.set(keyword, stmt.keyword);
|
|
610
|
+
const value = this.readValue(stmt, { kind: keyword === "layer" ? "name" : "link" }, keyword);
|
|
611
|
+
if (value === undefined)
|
|
612
|
+
return;
|
|
613
|
+
const arg = stmt.args[0];
|
|
614
|
+
if (keyword === "layer") {
|
|
615
|
+
const token = singleIdent(arg);
|
|
616
|
+
extras.layer = token.text;
|
|
617
|
+
extras.layerRange = this.range(token);
|
|
618
|
+
this.layerUses.push({ name: token.text, token });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (arg.kind !== "QualifiedRef" || !ownerToken)
|
|
622
|
+
return;
|
|
623
|
+
// Read provisionally as `<block>.<rest>`; compile decides what the first part names.
|
|
624
|
+
const parts = arg.parts;
|
|
625
|
+
extras.link = {
|
|
626
|
+
owner: ownerToken.text,
|
|
627
|
+
text: arg.text,
|
|
628
|
+
binding: null,
|
|
629
|
+
diagram: parts[0].text,
|
|
630
|
+
element: parts
|
|
631
|
+
.slice(1)
|
|
632
|
+
.map((p) => p.text)
|
|
633
|
+
.join("."),
|
|
634
|
+
target: null,
|
|
635
|
+
range: this.range(arg),
|
|
636
|
+
diagramRange: this.range(parts[0]),
|
|
637
|
+
elementRange: this.lines.range({ start: parts[1].start, end: arg.end }),
|
|
638
|
+
node: stmt,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
/** Reads `port <name> <side> [<n>%]`. */
|
|
642
|
+
port(stmt, ports) {
|
|
643
|
+
const form = "`port <name> north|south|east|west [<n>%]`";
|
|
644
|
+
if (stmt.block || stmt.chain) {
|
|
645
|
+
this.invalid(messages().diagnostics.common.takesNoBlockExpected("port", form), stmt.block ?? stmt.chain);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const [nameArg, sideArg, positionArg, extra] = stmt.args;
|
|
649
|
+
const name = singleIdent(nameArg);
|
|
650
|
+
if (!name)
|
|
651
|
+
return this.invalid(messages().diagnostics.common.expected(form), nameArg ?? stmt.keyword);
|
|
652
|
+
if (PORTS.includes(name.text)) {
|
|
653
|
+
const m = messages();
|
|
654
|
+
return this.invalid(m.diagnostics.flow.builtInPort(name.text), name, [
|
|
655
|
+
m.hints.flow.compassPorts,
|
|
656
|
+
]);
|
|
657
|
+
}
|
|
658
|
+
const side = singleIdent(sideArg);
|
|
659
|
+
if (!side || !PORT_SIDES.includes(side.text)) {
|
|
660
|
+
return this.invalid(messages().diagnostics.flow.portSide(name.text, PORT_SIDES), sideArg ?? name);
|
|
661
|
+
}
|
|
662
|
+
let position = 0.5;
|
|
663
|
+
if (positionArg) {
|
|
664
|
+
if (positionArg.kind !== "NumberLiteral" ||
|
|
665
|
+
positionArg.unit !== "%" ||
|
|
666
|
+
positionArg.value < 0 ||
|
|
667
|
+
positionArg.value > 100) {
|
|
668
|
+
return this.invalid(messages().diagnostics.flow.portPosition, positionArg);
|
|
669
|
+
}
|
|
670
|
+
position = positionArg.value / 100;
|
|
671
|
+
}
|
|
672
|
+
if (extra)
|
|
673
|
+
return this.invalid(messages().diagnostics.flow.unexpectedValue(form), extra);
|
|
674
|
+
const first = ports.find((p) => p.name === name.text);
|
|
675
|
+
if (first) {
|
|
676
|
+
const m = messages();
|
|
677
|
+
this.error("ref.duplicate-id", m.diagnostics.flow.duplicatePort(name.text), name, {
|
|
678
|
+
related: [
|
|
679
|
+
{
|
|
680
|
+
message: m.related.firstDeclared,
|
|
681
|
+
at: { start: first.nameRange.start.offset, end: first.nameRange.end.offset },
|
|
682
|
+
},
|
|
683
|
+
],
|
|
684
|
+
});
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
ports.push({
|
|
688
|
+
name: name.text,
|
|
689
|
+
side: side.text,
|
|
690
|
+
position,
|
|
691
|
+
range: this.range(stmt),
|
|
692
|
+
nameRange: this.range(name),
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Reads the `description`, `class`, `layer` and `link` properties of a group block; children are
|
|
697
|
+
* classified.
|
|
698
|
+
*/
|
|
699
|
+
groupBlock(block, ownerToken) {
|
|
700
|
+
let description = null;
|
|
701
|
+
let url = null;
|
|
702
|
+
let classes = [];
|
|
703
|
+
const extras = { layer: null, layerRange: null, link: null };
|
|
704
|
+
if (!block)
|
|
705
|
+
return { description, url, classes, ...extras };
|
|
706
|
+
const seen = new Map();
|
|
707
|
+
for (const stmt of block.statements) {
|
|
708
|
+
if (stmt.kind !== "KeywordStatement" || headerHasError(stmt))
|
|
709
|
+
continue;
|
|
710
|
+
const name = stmt.keyword.text;
|
|
711
|
+
if ((name === "layer" || name === "link") && name in this.profile.groupProperties) {
|
|
712
|
+
this.extra(stmt, extras, seen, ownerToken);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (name !== "description" && name !== "url" && !(name === "class" && !stmt.block))
|
|
716
|
+
continue;
|
|
717
|
+
const first = seen.get(name);
|
|
718
|
+
if (first) {
|
|
719
|
+
const m = messages();
|
|
720
|
+
this.error("prop.duplicate", m.diagnostics.flow.setTwiceInGroup(name), stmt.keyword, {
|
|
721
|
+
related: [{ message: m.related.firstSet, at: first }],
|
|
722
|
+
});
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
seen.set(name, stmt.keyword);
|
|
726
|
+
if (name === "class") {
|
|
727
|
+
classes = readClasses(stmt, this.lines, this.report) ?? [];
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (name === "url") {
|
|
731
|
+
const value = this.readValue(stmt, { kind: "url" }, "url");
|
|
732
|
+
if (typeof value === "string")
|
|
733
|
+
url = value;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
const value = this.readValue(stmt, this.profile.groupProperties.description, "description");
|
|
737
|
+
if (typeof value === "string")
|
|
738
|
+
description = value;
|
|
739
|
+
}
|
|
740
|
+
return { description, url, classes, ...extras };
|
|
741
|
+
}
|
|
742
|
+
// ------------------------------------------------------------------ relations
|
|
743
|
+
relation(stmt, groupPath) {
|
|
744
|
+
const target = stmt.target;
|
|
745
|
+
const idToken = stmt.id && isToken(stmt.id) ? stmt.id : undefined;
|
|
746
|
+
const from = this.resolveEndpoint(stmt.source, stmt, groupPath);
|
|
747
|
+
const to = this.resolveEndpoint(target, stmt, groupPath);
|
|
748
|
+
if (from === undefined || to === undefined) {
|
|
749
|
+
if (idToken)
|
|
750
|
+
this.brokenRelationIds.add(idToken.text);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
let id = null;
|
|
754
|
+
if (idToken) {
|
|
755
|
+
const element = this.elementsById.get(idToken.text);
|
|
756
|
+
const firstRelation = this.relationIdTokens.get(idToken.text);
|
|
757
|
+
if (element) {
|
|
758
|
+
const m = messages();
|
|
759
|
+
this.error("ref.duplicate-id", m.diagnostics.flow.relationIdUsedByElement(idToken.text), idToken, {
|
|
760
|
+
related: [{ message: m.related.elementDeclared, at: element.idToken }],
|
|
761
|
+
hints: [m.hints.flow.sharedNamespace],
|
|
762
|
+
});
|
|
763
|
+
this.brokenRelationIds.add(idToken.text);
|
|
764
|
+
}
|
|
765
|
+
else if (firstRelation && firstRelation !== idToken) {
|
|
766
|
+
const m = messages();
|
|
767
|
+
this.error("ref.duplicate-id", m.diagnostics.flow.relationIdUsed(idToken.text), idToken, {
|
|
768
|
+
related: [{ message: m.related.firstUsed, at: firstRelation }],
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
id = idToken.text;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (this.profile.arrows[stmt.arrow.text] === undefined) {
|
|
776
|
+
this.invalid(messages().diagnostics.flow.arrowUnavailable(stmt.arrow.text, Object.keys(this.profile.arrows)), stmt.arrow);
|
|
777
|
+
}
|
|
778
|
+
const { description, classes, layer, layerRange, values } = this.elementBlock(stmt.block, this.profile.relationProperties, "Relation", undefined);
|
|
779
|
+
const relation = {
|
|
780
|
+
key: "",
|
|
781
|
+
id,
|
|
782
|
+
from,
|
|
783
|
+
to,
|
|
784
|
+
line: this.profile.arrows[stmt.arrow.text] ?? "solid",
|
|
785
|
+
twoWay: stmt.arrow.text.startsWith("<"),
|
|
786
|
+
async: values.kind === "async",
|
|
787
|
+
technology: null,
|
|
788
|
+
sources: [],
|
|
789
|
+
derived: false,
|
|
790
|
+
label: stmt.label && isToken(stmt.label) ? String(stmt.label.value ?? "") : null,
|
|
791
|
+
description,
|
|
792
|
+
assumption: typeof values.assumption === "string" ? values.assumption : null,
|
|
793
|
+
data: this.declared ? dataOf(values, this.declared.relations.properties) : {},
|
|
794
|
+
classes,
|
|
795
|
+
layer,
|
|
796
|
+
layerRange,
|
|
797
|
+
groupPath,
|
|
798
|
+
range: this.range(stmt),
|
|
799
|
+
idRange: id !== null && idToken ? this.range(idToken) : null,
|
|
800
|
+
node: stmt,
|
|
801
|
+
};
|
|
802
|
+
this.relations.push(relation);
|
|
803
|
+
if (id !== null && idToken)
|
|
804
|
+
this.reference(idToken, id, "relation", "relation-id", stmt);
|
|
805
|
+
}
|
|
806
|
+
resolveEndpoint(ref, stmt, groupPath) {
|
|
807
|
+
if (ref.parts.length > 1) {
|
|
808
|
+
const m = messages();
|
|
809
|
+
this.error("ref.unknown-element", m.diagnostics.common.unknownElement(ref.text), ref, {
|
|
810
|
+
hints: [m.hints.flow.qualifiedEndpoint],
|
|
811
|
+
data: { ids: [ref.text] },
|
|
812
|
+
});
|
|
813
|
+
return undefined;
|
|
814
|
+
}
|
|
815
|
+
const token = ref.parts[0];
|
|
816
|
+
const name = token.text;
|
|
817
|
+
if (this.elementsById.has(name)) {
|
|
818
|
+
this.reference(token, name, "element", "relation-endpoint", stmt);
|
|
819
|
+
return name;
|
|
820
|
+
}
|
|
821
|
+
if (this.relationIdTokens.has(name)) {
|
|
822
|
+
this.error("ref.unknown-element", messages().diagnostics.flow.relationAsEndpoint(name), token, { data: { ids: [name] } });
|
|
823
|
+
return undefined;
|
|
824
|
+
}
|
|
825
|
+
this.addElement({
|
|
826
|
+
id: name,
|
|
827
|
+
kind: this.profile.implicitKind,
|
|
828
|
+
label: name,
|
|
829
|
+
implicit: true,
|
|
830
|
+
description: null,
|
|
831
|
+
url: null,
|
|
832
|
+
classes: [],
|
|
833
|
+
ports: [],
|
|
834
|
+
layer: null,
|
|
835
|
+
layerRange: null,
|
|
836
|
+
link: null,
|
|
837
|
+
source: null,
|
|
838
|
+
sourceRange: null,
|
|
839
|
+
labelWritten: false,
|
|
840
|
+
entity: null,
|
|
841
|
+
technology: null,
|
|
842
|
+
external: false,
|
|
843
|
+
shape: null,
|
|
844
|
+
typeKind: null,
|
|
845
|
+
data: {},
|
|
846
|
+
groupPath,
|
|
847
|
+
range: this.range(token),
|
|
848
|
+
idRange: this.range(token),
|
|
849
|
+
idToken: token,
|
|
850
|
+
node: undefined,
|
|
851
|
+
});
|
|
852
|
+
this.reference(token, name, "element", "relation-endpoint", stmt);
|
|
853
|
+
return name;
|
|
854
|
+
}
|
|
855
|
+
assignKeys() {
|
|
856
|
+
const counts = new Map();
|
|
857
|
+
for (const relation of this.relations) {
|
|
858
|
+
const pair = `${relation.from}->${relation.to}`;
|
|
859
|
+
const n = (counts.get(pair) ?? 0) + 1;
|
|
860
|
+
counts.set(pair, n);
|
|
861
|
+
relation.key = relation.id ?? `${pair}#${n}`;
|
|
862
|
+
if (!this.relationsByKey.has(relation.key))
|
|
863
|
+
this.relationsByKey.set(relation.key, relation);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// ------------------------------------------------------------------ diagram properties
|
|
867
|
+
propertySeen = new Map();
|
|
868
|
+
diagramProperty(stmt) {
|
|
869
|
+
const name = stmt.keyword.text;
|
|
870
|
+
const first = this.propertySeen.get(name);
|
|
871
|
+
if (first) {
|
|
872
|
+
const m = messages();
|
|
873
|
+
this.error("prop.duplicate", m.diagnostics.flow.setMoreThanOnceInDiagram(name), stmt.keyword, {
|
|
874
|
+
related: [{ message: m.related.firstSet, at: first }],
|
|
875
|
+
});
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
this.propertySeen.set(name, stmt.keyword);
|
|
879
|
+
if (name === "layers") {
|
|
880
|
+
this.layersProperty(stmt);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (name === "autonumber" && stmt.args.length === 0 && !stmt.block && !stmt.chain) {
|
|
884
|
+
this.properties.autonumber = 1;
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
const value = this.readValue(stmt, this.profile.diagramProperties[name], name);
|
|
888
|
+
if (value === undefined)
|
|
889
|
+
return;
|
|
890
|
+
switch (name) {
|
|
891
|
+
case "direction":
|
|
892
|
+
this.properties.direction = value;
|
|
893
|
+
break;
|
|
894
|
+
case "layout":
|
|
895
|
+
this.properties.layout = value;
|
|
896
|
+
break;
|
|
897
|
+
case "theme":
|
|
898
|
+
case "title":
|
|
899
|
+
case "description":
|
|
900
|
+
this.properties[name] = String(value);
|
|
901
|
+
break;
|
|
902
|
+
case "preset":
|
|
903
|
+
case "canvas": {
|
|
904
|
+
const other = name === "preset" ? "canvas" : "preset";
|
|
905
|
+
const set = name === "preset" ? this.properties.canvas : this.properties.preset;
|
|
906
|
+
if (set !== null) {
|
|
907
|
+
const m = messages();
|
|
908
|
+
this.invalid(m.diagnostics.flow.canvasConflict(name, other), stmt, [
|
|
909
|
+
m.hints.flow.oneCanvas,
|
|
910
|
+
]);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (name === "preset")
|
|
914
|
+
this.properties.preset = value;
|
|
915
|
+
else
|
|
916
|
+
this.properties.canvas = value;
|
|
917
|
+
break;
|
|
918
|
+
}
|
|
919
|
+
case "routing":
|
|
920
|
+
this.properties.routing = value;
|
|
921
|
+
break;
|
|
922
|
+
case "autonumber":
|
|
923
|
+
this.properties.autonumber = Number(value);
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
/** Reads `layers a, b, c`: back to front, `main` last unless listed. */
|
|
928
|
+
layersProperty(stmt) {
|
|
929
|
+
if (stmt.block || stmt.chain) {
|
|
930
|
+
this.invalid(messages().diagnostics.common.doesNotTakeBlock("layers"), stmt.block ?? stmt.chain);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (stmt.args.length === 0) {
|
|
934
|
+
const m = messages();
|
|
935
|
+
this.invalid(m.diagnostics.flow.layersEmpty, stmt.keyword, [
|
|
936
|
+
m.hints.example("layers background, main, front"),
|
|
937
|
+
]);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const names = [];
|
|
941
|
+
for (const arg of stmt.args) {
|
|
942
|
+
const token = singleIdent(arg);
|
|
943
|
+
if (!token) {
|
|
944
|
+
this.invalid(messages().diagnostics.flow.invalidLayer(describeArg(arg)), arg);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
if (names.includes(token.text)) {
|
|
948
|
+
this.error("prop.duplicate", messages().diagnostics.flow.layerTwice(token.text), token);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
names.push(token.text);
|
|
952
|
+
this.layerRanges.set(token.text, this.range(token));
|
|
953
|
+
}
|
|
954
|
+
if (!names.includes(MAIN_LAYER))
|
|
955
|
+
names.push(MAIN_LAYER);
|
|
956
|
+
this.properties.layers = names;
|
|
957
|
+
}
|
|
958
|
+
/** Reports `layer` statements that name an undeclared layer. */
|
|
959
|
+
checkLayers() {
|
|
960
|
+
const layers = this.properties.layers;
|
|
961
|
+
for (const use of this.layerUses) {
|
|
962
|
+
if (layers.includes(use.name))
|
|
963
|
+
continue;
|
|
964
|
+
const m = messages();
|
|
965
|
+
this.error("ref.unknown-layer", m.diagnostics.flow.unknownLayer(use.name), use.token, {
|
|
966
|
+
data: { ids: [use.name] },
|
|
967
|
+
hints: [
|
|
968
|
+
m.hints.flow.layers(layers),
|
|
969
|
+
m.hints.flow.declareLayer([
|
|
970
|
+
...layers.filter((l) => l !== MAIN_LAYER),
|
|
971
|
+
use.name,
|
|
972
|
+
MAIN_LAYER,
|
|
973
|
+
]),
|
|
974
|
+
],
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/** Reads the single value of a property statement, reporting `prop.invalid-value` when it does not fit. */
|
|
979
|
+
readValue(stmt, spec, name) {
|
|
980
|
+
return readStatementValue(stmt, spec, name, this.report);
|
|
981
|
+
}
|
|
982
|
+
/** Reads `name value` statements from a block against a property table. */
|
|
983
|
+
readProperties(block, table,
|
|
984
|
+
/** The hint for an unknown statement, built when one is reported. */
|
|
985
|
+
unknownHint, owner, accepted) {
|
|
986
|
+
return readPropertyBlock(block, table, unknownHint, owner, this.lines, this.report, accepted);
|
|
987
|
+
}
|
|
988
|
+
// ------------------------------------------------------------------ placement
|
|
989
|
+
placementBlock(stmt) {
|
|
990
|
+
if (stmt.args.length > 0) {
|
|
991
|
+
this.invalid(messages().diagnostics.flow.placementArguments, stmt.args[0]);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (stmt.chain || !stmt.block) {
|
|
995
|
+
this.invalid(messages().diagnostics.flow.placementBlock, stmt.chain ?? stmt.keyword);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
this.placementBlocks.push(stmt);
|
|
999
|
+
const hint = () => messages().hints.flow.placementAccepts(this.profile.placementStatements);
|
|
1000
|
+
for (const inner of stmt.block.statements) {
|
|
1001
|
+
if (inner.kind === "Error" || headerHasError(inner))
|
|
1002
|
+
continue;
|
|
1003
|
+
if (inner.kind !== "KeywordStatement") {
|
|
1004
|
+
const m = messages().diagnostics.flow;
|
|
1005
|
+
this.error("prop.unknown", inner.kind === "RelationStatement" ? m.relationsInPlacement : m.declarationsInPlacement, inner, {
|
|
1006
|
+
hints: [hint()],
|
|
1007
|
+
data: { candidates: this.profile.placementStatements },
|
|
1008
|
+
});
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (this.placementStatement(inner))
|
|
1012
|
+
continue;
|
|
1013
|
+
switch (inner.keyword.text) {
|
|
1014
|
+
case "place":
|
|
1015
|
+
this.place(inner);
|
|
1016
|
+
break;
|
|
1017
|
+
case "size":
|
|
1018
|
+
this.size(inner);
|
|
1019
|
+
break;
|
|
1020
|
+
case "route":
|
|
1021
|
+
this.route(inner);
|
|
1022
|
+
break;
|
|
1023
|
+
case "label":
|
|
1024
|
+
this.label(inner);
|
|
1025
|
+
break;
|
|
1026
|
+
case "align":
|
|
1027
|
+
case "row":
|
|
1028
|
+
case "column":
|
|
1029
|
+
case "order":
|
|
1030
|
+
case "distance":
|
|
1031
|
+
this.constraint(inner, inner.keyword.text);
|
|
1032
|
+
break;
|
|
1033
|
+
default:
|
|
1034
|
+
this.error("prop.unknown", messages().diagnostics.flow.unknownPlacement(inner.keyword.text), inner.keyword, {
|
|
1035
|
+
hints: [hint()],
|
|
1036
|
+
data: { candidates: this.profile.placementStatements },
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
shapeError(stmt, form, at) {
|
|
1042
|
+
this.invalid(messages().diagnostics.common.expected(form), at ?? stmt);
|
|
1043
|
+
}
|
|
1044
|
+
noBlock(stmt, form) {
|
|
1045
|
+
if (stmt.block || stmt.chain) {
|
|
1046
|
+
this.invalid(messages().diagnostics.flow.noBlock(stmt.keyword.text, form), stmt.block ?? stmt.chain);
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
duplicatePlacement(stmt, target) {
|
|
1052
|
+
const key = `${stmt.keyword.text}:${target}`;
|
|
1053
|
+
const first = this.placementSeen.get(key);
|
|
1054
|
+
if (first) {
|
|
1055
|
+
const m = messages();
|
|
1056
|
+
this.error("placement.duplicate", m.diagnostics.flow.duplicatePlacement(target, stmt.keyword.text), stmt, { related: [{ message: m.related.firstOne, at: first }] });
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
this.placementSeen.set(key, stmt);
|
|
1060
|
+
return false;
|
|
1061
|
+
}
|
|
1062
|
+
place(stmt) {
|
|
1063
|
+
const form = "`place <element> at (x, y)`";
|
|
1064
|
+
if (!this.noBlock(stmt, form))
|
|
1065
|
+
return;
|
|
1066
|
+
const [target, at, position, extra] = stmt.args;
|
|
1067
|
+
const targetToken = this.targetToken(target, "placement");
|
|
1068
|
+
if (!targetToken)
|
|
1069
|
+
return this.shapeError(stmt, form, target);
|
|
1070
|
+
if (!isWord(at, "at"))
|
|
1071
|
+
return this.shapeError(stmt, form, at);
|
|
1072
|
+
const point = readPoint(position);
|
|
1073
|
+
if (!point)
|
|
1074
|
+
return this.invalid(messages().diagnostics.flow.position, position ?? stmt);
|
|
1075
|
+
if (extra)
|
|
1076
|
+
return this.shapeError(stmt, form, extra);
|
|
1077
|
+
const element = this.resolveElement(targetToken, "place", stmt);
|
|
1078
|
+
if (!element || this.duplicatePlacement(stmt, element))
|
|
1079
|
+
return;
|
|
1080
|
+
this.placement.push({
|
|
1081
|
+
statement: "place",
|
|
1082
|
+
target: element,
|
|
1083
|
+
x: point.x,
|
|
1084
|
+
y: point.y,
|
|
1085
|
+
range: this.range(stmt),
|
|
1086
|
+
targetRange: this.range(targetToken),
|
|
1087
|
+
node: stmt,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
size(stmt) {
|
|
1091
|
+
const form = "`size <element> (width, height)`";
|
|
1092
|
+
if (!this.noBlock(stmt, form))
|
|
1093
|
+
return;
|
|
1094
|
+
const [target, dimensions, extra] = stmt.args;
|
|
1095
|
+
const targetToken = this.targetToken(target, "placement");
|
|
1096
|
+
if (!targetToken)
|
|
1097
|
+
return this.shapeError(stmt, form, target);
|
|
1098
|
+
const point = readPoint(dimensions);
|
|
1099
|
+
if (!point || point.x <= 0 || point.y <= 0) {
|
|
1100
|
+
return this.invalid(messages().diagnostics.flow.size, dimensions ?? stmt);
|
|
1101
|
+
}
|
|
1102
|
+
if (extra)
|
|
1103
|
+
return this.shapeError(stmt, form, extra);
|
|
1104
|
+
const element = this.resolveElement(targetToken, "size", stmt);
|
|
1105
|
+
if (!element || this.duplicatePlacement(stmt, element))
|
|
1106
|
+
return;
|
|
1107
|
+
this.placement.push({
|
|
1108
|
+
statement: "size",
|
|
1109
|
+
target: element,
|
|
1110
|
+
width: point.x,
|
|
1111
|
+
height: point.y,
|
|
1112
|
+
range: this.range(stmt),
|
|
1113
|
+
targetRange: this.range(targetToken),
|
|
1114
|
+
node: stmt,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
route(stmt) {
|
|
1118
|
+
const form = "`route <relation> from <element>.<port> [<n>%] to <element>.<port> [<n>%] via [(x, y), ...]`";
|
|
1119
|
+
if (!this.noBlock(stmt, form))
|
|
1120
|
+
return;
|
|
1121
|
+
// A percentage may follow each end: the point of its compass side.
|
|
1122
|
+
const args = [...stmt.args];
|
|
1123
|
+
const percentAt = (index) => args[index]?.kind === "NumberLiteral" ? args.splice(index, 1)[0] : undefined;
|
|
1124
|
+
const fromAt = percentAt(3);
|
|
1125
|
+
const toAt = percentAt(5);
|
|
1126
|
+
const [target, fromWord, fromRef, toWord, toRef, viaWord, viaList, extra] = args;
|
|
1127
|
+
const targetToken = this.targetToken(target, "placement");
|
|
1128
|
+
if (!targetToken)
|
|
1129
|
+
return this.shapeError(stmt, form, target);
|
|
1130
|
+
if (!isWord(fromWord, "from"))
|
|
1131
|
+
return this.shapeError(stmt, form, fromWord);
|
|
1132
|
+
if (fromRef?.kind !== "QualifiedRef")
|
|
1133
|
+
return this.shapeError(stmt, form, fromRef);
|
|
1134
|
+
if (!isWord(toWord, "to"))
|
|
1135
|
+
return this.shapeError(stmt, form, toWord);
|
|
1136
|
+
if (toRef?.kind !== "QualifiedRef")
|
|
1137
|
+
return this.shapeError(stmt, form, toRef);
|
|
1138
|
+
const via = [];
|
|
1139
|
+
if (viaWord) {
|
|
1140
|
+
if (!isWord(viaWord, "via"))
|
|
1141
|
+
return this.shapeError(stmt, form, viaWord);
|
|
1142
|
+
if (viaList?.kind !== "List")
|
|
1143
|
+
return this.invalid(messages().diagnostics.flow.viaList, viaList ?? viaWord);
|
|
1144
|
+
for (const item of viaList.items) {
|
|
1145
|
+
const point = readPoint(item);
|
|
1146
|
+
if (!point)
|
|
1147
|
+
return this.invalid(messages().diagnostics.flow.waypoint, item);
|
|
1148
|
+
via.push(point);
|
|
1149
|
+
}
|
|
1150
|
+
if (extra)
|
|
1151
|
+
return this.shapeError(stmt, form, extra);
|
|
1152
|
+
}
|
|
1153
|
+
const from = this.positioned(this.readEndpoint(fromRef), fromRef, fromAt);
|
|
1154
|
+
const to = this.positioned(this.readEndpoint(toRef), toRef, toAt);
|
|
1155
|
+
if (!from || !to)
|
|
1156
|
+
return;
|
|
1157
|
+
const relation = this.resolveRelation(targetToken, "route", stmt);
|
|
1158
|
+
if (!relation)
|
|
1159
|
+
return;
|
|
1160
|
+
if (from.endpoint.element !== relation.from) {
|
|
1161
|
+
this.error("placement.route-mismatch", messages().diagnostics.flow.routeStart(relation.key, relation.from, from.endpoint.element), fromRef);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
if (to.endpoint.element !== relation.to) {
|
|
1165
|
+
this.error("placement.route-mismatch", messages().diagnostics.flow.routeEnd(relation.key, relation.to, to.endpoint.element), toRef);
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
if (this.duplicatePlacement(stmt, relation.key))
|
|
1169
|
+
return;
|
|
1170
|
+
this.reference(from.token, from.endpoint.element, "element", "route-endpoint", stmt);
|
|
1171
|
+
this.reference(to.token, to.endpoint.element, "element", "route-endpoint", stmt);
|
|
1172
|
+
this.placement.push({
|
|
1173
|
+
statement: "route",
|
|
1174
|
+
target: relation.key,
|
|
1175
|
+
from: from.endpoint,
|
|
1176
|
+
to: to.endpoint,
|
|
1177
|
+
via,
|
|
1178
|
+
range: this.range(stmt),
|
|
1179
|
+
targetRange: this.range(targetToken),
|
|
1180
|
+
node: stmt,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
/** An endpoint with the position written after it, when it is a compass side. */
|
|
1184
|
+
positioned(read, ref, at) {
|
|
1185
|
+
if (!read || !at)
|
|
1186
|
+
return read;
|
|
1187
|
+
const { endpoint } = read;
|
|
1188
|
+
if (endpoint.named || endpoint.port === "auto") {
|
|
1189
|
+
this.invalid(messages().diagnostics.flow.endPosition(ref.text, endpoint.element), at);
|
|
1190
|
+
return undefined;
|
|
1191
|
+
}
|
|
1192
|
+
if (at.kind !== "NumberLiteral" || at.unit !== "%" || at.value < 0 || at.value > 100) {
|
|
1193
|
+
this.invalid(messages().diagnostics.flow.endPositionRange, at);
|
|
1194
|
+
return undefined;
|
|
1195
|
+
}
|
|
1196
|
+
return { ...read, endpoint: { ...endpoint, position: at.value / 100 } };
|
|
1197
|
+
}
|
|
1198
|
+
readEndpoint(ref) {
|
|
1199
|
+
if (ref.parts.length > 2) {
|
|
1200
|
+
this.invalid(messages().diagnostics.flow.endpointShape(ref.text), ref);
|
|
1201
|
+
return undefined;
|
|
1202
|
+
}
|
|
1203
|
+
const [elementToken, portToken] = ref.parts;
|
|
1204
|
+
let port = "auto";
|
|
1205
|
+
let named = false;
|
|
1206
|
+
if (portToken) {
|
|
1207
|
+
const declared = this.elementsById.get(elementToken.text)?.ports ?? [];
|
|
1208
|
+
if (PORTS.includes(portToken.text)) {
|
|
1209
|
+
port = portToken.text;
|
|
1210
|
+
}
|
|
1211
|
+
else if (declared.some((p) => p.name === portToken.text)) {
|
|
1212
|
+
port = portToken.text;
|
|
1213
|
+
named = true;
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
const m = messages();
|
|
1217
|
+
this.error("placement.unknown-port", m.diagnostics.common.unknownPort(elementToken.text, portToken.text), portToken, {
|
|
1218
|
+
data: { ids: [elementToken.text, portToken.text] },
|
|
1219
|
+
hints: [
|
|
1220
|
+
m.hints.flow.ports(elementToken.text, [...declared.map((p) => p.name), ...PORTS]),
|
|
1221
|
+
],
|
|
1222
|
+
});
|
|
1223
|
+
return undefined;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
endpoint: { element: elementToken.text, port, named, explicitPort: portToken !== undefined },
|
|
1228
|
+
token: elementToken,
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
/** Reads `align`, `row`, `column`, `order` and `distance` constraints. */
|
|
1232
|
+
constraint(stmt, keyword) {
|
|
1233
|
+
const kind = keyword;
|
|
1234
|
+
const forms = {
|
|
1235
|
+
align: "`align <element>, <element>... top|middle|bottom|left|center|right`",
|
|
1236
|
+
row: "`row <element>, <element>...`",
|
|
1237
|
+
column: "`column <element>, <element>...`",
|
|
1238
|
+
order: "`order <element>, <element>... [horizontal|vertical]`",
|
|
1239
|
+
distance: "`distance <element>, <element> <n>`",
|
|
1240
|
+
};
|
|
1241
|
+
if (!this.noBlock(stmt, forms[kind]))
|
|
1242
|
+
return;
|
|
1243
|
+
const args = [...stmt.args];
|
|
1244
|
+
let edge = kind === "row" ? "middle" : kind === "column" ? "center" : null;
|
|
1245
|
+
let axis = null;
|
|
1246
|
+
let distance = null;
|
|
1247
|
+
if (kind === "align") {
|
|
1248
|
+
const last = args.pop();
|
|
1249
|
+
const word = singleIdent(last);
|
|
1250
|
+
if (!word || !ALIGN_EDGES.includes(word.text)) {
|
|
1251
|
+
return this.invalid(messages().diagnostics.flow.alignEdge(ALIGN_EDGES), last ?? stmt.keyword);
|
|
1252
|
+
}
|
|
1253
|
+
edge = word.text;
|
|
1254
|
+
}
|
|
1255
|
+
else if (kind === "order") {
|
|
1256
|
+
const word = singleIdent(args[args.length - 1]);
|
|
1257
|
+
if (word &&
|
|
1258
|
+
AXES.includes(word.text) &&
|
|
1259
|
+
!this.elementsById.has(word.text)) {
|
|
1260
|
+
axis = word.text;
|
|
1261
|
+
args.pop();
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
else if (kind === "distance") {
|
|
1265
|
+
const last = args.pop();
|
|
1266
|
+
if (last?.kind !== "NumberLiteral" ||
|
|
1267
|
+
(last.unit !== null && last.unit !== "px") ||
|
|
1268
|
+
last.value <= 0) {
|
|
1269
|
+
const m = messages();
|
|
1270
|
+
return this.invalid(m.diagnostics.flow.distanceGap, last ?? stmt.keyword, [
|
|
1271
|
+
m.hints.example("distance api, db 80"),
|
|
1272
|
+
]);
|
|
1273
|
+
}
|
|
1274
|
+
distance = last.value;
|
|
1275
|
+
}
|
|
1276
|
+
const targets = [];
|
|
1277
|
+
const targetRanges = [];
|
|
1278
|
+
for (const arg of args) {
|
|
1279
|
+
const token = this.targetToken(arg, "placement");
|
|
1280
|
+
if (!token)
|
|
1281
|
+
return this.shapeError(stmt, forms[kind], arg);
|
|
1282
|
+
const element = this.elementsById.get(token.text);
|
|
1283
|
+
if (!element) {
|
|
1284
|
+
const m = messages();
|
|
1285
|
+
this.error("ref.unknown-element", m.diagnostics.common.unknownElement(token.text), token, {
|
|
1286
|
+
hints: [m.hints.flow.constraintElements],
|
|
1287
|
+
data: { ids: [token.text] },
|
|
1288
|
+
});
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (element.kind === "group") {
|
|
1292
|
+
return this.invalid(messages().diagnostics.flow.constraintGroup(token.text), token);
|
|
1293
|
+
}
|
|
1294
|
+
if (targets.includes(token.text)) {
|
|
1295
|
+
return this.invalid(messages().diagnostics.flow.constraintTwice(token.text), token);
|
|
1296
|
+
}
|
|
1297
|
+
targets.push(token.text);
|
|
1298
|
+
targetRanges.push(this.range(token));
|
|
1299
|
+
this.reference(token, token.text, "element", "constraint", stmt);
|
|
1300
|
+
}
|
|
1301
|
+
if ((kind === "distance" && targets.length !== 2) || targets.length < 2) {
|
|
1302
|
+
const m = messages();
|
|
1303
|
+
return this.invalid(m.diagnostics.flow.constraintCount(kind, kind === "distance"), stmt.keyword, [m.hints.flow.form(forms[kind])]);
|
|
1304
|
+
}
|
|
1305
|
+
this.constraints.push({
|
|
1306
|
+
kind,
|
|
1307
|
+
targets,
|
|
1308
|
+
targetRanges,
|
|
1309
|
+
edge,
|
|
1310
|
+
axis,
|
|
1311
|
+
distance,
|
|
1312
|
+
range: this.range(stmt),
|
|
1313
|
+
node: stmt,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
label(stmt) {
|
|
1317
|
+
const form = "`label <relation> at <n>% offset (dx, dy)`";
|
|
1318
|
+
if (!this.noBlock(stmt, form))
|
|
1319
|
+
return;
|
|
1320
|
+
const [target, atWord, at, offsetWord, offset, extra] = stmt.args;
|
|
1321
|
+
const targetToken = this.targetToken(target, "placement");
|
|
1322
|
+
if (!targetToken)
|
|
1323
|
+
return this.shapeError(stmt, form, target);
|
|
1324
|
+
if (!isWord(atWord, "at"))
|
|
1325
|
+
return this.shapeError(stmt, form, atWord);
|
|
1326
|
+
if (at?.kind !== "NumberLiteral" || at.unit !== "%") {
|
|
1327
|
+
return this.invalid(messages().diagnostics.flow.labelPosition, at ?? stmt);
|
|
1328
|
+
}
|
|
1329
|
+
if (at.value < 0 || at.value > 100) {
|
|
1330
|
+
return this.invalid(messages().diagnostics.flow.labelOutside(at.token.text), at);
|
|
1331
|
+
}
|
|
1332
|
+
let point = { x: 0, y: 0 };
|
|
1333
|
+
if (offsetWord) {
|
|
1334
|
+
if (!isWord(offsetWord, "offset"))
|
|
1335
|
+
return this.shapeError(stmt, form, offsetWord);
|
|
1336
|
+
const p = readPoint(offset);
|
|
1337
|
+
if (!p)
|
|
1338
|
+
return this.invalid(messages().diagnostics.flow.offset, offset ?? offsetWord);
|
|
1339
|
+
point = p;
|
|
1340
|
+
if (extra)
|
|
1341
|
+
return this.shapeError(stmt, form, extra);
|
|
1342
|
+
}
|
|
1343
|
+
const relation = this.resolveRelation(targetToken, "label", stmt);
|
|
1344
|
+
if (!relation || this.duplicatePlacement(stmt, relation.key))
|
|
1345
|
+
return;
|
|
1346
|
+
this.placement.push({
|
|
1347
|
+
statement: "label",
|
|
1348
|
+
target: relation.key,
|
|
1349
|
+
at: at.value,
|
|
1350
|
+
offset: point,
|
|
1351
|
+
range: this.range(stmt),
|
|
1352
|
+
targetRange: this.range(targetToken),
|
|
1353
|
+
node: stmt,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
resolveElement(token, statement, stmt) {
|
|
1357
|
+
const name = token.text;
|
|
1358
|
+
if (this.elementsById.has(name)) {
|
|
1359
|
+
this.reference(token, name, "element", statement, stmt);
|
|
1360
|
+
return name;
|
|
1361
|
+
}
|
|
1362
|
+
if (this.brokenRelationIds.has(name))
|
|
1363
|
+
return undefined;
|
|
1364
|
+
if (this.relationsByKey.has(name)) {
|
|
1365
|
+
this.error("ref.unknown-element", messages().diagnostics.flow.relationNotElement(name, statement), token, { data: { ids: [name] } });
|
|
1366
|
+
return undefined;
|
|
1367
|
+
}
|
|
1368
|
+
const m = messages();
|
|
1369
|
+
this.error("ref.unknown-element", m.diagnostics.common.unknownElement(name), token, {
|
|
1370
|
+
hints: [m.hints.flow.declareFirst(name)],
|
|
1371
|
+
data: { ids: [name] },
|
|
1372
|
+
});
|
|
1373
|
+
return undefined;
|
|
1374
|
+
}
|
|
1375
|
+
resolveRelation(token, statement, stmt) {
|
|
1376
|
+
const name = token.text;
|
|
1377
|
+
const relation = this.relationsByKey.get(name);
|
|
1378
|
+
if (relation && relation.id !== null) {
|
|
1379
|
+
this.reference(token, name, "relation", statement, stmt);
|
|
1380
|
+
return relation;
|
|
1381
|
+
}
|
|
1382
|
+
if (this.brokenRelationIds.has(name))
|
|
1383
|
+
return undefined;
|
|
1384
|
+
if (this.elementsById.has(name)) {
|
|
1385
|
+
this.error("ref.unknown-relation", messages().diagnostics.flow.elementNotRelation(name, statement), token);
|
|
1386
|
+
return undefined;
|
|
1387
|
+
}
|
|
1388
|
+
const m = messages();
|
|
1389
|
+
this.error("ref.unknown-relation", m.diagnostics.flow.unknownRelation(name), token, {
|
|
1390
|
+
hints: [m.hints.flow.nameRelation],
|
|
1391
|
+
});
|
|
1392
|
+
return undefined;
|
|
1393
|
+
}
|
|
1394
|
+
// ------------------------------------------------------------------ style and animate
|
|
1395
|
+
/** `style a, b { ... }` and `animate a, b { ... }`: one block applied to one or more targets. */
|
|
1396
|
+
styleOrAnimate(stmt, kind) {
|
|
1397
|
+
const form = `\`${kind} <target>[, <target>...] { ... }\``;
|
|
1398
|
+
if (stmt.args.length === 0)
|
|
1399
|
+
return this.shapeError(stmt, form, stmt.keyword);
|
|
1400
|
+
const targetTokens = [];
|
|
1401
|
+
for (const arg of stmt.args) {
|
|
1402
|
+
const token = this.targetToken(arg, kind);
|
|
1403
|
+
if (!token)
|
|
1404
|
+
return this.shapeError(stmt, form, arg);
|
|
1405
|
+
targetTokens.push(token);
|
|
1406
|
+
}
|
|
1407
|
+
if (stmt.chain || !stmt.block) {
|
|
1408
|
+
return this.invalid(messages().diagnostics.common.needsOneBlock(kind, form), stmt.chain ?? stmt.keyword);
|
|
1409
|
+
}
|
|
1410
|
+
const table = kind === "style" ? this.profile.styleProperties : this.profile.animateProperties;
|
|
1411
|
+
const shapes = profileTakesShape(this.profile);
|
|
1412
|
+
const words = kind === "style"
|
|
1413
|
+
? Object.keys(table).filter((p) => p === "shape" ? shapes : this.free || !(p in FREE_PROFILE_ONLY))
|
|
1414
|
+
: ["effect", "duration", "repeat"];
|
|
1415
|
+
const hint = () => {
|
|
1416
|
+
const m = messages().hints.flow;
|
|
1417
|
+
return kind === "style" ? m.styleAccepts(words) : m.animateAccepts;
|
|
1418
|
+
};
|
|
1419
|
+
const read = this.readProperties(stmt.block, table, hint, kind, words);
|
|
1420
|
+
const { classes, classToken, icon } = read;
|
|
1421
|
+
const values = { ...read.values };
|
|
1422
|
+
const tokens = { ...read.tokens };
|
|
1423
|
+
if (kind === "animate")
|
|
1424
|
+
this.checkAnimationBlock(stmt, values, tokens);
|
|
1425
|
+
if (kind === "style" && !this.free) {
|
|
1426
|
+
refuseFreeProperties(values, tokens, this.report);
|
|
1427
|
+
for (const name of Object.keys(tokens))
|
|
1428
|
+
if (!(name in values))
|
|
1429
|
+
delete tokens[name];
|
|
1430
|
+
}
|
|
1431
|
+
if (icon)
|
|
1432
|
+
this.icons.push(icon);
|
|
1433
|
+
const applied = { ...tokens };
|
|
1434
|
+
if (classToken)
|
|
1435
|
+
applied.class = classToken;
|
|
1436
|
+
const misplaced = new Set();
|
|
1437
|
+
for (const targetToken of targetTokens) {
|
|
1438
|
+
const resolved = this.resolveStyleTarget(targetToken, kind, stmt);
|
|
1439
|
+
if (!resolved)
|
|
1440
|
+
continue;
|
|
1441
|
+
const misfit = kind === "style"
|
|
1442
|
+
? resolved.kind === "relation"
|
|
1443
|
+
? ELEMENT_ONLY_PROPERTIES
|
|
1444
|
+
: RELATION_ONLY_PROPERTIES
|
|
1445
|
+
: [];
|
|
1446
|
+
for (const name of misfit) {
|
|
1447
|
+
const token = tokens[name];
|
|
1448
|
+
// `shape` is reported below, with the kinds that take it.
|
|
1449
|
+
if (!token || name === "shape" || misplaced.has(name))
|
|
1450
|
+
continue;
|
|
1451
|
+
misplaced.add(name);
|
|
1452
|
+
this.invalid(messages().diagnostics.flow.notForTargets(name, resolved.kind === "relation", resolved.name), token, [targetHint(name)]);
|
|
1453
|
+
}
|
|
1454
|
+
// `shape` is refused per target: a block may list a node and a decision.
|
|
1455
|
+
const shapeless = kind === "style" && tokens.shape ? this.shapeRefusal(resolved) : null;
|
|
1456
|
+
if (shapeless !== null && !misplaced.has("shape")) {
|
|
1457
|
+
misplaced.add("shape");
|
|
1458
|
+
this.invalid(messages().diagnostics.values.shapeNotFor(shapeless, resolved.name), tokens.shape);
|
|
1459
|
+
}
|
|
1460
|
+
// `rotation` too: a block may list a node and a group (decision 0045).
|
|
1461
|
+
const unturned = kind === "style" && tokens.rotation ? this.rotationRefusal(resolved) : null;
|
|
1462
|
+
if (unturned !== null && !misplaced.has("rotation")) {
|
|
1463
|
+
misplaced.add("rotation");
|
|
1464
|
+
this.invalid(unturned.profile
|
|
1465
|
+
? messages().diagnostics.values.rotationInProfile(this.profile.name)
|
|
1466
|
+
: messages().diagnostics.values.rotationNotFor(unturned.kind, resolved.name), tokens.rotation);
|
|
1467
|
+
}
|
|
1468
|
+
const refused = [
|
|
1469
|
+
...(shapeless === null ? [] : ["shape"]),
|
|
1470
|
+
...(unturned === null ? [] : ["rotation"]),
|
|
1471
|
+
];
|
|
1472
|
+
const excluded = refused.length === 0 ? misfit : [...misfit, ...refused];
|
|
1473
|
+
if (kind === "animate") {
|
|
1474
|
+
// One effect per channel and target; different channels may share a target.
|
|
1475
|
+
const effect = values.effect;
|
|
1476
|
+
if (effect && !this.animationTargetFits(effect, resolved, targetToken))
|
|
1477
|
+
continue;
|
|
1478
|
+
if (effect) {
|
|
1479
|
+
const key = `${resolved.name}:${effect}`;
|
|
1480
|
+
const first = this.animationSeen.get(key);
|
|
1481
|
+
if (first) {
|
|
1482
|
+
const m = messages();
|
|
1483
|
+
this.error("animate.conflict", m.diagnostics.flow.effectConflict(resolved.name, effect), targetToken, {
|
|
1484
|
+
related: [{ message: m.related.firstEffect, at: first }],
|
|
1485
|
+
hints: [m.hints.flow.mergeEffects],
|
|
1486
|
+
});
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
this.animationSeen.set(key, targetToken);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
for (const [name, token] of Object.entries(applied)) {
|
|
1493
|
+
if (kind === "animate")
|
|
1494
|
+
break;
|
|
1495
|
+
if (excluded.includes(name))
|
|
1496
|
+
continue;
|
|
1497
|
+
const key = `${kind}:${resolved.name}:${name}`;
|
|
1498
|
+
const first = this.styleSeen.get(key);
|
|
1499
|
+
if (first) {
|
|
1500
|
+
const m = messages();
|
|
1501
|
+
this.error("prop.duplicate", m.diagnostics.flow.alreadySetFor(name, resolved.name), token, {
|
|
1502
|
+
related: [{ message: m.related.firstSet, at: first }],
|
|
1503
|
+
});
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
this.styleSeen.set(key, token);
|
|
1507
|
+
}
|
|
1508
|
+
const common = {
|
|
1509
|
+
target: resolved.name,
|
|
1510
|
+
targetKind: resolved.kind,
|
|
1511
|
+
range: this.range(stmt),
|
|
1512
|
+
targetRange: this.range(targetToken),
|
|
1513
|
+
node: stmt,
|
|
1514
|
+
};
|
|
1515
|
+
if (kind === "style") {
|
|
1516
|
+
const properties = {};
|
|
1517
|
+
for (const [name, value] of Object.entries(values)) {
|
|
1518
|
+
if (excluded.includes(name))
|
|
1519
|
+
continue;
|
|
1520
|
+
if (typeof value === "string" || typeof value === "number")
|
|
1521
|
+
properties[name] = value;
|
|
1522
|
+
}
|
|
1523
|
+
this.styles.push({ ...common, properties, classes });
|
|
1524
|
+
}
|
|
1525
|
+
else {
|
|
1526
|
+
this.animations.push({
|
|
1527
|
+
...common,
|
|
1528
|
+
effect: values.effect ?? null,
|
|
1529
|
+
durationMs: typeof values.duration === "number" ? values.duration : null,
|
|
1530
|
+
repeat: values.repeat === "forever"
|
|
1531
|
+
? "forever"
|
|
1532
|
+
: typeof values.repeat === "number"
|
|
1533
|
+
? values.repeat
|
|
1534
|
+
: null,
|
|
1535
|
+
delayMs: typeof values.delay === "number" ? values.delay : null,
|
|
1536
|
+
direction: values.direction ?? null,
|
|
1537
|
+
particles: typeof values.particles === "number" ? values.particles : null,
|
|
1538
|
+
color: typeof values.color === "string" ? values.color : null,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* An `animate` block names its effect, and its other properties must apply to that effect.
|
|
1545
|
+
* Properties that do not are reported and dropped.
|
|
1546
|
+
*/
|
|
1547
|
+
checkAnimationBlock(stmt, values, tokens) {
|
|
1548
|
+
const effect = values.effect;
|
|
1549
|
+
if (effect === undefined) {
|
|
1550
|
+
// An `effect` with a wrong value is already reported as such.
|
|
1551
|
+
const written = stmt.block?.statements.some((s) => s.kind === "KeywordStatement" && s.keyword.text === "effect");
|
|
1552
|
+
if (!written) {
|
|
1553
|
+
const m = messages();
|
|
1554
|
+
this.error("animate.missing-effect", m.diagnostics.flow.missingEffect, stmt.keyword, {
|
|
1555
|
+
hints: [m.hints.example("animate r1 { effect flow; duration 1200ms }")],
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
for (const name of ["direction", "particles", "color"]) {
|
|
1561
|
+
const token = tokens[name];
|
|
1562
|
+
if (!token || ANIMATION_EFFECT_PROPERTIES[effect].includes(name))
|
|
1563
|
+
continue;
|
|
1564
|
+
const users = Object.keys(ANIMATION_EFFECT_PROPERTIES).filter((e) => ANIMATION_EFFECT_PROPERTIES[e].includes(name));
|
|
1565
|
+
this.invalid(messages().diagnostics.flow.effectProperty(name, users, effect), token);
|
|
1566
|
+
delete values[name];
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* The kind a refusal of `shape` names for a style target (`relation` for relations, the event
|
|
1571
|
+
* kind for events), or null when the target takes it: nodes and free boxes. Free text, icons and
|
|
1572
|
+
* images are refused as `rule text|icon|image` refuses them.
|
|
1573
|
+
*/
|
|
1574
|
+
shapeRefusal(resolved) {
|
|
1575
|
+
if (resolved.kind === "relation")
|
|
1576
|
+
return "relation";
|
|
1577
|
+
if (resolved.kind === "event")
|
|
1578
|
+
return this.eventsByKey.get(resolved.name)?.kind ?? "event";
|
|
1579
|
+
const element = this.elementsById.get(resolved.name);
|
|
1580
|
+
if (!element || takesShape(element.kind))
|
|
1581
|
+
return null;
|
|
1582
|
+
return element.kind;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Why `rotation` does not turn a style target, or null when it does: the profile (sequence
|
|
1586
|
+
* diagrams, C4 views), or the kind of the element (groups). Relations are refused with the other
|
|
1587
|
+
* element properties.
|
|
1588
|
+
*/
|
|
1589
|
+
rotationRefusal(resolved) {
|
|
1590
|
+
if (!profileTakesRotation(this.profile))
|
|
1591
|
+
return { profile: true };
|
|
1592
|
+
if (resolved.kind !== "element")
|
|
1593
|
+
return null;
|
|
1594
|
+
const element = this.elementsById.get(resolved.name);
|
|
1595
|
+
if (!element || takesRotation(element.kind))
|
|
1596
|
+
return null;
|
|
1597
|
+
return { profile: false, kind: element.kind };
|
|
1598
|
+
}
|
|
1599
|
+
/** Whether an effect can run on a target; reports `animate.invalid-target` when not. */
|
|
1600
|
+
animationTargetFits(effect, resolved, token) {
|
|
1601
|
+
const element = resolved.kind === "element" ? this.elementsById.get(resolved.name) : undefined;
|
|
1602
|
+
const m = messages().diagnostics.flow;
|
|
1603
|
+
let reason = null;
|
|
1604
|
+
if (effect === "flow" && resolved.kind !== "relation") {
|
|
1605
|
+
reason = m.flowTarget;
|
|
1606
|
+
}
|
|
1607
|
+
else if (effect === "trace" && resolved.kind !== "relation") {
|
|
1608
|
+
reason = m.traceTarget;
|
|
1609
|
+
}
|
|
1610
|
+
else if (effect === "dash") {
|
|
1611
|
+
if (resolved.kind === "event")
|
|
1612
|
+
reason = m.dashOnEvent;
|
|
1613
|
+
else if (element && NO_OUTLINE_KINDS.has(element.kind)) {
|
|
1614
|
+
reason = m.dashWithoutOutline(resolved.name, element.kind);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (reason === null)
|
|
1618
|
+
return true;
|
|
1619
|
+
this.error("animate.invalid-target", reason, token);
|
|
1620
|
+
return false;
|
|
1621
|
+
}
|
|
1622
|
+
resolveStyleTarget(token, kind, stmt) {
|
|
1623
|
+
const name = token.text;
|
|
1624
|
+
if (this.elementsById.has(name)) {
|
|
1625
|
+
this.reference(token, name, "element", kind, stmt);
|
|
1626
|
+
return { name, kind: "element" };
|
|
1627
|
+
}
|
|
1628
|
+
const relation = this.relationsByKey.get(name);
|
|
1629
|
+
if (relation && relation.id !== null) {
|
|
1630
|
+
this.reference(token, name, "relation", kind, stmt);
|
|
1631
|
+
return { name, kind: "relation" };
|
|
1632
|
+
}
|
|
1633
|
+
const event = this.eventsByKey.get(name);
|
|
1634
|
+
if (event && event.id === name && event.kind !== "message") {
|
|
1635
|
+
this.reference(token, name, "event", kind, stmt);
|
|
1636
|
+
return { name, kind: "event" };
|
|
1637
|
+
}
|
|
1638
|
+
if (this.brokenRelationIds.has(name))
|
|
1639
|
+
return undefined;
|
|
1640
|
+
const m = messages();
|
|
1641
|
+
this.error("ref.unknown-element", m.diagnostics.flow.unknownTarget(name), token, {
|
|
1642
|
+
hints: [m.hints.flow.styleTargets(kind)],
|
|
1643
|
+
data: { ids: [name] },
|
|
1644
|
+
});
|
|
1645
|
+
return undefined;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
// -------------------------------------------------------------------- free helpers
|
|
1649
|
+
/** The declarations and statements of flow and free diagrams, as their hints list them. */
|
|
1650
|
+
const FLOW_DECLARATIONS = ["node", "decision", "data", "group", "note", "tag"];
|
|
1651
|
+
const FREE_DECLARATIONS = ["box", "text", "icon", "image", "group"];
|
|
1652
|
+
const FLOW_STATEMENTS = [
|
|
1653
|
+
"direction",
|
|
1654
|
+
"layout",
|
|
1655
|
+
"theme",
|
|
1656
|
+
"title",
|
|
1657
|
+
"description",
|
|
1658
|
+
"preset",
|
|
1659
|
+
"canvas",
|
|
1660
|
+
"routing",
|
|
1661
|
+
"placement",
|
|
1662
|
+
"style",
|
|
1663
|
+
"animate",
|
|
1664
|
+
"class",
|
|
1665
|
+
"rule",
|
|
1666
|
+
];
|
|
1667
|
+
const FREE_STATEMENTS = [
|
|
1668
|
+
...FREE_DECLARATIONS,
|
|
1669
|
+
...FLOW_STATEMENTS.slice(0, 8),
|
|
1670
|
+
"layers",
|
|
1671
|
+
...FLOW_STATEMENTS.slice(8),
|
|
1672
|
+
];
|
|
1673
|
+
/** Style properties that only free diagrams accept, for hints. */
|
|
1674
|
+
const FREE_PROFILE_ONLY = {
|
|
1675
|
+
fontSize: true,
|
|
1676
|
+
fontWeight: true,
|
|
1677
|
+
textAlign: true,
|
|
1678
|
+
};
|
|
1679
|
+
function isWord(arg, word) {
|
|
1680
|
+
return singleIdent(arg)?.text === word;
|
|
1681
|
+
}
|
|
1682
|
+
/** A tuple of exactly two numbers without units. */
|
|
1683
|
+
export function readPoint(arg) {
|
|
1684
|
+
if (arg?.kind !== "Tuple" || arg.items.length !== 2)
|
|
1685
|
+
return undefined;
|
|
1686
|
+
const [x, y] = arg.items;
|
|
1687
|
+
if (x?.kind !== "NumberLiteral" || y?.kind !== "NumberLiteral")
|
|
1688
|
+
return undefined;
|
|
1689
|
+
if (x.unit !== null || y.unit !== null)
|
|
1690
|
+
return undefined;
|
|
1691
|
+
return { x: x.value, y: y.value };
|
|
1692
|
+
}
|
|
1693
|
+
//# sourceMappingURL=flow.js.map
|