@meltemi/diagrams-model 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/c4.d.ts +5 -4
- package/dist/c4.d.ts.map +1 -1
- package/dist/c4.js +108 -36
- package/dist/c4.js.map +1 -1
- package/dist/diagram-types.d.ts.map +1 -1
- package/dist/diagram-types.js +4 -1
- package/dist/diagram-types.js.map +1 -1
- package/dist/diff.d.ts +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +4 -0
- package/dist/diff.js.map +1 -1
- package/dist/export.d.ts.map +1 -1
- package/dist/export.js +1 -16
- package/dist/export.js.map +1 -1
- package/dist/flow.d.ts +43 -1
- package/dist/flow.d.ts.map +1 -1
- package/dist/flow.js +230 -20
- package/dist/flow.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +19 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +9 -0
- package/dist/json.js.map +1 -1
- package/dist/messages/changes.d.ts +1 -0
- package/dist/messages/changes.d.ts.map +1 -1
- package/dist/messages/changes.js +2 -0
- package/dist/messages/changes.js.map +1 -1
- package/dist/messages/en.d.ts +16 -6
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +21 -11
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/es.d.ts.map +1 -1
- package/dist/messages/es.js +13 -9
- package/dist/messages/es.js.map +1 -1
- package/dist/messages/index.d.ts +8 -4
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/outline.d.ts +14 -0
- package/dist/outline.d.ts.map +1 -0
- package/dist/outline.js +21 -0
- package/dist/outline.js.map +1 -0
- package/dist/schema.d.ts +24 -4
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +66 -10
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +31 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/flow.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
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";
|
|
2
|
+
import { DIRECTIONS, LAYOUT_MODES, ALIGN_EDGES, ANIMATION_EFFECT_PROPERTIES, AXES, ELEMENT_ONLY_PROPERTIES, FLOW_PROFILE, FREE_PROFILE, MAIN_LAYER, PORTS, PORT_SIDES, RELATION_ARROWS, RELATION_ONLY_PROPERTIES, diagramStatements, profileTakesShape, takesShape, profileTakesRotation, takesRotation, } from "./schema.js";
|
|
3
3
|
import { relationAllowed, } from "./diagram-types.js";
|
|
4
4
|
import { messages } from "./messages/index.js";
|
|
5
5
|
import { StyleDefinitions, checkArg, describeArg, headerHasError, readClasses, readPropertyBlock, readStatementValue, refuseFreeProperties, singleIdent, spanOf, targetHint, } from "./style-values.js";
|
|
6
|
+
import { outlined } from "./outline.js";
|
|
6
7
|
/** Element kinds drawn without an outline, which a moving dash cannot follow. */
|
|
7
8
|
const NO_OUTLINE_KINDS = new Set([
|
|
8
9
|
"note",
|
|
@@ -115,6 +116,10 @@ export class FlowAnalyzer {
|
|
|
115
116
|
layerUses = [];
|
|
116
117
|
layerRanges = new Map();
|
|
117
118
|
animations = [];
|
|
119
|
+
/** Dashes on notes and texts, whose outline is checked once every style is read. */
|
|
120
|
+
pendingDashes = [];
|
|
121
|
+
/** The `on <element>` clauses of notes, resolved once every element is declared. */
|
|
122
|
+
attachments = [];
|
|
118
123
|
references = [];
|
|
119
124
|
entityReferences = [];
|
|
120
125
|
placementBlocks = [];
|
|
@@ -198,6 +203,8 @@ export class FlowAnalyzer {
|
|
|
198
203
|
this.declare(decl.stmt, decl.groupPath);
|
|
199
204
|
for (const rel of classified.relations)
|
|
200
205
|
this.relation(rel.stmt, rel.groupPath);
|
|
206
|
+
// After the relations, which may declare elements that only they mention.
|
|
207
|
+
this.resolveAttachments();
|
|
201
208
|
this.assignKeys();
|
|
202
209
|
this.afterRelations(classified);
|
|
203
210
|
for (const stmt of classified.properties)
|
|
@@ -216,6 +223,7 @@ export class FlowAnalyzer {
|
|
|
216
223
|
this.styleOrAnimate(stmt, keyword);
|
|
217
224
|
}
|
|
218
225
|
this.checkLayers();
|
|
226
|
+
this.checkOutlinedDashes();
|
|
219
227
|
this.finish();
|
|
220
228
|
if (this.declared)
|
|
221
229
|
this.checkDeclared(this.declared);
|
|
@@ -254,6 +262,26 @@ export class FlowAnalyzer {
|
|
|
254
262
|
hasErrors: this.node.hasError || this.errors > 0,
|
|
255
263
|
};
|
|
256
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* A dash follows an outline: a note or a free text that no style statement, class or rule of the
|
|
267
|
+
* diagram gives a `fill`, `stroke` or `pattern` has none, and the dash is refused and dropped.
|
|
268
|
+
*/
|
|
269
|
+
checkOutlinedDashes() {
|
|
270
|
+
const source = {
|
|
271
|
+
styles: this.styles,
|
|
272
|
+
classes: this.definitions.classes,
|
|
273
|
+
rules: this.definitions.rules,
|
|
274
|
+
};
|
|
275
|
+
for (const { name, token, stmt } of this.pendingDashes) {
|
|
276
|
+
const element = this.elementsById.get(name);
|
|
277
|
+
if (!element || outlined(source, element))
|
|
278
|
+
continue;
|
|
279
|
+
this.error("animate.invalid-target", messages().diagnostics.flow.dashNeedsOutline(name, element.kind), token, { hints: [messages().hints.flow.dashOutline] });
|
|
280
|
+
const at = this.animations.findIndex((a) => a.node === stmt && a.target === name && a.effect === "dash");
|
|
281
|
+
if (at >= 0)
|
|
282
|
+
this.animations.splice(at, 1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
257
285
|
/** A declared type's rules: required properties, and the kinds relations may join. */
|
|
258
286
|
checkDeclared(definition) {
|
|
259
287
|
for (const element of this.elements) {
|
|
@@ -420,6 +448,9 @@ export class FlowAnalyzer {
|
|
|
420
448
|
technology: null,
|
|
421
449
|
external: false,
|
|
422
450
|
shape: data ? "cylinder" : null,
|
|
451
|
+
line: null,
|
|
452
|
+
attachedTo: null,
|
|
453
|
+
attachedToRange: null,
|
|
423
454
|
typeKind: null,
|
|
424
455
|
data: {},
|
|
425
456
|
groupPath,
|
|
@@ -435,19 +466,25 @@ export class FlowAnalyzer {
|
|
|
435
466
|
const typeKind = declaredKind ? stmt.keyword.text : null;
|
|
436
467
|
const kind = (declaredKind ? "box" : stmt.keyword.text);
|
|
437
468
|
const sourced = kind === "text" || kind === "icon" || kind === "image";
|
|
469
|
+
const note = kind === "note" && !declaredKind;
|
|
438
470
|
const second = { text: '"Text"', icon: '"<pack>:<name>"', image: '"<path>"' }[kind];
|
|
439
|
-
const form =
|
|
440
|
-
?
|
|
441
|
-
:
|
|
471
|
+
const form = note
|
|
472
|
+
? '`note <id> "Text" [on <element>]`'
|
|
473
|
+
: sourced
|
|
474
|
+
? `\`${kind} <id> ${second}\``
|
|
475
|
+
: `\`${kind} <id> ["Label"]${kind === "group" ? " { ... }" : ""}\``;
|
|
442
476
|
const example = {
|
|
443
477
|
text: 'text heading "Architecture"',
|
|
444
478
|
icon: 'icon server "tabler:server"',
|
|
445
479
|
image: 'image logo "logo.png"',
|
|
480
|
+
note: 'note limits "500 rps" on api',
|
|
446
481
|
}[kind];
|
|
447
482
|
if (stmt.chain) {
|
|
448
483
|
this.invalid(messages().diagnostics.common.singleBlock(kind), stmt.chain.clauses[1].keyword);
|
|
449
484
|
return;
|
|
450
485
|
}
|
|
486
|
+
if (kind === "line" && !declaredKind)
|
|
487
|
+
return this.declareLine(stmt, groupPath);
|
|
451
488
|
const [idArg, labelArg, ...extra] = stmt.args;
|
|
452
489
|
const idToken = singleIdent(idArg);
|
|
453
490
|
if (!idToken) {
|
|
@@ -491,15 +528,22 @@ export class FlowAnalyzer {
|
|
|
491
528
|
sourceRange = this.range(labelArg);
|
|
492
529
|
}
|
|
493
530
|
}
|
|
494
|
-
else if (sourced) {
|
|
531
|
+
else if (sourced || (note && this.notesNeedText)) {
|
|
495
532
|
const m = messages();
|
|
496
533
|
this.invalid(m.diagnostics.flow.needsSource(kind, idToken.text, form), stmt, [
|
|
497
534
|
m.hints.example(example),
|
|
498
535
|
]);
|
|
499
536
|
}
|
|
537
|
+
// A note may end with `on <element>`; nothing else follows a label.
|
|
538
|
+
let attachment;
|
|
500
539
|
if (extra.length > 0) {
|
|
501
|
-
|
|
502
|
-
|
|
540
|
+
attachment = note ? this.attachmentOf(extra, form) : undefined;
|
|
541
|
+
if (!attachment) {
|
|
542
|
+
if (!note || !isWord(extra[0], "on")) {
|
|
543
|
+
this.invalid(messages().diagnostics.flow.valueAfter(note ? "text" : sourced ? kind : "label", form), extra[0]);
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
503
547
|
}
|
|
504
548
|
const block = kind === "group"
|
|
505
549
|
? { ...this.groupBlock(stmt.block, idToken), ports: [], values: {} }
|
|
@@ -520,6 +564,9 @@ export class FlowAnalyzer {
|
|
|
520
564
|
technology: null,
|
|
521
565
|
external: false,
|
|
522
566
|
shape: null,
|
|
567
|
+
line: null,
|
|
568
|
+
attachedTo: attachment?.target ?? null,
|
|
569
|
+
attachedToRange: attachment ? this.range(attachment.arg) : null,
|
|
523
570
|
typeKind,
|
|
524
571
|
data: declaredKind ? dataOf(blockValues, declaredKind.properties) : {},
|
|
525
572
|
groupPath,
|
|
@@ -528,6 +575,8 @@ export class FlowAnalyzer {
|
|
|
528
575
|
idToken,
|
|
529
576
|
node: stmt,
|
|
530
577
|
});
|
|
578
|
+
if (added && attachment)
|
|
579
|
+
this.attachments.push({ note: idToken.text, ...attachment, stmt });
|
|
531
580
|
if (!added || source === null || !sourceRange)
|
|
532
581
|
return;
|
|
533
582
|
if (kind === "icon")
|
|
@@ -535,6 +584,123 @@ export class FlowAnalyzer {
|
|
|
535
584
|
else
|
|
536
585
|
this.images.push({ element: idToken.text, path: source, range: sourceRange });
|
|
537
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* `line <id> ["Label"] from (x, y) to (x, y) [via [(x, y), ...]] [{ ... }]`: a free line that
|
|
589
|
+
* joins no element, its points in the interior coordinates of its innermost group. The label is
|
|
590
|
+
* empty unless written: a line has no name to show.
|
|
591
|
+
*/
|
|
592
|
+
declareLine(stmt, groupPath) {
|
|
593
|
+
const form = '`line <id> ["Label"] from (x, y) to (x, y) [via [(x, y), ...]]`';
|
|
594
|
+
const m = messages();
|
|
595
|
+
const [idArg, ...rest] = stmt.args;
|
|
596
|
+
const idToken = singleIdent(idArg);
|
|
597
|
+
if (!idToken) {
|
|
598
|
+
this.invalid(m.diagnostics.common.expected(form), idArg ?? stmt.keyword, [
|
|
599
|
+
m.hints.example("line divider from (40, 200) to (520, 200)"),
|
|
600
|
+
]);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const refuse = (at) => this.invalid(m.diagnostics.flow.linePoints, at ?? stmt, [m.hints.flow.form(form)]);
|
|
604
|
+
const labelArg = rest[0]?.kind === "StringLiteral" ? rest[0] : undefined;
|
|
605
|
+
const [fromWord, fromArg, toWord, toArg, viaWord, viaList, extra] = labelArg
|
|
606
|
+
? rest.slice(1)
|
|
607
|
+
: rest;
|
|
608
|
+
if (!isWord(fromWord, "from"))
|
|
609
|
+
return refuse(fromWord);
|
|
610
|
+
const from = readPoint(fromArg);
|
|
611
|
+
if (!from)
|
|
612
|
+
return refuse(fromArg);
|
|
613
|
+
if (!isWord(toWord, "to"))
|
|
614
|
+
return refuse(toWord);
|
|
615
|
+
const to = readPoint(toArg);
|
|
616
|
+
if (!to)
|
|
617
|
+
return refuse(toArg);
|
|
618
|
+
const via = [];
|
|
619
|
+
if (viaWord) {
|
|
620
|
+
if (!isWord(viaWord, "via") || viaList?.kind !== "List")
|
|
621
|
+
return refuse(viaList ?? viaWord);
|
|
622
|
+
for (const item of viaList.items) {
|
|
623
|
+
const point = readPoint(item);
|
|
624
|
+
if (!point)
|
|
625
|
+
return refuse(item);
|
|
626
|
+
via.push(point);
|
|
627
|
+
}
|
|
628
|
+
if (extra)
|
|
629
|
+
return refuse(extra);
|
|
630
|
+
}
|
|
631
|
+
const { values: _values, ...blockFields } = this.elementBlock(stmt.block, this.profile.elementProperties, "Element", idToken);
|
|
632
|
+
this.addElement({
|
|
633
|
+
id: idToken.text,
|
|
634
|
+
kind: "line",
|
|
635
|
+
label: labelArg?.kind === "StringLiteral" ? labelArg.value : "",
|
|
636
|
+
implicit: false,
|
|
637
|
+
...blockFields,
|
|
638
|
+
source: null,
|
|
639
|
+
sourceRange: null,
|
|
640
|
+
labelWritten: labelArg !== undefined,
|
|
641
|
+
entity: null,
|
|
642
|
+
technology: null,
|
|
643
|
+
external: false,
|
|
644
|
+
shape: null,
|
|
645
|
+
line: { from, to, via },
|
|
646
|
+
attachedTo: null,
|
|
647
|
+
attachedToRange: null,
|
|
648
|
+
typeKind: null,
|
|
649
|
+
data: {},
|
|
650
|
+
groupPath,
|
|
651
|
+
range: this.range(stmt),
|
|
652
|
+
idRange: this.range(idToken),
|
|
653
|
+
idToken,
|
|
654
|
+
node: stmt,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/** Whether a note must write its text: free compositions and views, where a note is no node. */
|
|
658
|
+
get notesNeedText() {
|
|
659
|
+
return this.free;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The target of a note's `on <element>`: `extra` holds the word `on` and one identifier (C4
|
|
663
|
+
* views read an entity path), nothing else. Undefined when it does not; a lone `on` is reported
|
|
664
|
+
* here, as the form is wanted.
|
|
665
|
+
*/
|
|
666
|
+
attachmentOf(extra, form) {
|
|
667
|
+
if (!isWord(extra[0], "on"))
|
|
668
|
+
return undefined;
|
|
669
|
+
const arg = extra[1];
|
|
670
|
+
const token = singleIdent(arg);
|
|
671
|
+
if (!token || extra.length > 2) {
|
|
672
|
+
this.invalid(messages().diagnostics.common.expected(form), extra[1] ?? extra[0]);
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
return { arg: arg, target: token.text };
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Resolves the targets of the notes' `on <element>`: an element of the diagram other than the
|
|
679
|
+
* note itself, of any kind. Renames follow the reference this records.
|
|
680
|
+
*/
|
|
681
|
+
resolveAttachments() {
|
|
682
|
+
const m = messages();
|
|
683
|
+
for (const { note, arg, target, stmt } of this.attachments) {
|
|
684
|
+
const token = singleIdent(arg);
|
|
685
|
+
if (target === note) {
|
|
686
|
+
this.invalid(m.diagnostics.flow.attachSelf(note), token);
|
|
687
|
+
}
|
|
688
|
+
else if (this.elementsById.has(target)) {
|
|
689
|
+
this.reference(token, target, "element", "attachment", stmt);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
this.error("ref.unknown-element", m.diagnostics.common.unknownElement(target), token, {
|
|
693
|
+
hints: [m.hints.flow.attachTarget(note)],
|
|
694
|
+
data: {
|
|
695
|
+
ids: [target],
|
|
696
|
+
candidates: this.elements
|
|
697
|
+
.filter((e) => e.id !== note && e.kind !== "line")
|
|
698
|
+
.map((e) => e.id),
|
|
699
|
+
},
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
538
704
|
addElement(element) {
|
|
539
705
|
const existing = this.elementsById.get(element.id);
|
|
540
706
|
if (existing) {
|
|
@@ -782,7 +948,8 @@ export class FlowAnalyzer {
|
|
|
782
948
|
from,
|
|
783
949
|
to,
|
|
784
950
|
line: this.profile.arrows[stmt.arrow.text] ?? "solid",
|
|
785
|
-
|
|
951
|
+
heads: RELATION_ARROWS[stmt.arrow.text]?.heads ?? "end",
|
|
952
|
+
twoWay: RELATION_ARROWS[stmt.arrow.text]?.heads === "both",
|
|
786
953
|
async: values.kind === "async",
|
|
787
954
|
technology: null,
|
|
788
955
|
sources: [],
|
|
@@ -841,6 +1008,9 @@ export class FlowAnalyzer {
|
|
|
841
1008
|
technology: null,
|
|
842
1009
|
external: false,
|
|
843
1010
|
shape: null,
|
|
1011
|
+
line: null,
|
|
1012
|
+
attachedTo: null,
|
|
1013
|
+
attachedToRange: null,
|
|
844
1014
|
typeKind: null,
|
|
845
1015
|
data: {},
|
|
846
1016
|
groupPath,
|
|
@@ -1075,7 +1245,9 @@ export class FlowAnalyzer {
|
|
|
1075
1245
|
if (extra)
|
|
1076
1246
|
return this.shapeError(stmt, form, extra);
|
|
1077
1247
|
const element = this.resolveElement(targetToken, "place", stmt);
|
|
1078
|
-
if (!element || this.
|
|
1248
|
+
if (!element || this.lineRefused(element, "place", targetToken))
|
|
1249
|
+
return;
|
|
1250
|
+
if (this.duplicatePlacement(stmt, element))
|
|
1079
1251
|
return;
|
|
1080
1252
|
this.placement.push({
|
|
1081
1253
|
statement: "place",
|
|
@@ -1102,7 +1274,9 @@ export class FlowAnalyzer {
|
|
|
1102
1274
|
if (extra)
|
|
1103
1275
|
return this.shapeError(stmt, form, extra);
|
|
1104
1276
|
const element = this.resolveElement(targetToken, "size", stmt);
|
|
1105
|
-
if (!element || this.
|
|
1277
|
+
if (!element || this.lineRefused(element, "size", targetToken))
|
|
1278
|
+
return;
|
|
1279
|
+
if (this.duplicatePlacement(stmt, element))
|
|
1106
1280
|
return;
|
|
1107
1281
|
this.placement.push({
|
|
1108
1282
|
statement: "size",
|
|
@@ -1340,12 +1514,17 @@ export class FlowAnalyzer {
|
|
|
1340
1514
|
if (extra)
|
|
1341
1515
|
return this.shapeError(stmt, form, extra);
|
|
1342
1516
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1517
|
+
// A free line's label pill moves as a relation's does.
|
|
1518
|
+
const line = this.elementsById.get(targetToken.text);
|
|
1519
|
+
if (line?.kind === "line")
|
|
1520
|
+
this.reference(targetToken, line.id, "element", "label", stmt);
|
|
1521
|
+
const relation = line?.kind === "line" ? undefined : this.resolveRelation(targetToken, "label", stmt);
|
|
1522
|
+
const labelled = line?.kind === "line" ? line.id : relation?.key;
|
|
1523
|
+
if (labelled === undefined || this.duplicatePlacement(stmt, labelled))
|
|
1345
1524
|
return;
|
|
1346
1525
|
this.placement.push({
|
|
1347
1526
|
statement: "label",
|
|
1348
|
-
target:
|
|
1527
|
+
target: labelled,
|
|
1349
1528
|
at: at.value,
|
|
1350
1529
|
offset: point,
|
|
1351
1530
|
range: this.range(stmt),
|
|
@@ -1353,6 +1532,16 @@ export class FlowAnalyzer {
|
|
|
1353
1532
|
node: stmt,
|
|
1354
1533
|
});
|
|
1355
1534
|
}
|
|
1535
|
+
/** A free line takes no `place` or `size`: its points are in its statement. Reports it. */
|
|
1536
|
+
lineRefused(id, statement, token) {
|
|
1537
|
+
if (this.elementsById.get(id)?.kind !== "line")
|
|
1538
|
+
return false;
|
|
1539
|
+
const m = messages();
|
|
1540
|
+
this.invalid(m.diagnostics.flow.lineHasPoints(id, statement), token, [
|
|
1541
|
+
m.hints.flow.linePointsWritten,
|
|
1542
|
+
]);
|
|
1543
|
+
return true;
|
|
1544
|
+
}
|
|
1356
1545
|
resolveElement(token, statement, stmt) {
|
|
1357
1546
|
const name = token.text;
|
|
1358
1547
|
if (this.elementsById.has(name)) {
|
|
@@ -1438,8 +1627,10 @@ export class FlowAnalyzer {
|
|
|
1438
1627
|
const resolved = this.resolveStyleTarget(targetToken, kind, stmt);
|
|
1439
1628
|
if (!resolved)
|
|
1440
1629
|
continue;
|
|
1630
|
+
// A free line takes a relation's properties, and none of a box's.
|
|
1631
|
+
const line = resolved.kind === "element" && this.elementsById.get(resolved.name)?.kind === "line";
|
|
1441
1632
|
const misfit = kind === "style"
|
|
1442
|
-
? resolved.kind === "relation"
|
|
1633
|
+
? resolved.kind === "relation" || line
|
|
1443
1634
|
? ELEMENT_ONLY_PROPERTIES
|
|
1444
1635
|
: RELATION_ONLY_PROPERTIES
|
|
1445
1636
|
: [];
|
|
@@ -1449,7 +1640,9 @@ export class FlowAnalyzer {
|
|
|
1449
1640
|
if (!token || name === "shape" || misplaced.has(name))
|
|
1450
1641
|
continue;
|
|
1451
1642
|
misplaced.add(name);
|
|
1452
|
-
this.invalid(
|
|
1643
|
+
this.invalid(line
|
|
1644
|
+
? messages().diagnostics.flow.notForLines(name, resolved.name)
|
|
1645
|
+
: messages().diagnostics.flow.notForTargets(name, resolved.kind === "relation", resolved.name), token, line ? [] : [targetHint(name)]);
|
|
1453
1646
|
}
|
|
1454
1647
|
// `shape` is refused per target: a block may list a node and a decision.
|
|
1455
1648
|
const shapeless = kind === "style" && tokens.shape ? this.shapeRefusal(resolved) : null;
|
|
@@ -1473,7 +1666,7 @@ export class FlowAnalyzer {
|
|
|
1473
1666
|
if (kind === "animate") {
|
|
1474
1667
|
// One effect per channel and target; different channels may share a target.
|
|
1475
1668
|
const effect = values.effect;
|
|
1476
|
-
if (effect && !this.animationTargetFits(effect, resolved, targetToken))
|
|
1669
|
+
if (effect && !this.animationTargetFits(effect, resolved, targetToken, stmt))
|
|
1477
1670
|
continue;
|
|
1478
1671
|
if (effect) {
|
|
1479
1672
|
const key = `${resolved.name}:${effect}`;
|
|
@@ -1597,19 +1790,27 @@ export class FlowAnalyzer {
|
|
|
1597
1790
|
return { profile: false, kind: element.kind };
|
|
1598
1791
|
}
|
|
1599
1792
|
/** Whether an effect can run on a target; reports `animate.invalid-target` when not. */
|
|
1600
|
-
animationTargetFits(effect, resolved, token) {
|
|
1793
|
+
animationTargetFits(effect, resolved, token, stmt) {
|
|
1601
1794
|
const element = resolved.kind === "element" ? this.elementsById.get(resolved.name) : undefined;
|
|
1602
1795
|
const m = messages().diagnostics.flow;
|
|
1603
1796
|
let reason = null;
|
|
1604
|
-
|
|
1797
|
+
// A free line runs effects as a relation does: it is a connector without ends.
|
|
1798
|
+
const connector = resolved.kind === "relation" || element?.kind === "line";
|
|
1799
|
+
if (effect === "flow" && !connector) {
|
|
1605
1800
|
reason = m.flowTarget;
|
|
1606
1801
|
}
|
|
1607
|
-
else if (effect === "trace" &&
|
|
1802
|
+
else if (effect === "trace" && !connector) {
|
|
1608
1803
|
reason = m.traceTarget;
|
|
1609
1804
|
}
|
|
1610
1805
|
else if (effect === "dash") {
|
|
1611
1806
|
if (resolved.kind === "event")
|
|
1612
1807
|
reason = m.dashOnEvent;
|
|
1808
|
+
else if (element && (element.kind === "note" || element.kind === "text")) {
|
|
1809
|
+
// A note or a text draws an outline when its styles say so, which a later statement may
|
|
1810
|
+
// write: checked once every block is read (`checkOutlinedDashes`).
|
|
1811
|
+
if (stmt)
|
|
1812
|
+
this.pendingDashes.push({ name: resolved.name, token, stmt });
|
|
1813
|
+
}
|
|
1613
1814
|
else if (element && NO_OUTLINE_KINDS.has(element.kind)) {
|
|
1614
1815
|
reason = m.dashWithoutOutline(resolved.name, element.kind);
|
|
1615
1816
|
}
|
|
@@ -1648,7 +1849,15 @@ export class FlowAnalyzer {
|
|
|
1648
1849
|
// -------------------------------------------------------------------- free helpers
|
|
1649
1850
|
/** The declarations and statements of flow and free diagrams, as their hints list them. */
|
|
1650
1851
|
const FLOW_DECLARATIONS = ["node", "decision", "data", "group", "note", "tag"];
|
|
1651
|
-
const FREE_DECLARATIONS = [
|
|
1852
|
+
const FREE_DECLARATIONS = [
|
|
1853
|
+
"box",
|
|
1854
|
+
"text",
|
|
1855
|
+
"icon",
|
|
1856
|
+
"image",
|
|
1857
|
+
"note",
|
|
1858
|
+
"line",
|
|
1859
|
+
"group",
|
|
1860
|
+
];
|
|
1652
1861
|
const FLOW_STATEMENTS = [
|
|
1653
1862
|
"direction",
|
|
1654
1863
|
"layout",
|
|
@@ -1675,6 +1884,7 @@ const FREE_PROFILE_ONLY = {
|
|
|
1675
1884
|
fontSize: true,
|
|
1676
1885
|
fontWeight: true,
|
|
1677
1886
|
textAlign: true,
|
|
1887
|
+
verticalAlign: true,
|
|
1678
1888
|
};
|
|
1679
1889
|
function isWord(arg, word) {
|
|
1680
1890
|
return singleIdent(arg)?.text === word;
|