@meltemi/diagrams-model 1.10.0 → 1.10.18
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.map +1 -1
- package/dist/c4.js +7 -0
- package/dist/c4.js.map +1 -1
- package/dist/diagram-types.d.ts.map +1 -1
- package/dist/diagram-types.js +6 -0
- 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 +23 -0
- package/dist/diff.js.map +1 -1
- package/dist/flow.d.ts +60 -10
- package/dist/flow.d.ts.map +1 -1
- package/dist/flow.js +405 -67
- package/dist/flow.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +29 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +19 -1
- 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 +67 -3
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +77 -9
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/es.d.ts.map +1 -1
- package/dist/messages/es.js +45 -8
- package/dist/messages/es.js.map +1 -1
- package/dist/messages/index.d.ts +36 -2
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/outline.d.ts +2 -2
- package/dist/outline.d.ts.map +1 -1
- package/dist/outline.js +9 -3
- package/dist/outline.js.map +1 -1
- package/dist/scenario.d.ts +15 -2
- package/dist/scenario.d.ts.map +1 -1
- package/dist/scenario.js +37 -1
- package/dist/scenario.js.map +1 -1
- package/dist/schema.d.ts +82 -11
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +106 -7
- package/dist/schema.js.map +1 -1
- package/dist/sequence.d.ts.map +1 -1
- package/dist/sequence.js +252 -12
- package/dist/sequence.js.map +1 -1
- package/dist/story.d.ts +8 -0
- package/dist/story.d.ts.map +1 -1
- package/dist/story.js +39 -12
- package/dist/story.js.map +1 -1
- package/dist/style-values.d.ts +9 -0
- package/dist/style-values.d.ts.map +1 -1
- package/dist/style-values.js +82 -27
- package/dist/style-values.js.map +1 -1
- package/dist/types.d.ts +94 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -3
package/dist/flow.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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_ARROWS, 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, LINE_REFUSED_PROPERTIES, LIST_LAYOUTS, LIST_PROPERTIES, MAIN_LAYER, PEN_PROPERTIES, PEN_REFUSED_PROPERTIES, PEN_WIDTH_RANGE, PORTS, PORT_SIDES, RELATION_ARROWS, RELATION_ONLY_PROPERTIES, STICKY_PROPERTIES, STICKY_REFUSED_PROPERTIES, TEXTLESS_KINDS, TEXT_PROPERTIES, diagramStatements, isFreeStyleProperty, profileTakesShape, takesShape, profileTakesRotation, takesRotation, } from "./schema.js";
|
|
3
3
|
import { relationAllowed, } from "./diagram-types.js";
|
|
4
4
|
import { messages } from "./messages/index.js";
|
|
5
|
-
import { StyleDefinitions, checkArg, describeArg, headerHasError, readClasses, readPropertyBlock, readStatementValue, refuseFreeProperties, singleIdent, spanOf, targetHint, } from "./style-values.js";
|
|
5
|
+
import { StyleDefinitions, argumentOf, checkArg, describeArg, describeSpec, headerHasError, readClasses, readPropertyBlock, readStatementValue, refuseFreeProperties, singleIdent, spanOf, TAG_FORM, targetHint, } from "./style-values.js";
|
|
6
6
|
import { outlined } from "./outline.js";
|
|
7
7
|
/** Element kinds drawn without an outline, which a moving dash cannot follow. */
|
|
8
8
|
const NO_OUTLINE_KINDS = new Set([
|
|
@@ -153,6 +153,7 @@ export class FlowAnalyzer {
|
|
|
153
153
|
routing: "auto",
|
|
154
154
|
layers: [MAIN_LAYER],
|
|
155
155
|
autonumber: null,
|
|
156
|
+
spacing: null,
|
|
156
157
|
};
|
|
157
158
|
}
|
|
158
159
|
/** Sequence diagrams: the ordered events, their index and the scenarios. */
|
|
@@ -264,7 +265,8 @@ export class FlowAnalyzer {
|
|
|
264
265
|
}
|
|
265
266
|
/**
|
|
266
267
|
* 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,
|
|
268
|
+
* diagram gives a `fill`, `stroke` or `pattern` has none, nor a sticky without a `stroke`, and
|
|
269
|
+
* the dash is refused and dropped.
|
|
268
270
|
*/
|
|
269
271
|
checkOutlinedDashes() {
|
|
270
272
|
const source = {
|
|
@@ -276,7 +278,13 @@ export class FlowAnalyzer {
|
|
|
276
278
|
const element = this.elementsById.get(name);
|
|
277
279
|
if (!element || outlined(source, element))
|
|
278
280
|
continue;
|
|
279
|
-
this.error("animate.invalid-target", messages().diagnostics.flow.dashNeedsOutline(name, element.kind), token, {
|
|
281
|
+
this.error("animate.invalid-target", messages().diagnostics.flow.dashNeedsOutline(name, element.kind), token, {
|
|
282
|
+
hints: [
|
|
283
|
+
element.kind === "sticky"
|
|
284
|
+
? messages().hints.flow.dashStroke
|
|
285
|
+
: messages().hints.flow.dashOutline,
|
|
286
|
+
],
|
|
287
|
+
});
|
|
280
288
|
const at = this.animations.findIndex((a) => a.node === stmt && a.target === name && a.effect === "dash");
|
|
281
289
|
if (at >= 0)
|
|
282
290
|
this.animations.splice(at, 1);
|
|
@@ -363,17 +371,32 @@ export class FlowAnalyzer {
|
|
|
363
371
|
keyword in this.profile.groupProperties) {
|
|
364
372
|
continue;
|
|
365
373
|
}
|
|
374
|
+
// `layout`, `spacing` and `align` make a list of a free group; other profiles have none.
|
|
375
|
+
if (isListProperty(keyword)) {
|
|
376
|
+
if (!(keyword in this.profile.groupProperties)) {
|
|
377
|
+
const m = messages();
|
|
378
|
+
this.invalid(m.diagnostics.flow.listInProfile(keyword, this.profile.name), stmt.keyword, [m.hints.flow.listsInFree]);
|
|
379
|
+
}
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
366
382
|
const m = messages();
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
383
|
+
const misplaced = this.misplacedHint(keyword);
|
|
384
|
+
const hints = misplaced
|
|
385
|
+
? [misplaced]
|
|
386
|
+
: otherKind
|
|
387
|
+
? [otherKind]
|
|
388
|
+
: diagramStatements(this.profile).includes(keyword)
|
|
389
|
+
? [m.hints.flow.diagramLevel(keyword)]
|
|
390
|
+
: [m.hints.flow.groupContents];
|
|
372
391
|
const candidates = otherKind
|
|
373
392
|
? this.declarationWords()
|
|
374
|
-
: diagramStatements(this.profile).includes(keyword)
|
|
393
|
+
: misplaced || diagramStatements(this.profile).includes(keyword)
|
|
375
394
|
? []
|
|
376
|
-
: [
|
|
395
|
+
: [
|
|
396
|
+
"description",
|
|
397
|
+
"url",
|
|
398
|
+
...LIST_PROPERTIES.filter((name) => name in this.profile.groupProperties),
|
|
399
|
+
];
|
|
377
400
|
this.error("prop.unknown", m.diagnostics.flow.unknownInGroup(keyword), stmt.keyword, {
|
|
378
401
|
hints,
|
|
379
402
|
data: { candidates },
|
|
@@ -392,6 +415,14 @@ export class FlowAnalyzer {
|
|
|
392
415
|
this.error("prop.unknown", messages().diagnostics.flow.unknownInDiagram(keyword, this.profile.name), stmt.keyword, { hints, data: { candidates: this.statementCandidates(keyword) } });
|
|
393
416
|
}
|
|
394
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* The profile's own hint for a statement written outside the block it belongs in (a sequence
|
|
420
|
+
* `tag` outside a message or participant block), in the interface language; undefined for every
|
|
421
|
+
* other statement.
|
|
422
|
+
*/
|
|
423
|
+
misplacedHint(_keyword) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
395
426
|
/** Hints for an unknown statement at the diagram level, in the interface language. */
|
|
396
427
|
statementHints(keyword, stmt) {
|
|
397
428
|
const otherKind = this.otherProfileKind(keyword);
|
|
@@ -402,7 +433,7 @@ export class FlowAnalyzer {
|
|
|
402
433
|
if (stmt.args.length === 0 && !stmt.block) {
|
|
403
434
|
hints.push(m.declareElement(keyword, this.profile.shorthand.box));
|
|
404
435
|
}
|
|
405
|
-
hints.push(this.free ? m.freeStatements : m.flowStatements);
|
|
436
|
+
hints.push(this.free ? m.freeStatements(FREE_DECLARATIONS) : m.flowStatements);
|
|
406
437
|
return hints;
|
|
407
438
|
}
|
|
408
439
|
/** The statements an unknown one at the diagram level may have meant, for fixes. */
|
|
@@ -427,7 +458,7 @@ export class FlowAnalyzer {
|
|
|
427
458
|
return undefined;
|
|
428
459
|
}
|
|
429
460
|
const m = messages().hints.flow;
|
|
430
|
-
return this.free ? m.freeDeclarations : m.flowDeclarations(keyword);
|
|
461
|
+
return this.free ? m.freeDeclarations(FREE_DECLARATIONS) : m.flowDeclarations(keyword);
|
|
431
462
|
}
|
|
432
463
|
// ------------------------------------------------------------------ elements
|
|
433
464
|
declare(stmt, groupPath) {
|
|
@@ -449,6 +480,8 @@ export class FlowAnalyzer {
|
|
|
449
480
|
external: false,
|
|
450
481
|
shape: data ? "cylinder" : null,
|
|
451
482
|
line: null,
|
|
483
|
+
pen: null,
|
|
484
|
+
list: null,
|
|
452
485
|
attachedTo: null,
|
|
453
486
|
attachedToRange: null,
|
|
454
487
|
typeKind: null,
|
|
@@ -485,6 +518,10 @@ export class FlowAnalyzer {
|
|
|
485
518
|
}
|
|
486
519
|
if (kind === "line" && !declaredKind)
|
|
487
520
|
return this.declareLine(stmt, groupPath);
|
|
521
|
+
if (kind === "pen" && !declaredKind)
|
|
522
|
+
return this.declarePen(stmt, groupPath);
|
|
523
|
+
if (kind === "sticky" && !declaredKind)
|
|
524
|
+
return this.declareSticky(stmt, groupPath);
|
|
488
525
|
const [idArg, labelArg, ...extra] = stmt.args;
|
|
489
526
|
const idToken = singleIdent(idArg);
|
|
490
527
|
if (!idToken) {
|
|
@@ -546,11 +583,14 @@ export class FlowAnalyzer {
|
|
|
546
583
|
}
|
|
547
584
|
}
|
|
548
585
|
const block = kind === "group"
|
|
549
|
-
? { ...this.groupBlock(stmt.block, idToken), ports: [], values: {} }
|
|
550
|
-
:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
586
|
+
? { ...this.groupBlock(stmt.block, idToken), ports: [], tags: [], values: {} }
|
|
587
|
+
: {
|
|
588
|
+
...this.elementBlock(stmt.block, declaredKind
|
|
589
|
+
? { ...this.profile.elementProperties, ...tableOf(declaredKind.properties) }
|
|
590
|
+
: this.profile.elementProperties, "Element", idToken),
|
|
591
|
+
list: null,
|
|
592
|
+
};
|
|
593
|
+
const { values: blockValues, list, ...blockFields } = block;
|
|
554
594
|
const added = this.addElement({
|
|
555
595
|
id: idToken.text,
|
|
556
596
|
kind,
|
|
@@ -565,6 +605,8 @@ export class FlowAnalyzer {
|
|
|
565
605
|
external: false,
|
|
566
606
|
shape: null,
|
|
567
607
|
line: null,
|
|
608
|
+
pen: null,
|
|
609
|
+
list,
|
|
568
610
|
attachedTo: attachment?.target ?? null,
|
|
569
611
|
attachedToRange: attachment ? this.range(attachment.arg) : null,
|
|
570
612
|
typeKind,
|
|
@@ -643,6 +685,123 @@ export class FlowAnalyzer {
|
|
|
643
685
|
external: false,
|
|
644
686
|
shape: null,
|
|
645
687
|
line: { from, to, via },
|
|
688
|
+
pen: null,
|
|
689
|
+
list: null,
|
|
690
|
+
attachedTo: null,
|
|
691
|
+
attachedToRange: null,
|
|
692
|
+
typeKind: null,
|
|
693
|
+
data: {},
|
|
694
|
+
groupPath,
|
|
695
|
+
range: this.range(stmt),
|
|
696
|
+
idRange: this.range(idToken),
|
|
697
|
+
idToken,
|
|
698
|
+
node: stmt,
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* `pen <id> [(x, y), (x, y), ...] [{ ... }]`: a freehand stroke through its points, in the
|
|
703
|
+
* interior coordinates of its innermost group as a line's are. One point is a dot; decimals are
|
|
704
|
+
* kept. A pen has no label.
|
|
705
|
+
*/
|
|
706
|
+
declarePen(stmt, groupPath) {
|
|
707
|
+
const form = "`pen <id> [(x, y), (x, y), ...]`";
|
|
708
|
+
const m = messages();
|
|
709
|
+
const [idArg, list, extra] = stmt.args;
|
|
710
|
+
const idToken = singleIdent(idArg);
|
|
711
|
+
if (!idToken) {
|
|
712
|
+
this.invalid(m.diagnostics.common.expected(form), idArg ?? stmt.keyword, [
|
|
713
|
+
m.hints.example("pen arrow [(40, 60), (90.5, 58), (140, 70)]"),
|
|
714
|
+
]);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const hints = [m.hints.flow.form(form)];
|
|
718
|
+
if (list?.kind === "StringLiteral") {
|
|
719
|
+
return this.invalid(m.diagnostics.flow.penNoLabel(idToken.text), list, hints);
|
|
720
|
+
}
|
|
721
|
+
if (list?.kind !== "List" || list.items.length === 0) {
|
|
722
|
+
return this.invalid(m.diagnostics.flow.penPoints, list ?? stmt, hints);
|
|
723
|
+
}
|
|
724
|
+
const points = [];
|
|
725
|
+
for (const item of list.items) {
|
|
726
|
+
const point = readPoint(item);
|
|
727
|
+
if (!point)
|
|
728
|
+
return this.invalid(m.diagnostics.flow.penPoints, item, hints);
|
|
729
|
+
points.push(point);
|
|
730
|
+
}
|
|
731
|
+
if (extra)
|
|
732
|
+
return this.invalid(m.diagnostics.flow.unexpectedValue(form), extra, hints);
|
|
733
|
+
// A stroke has no sides for ports: `port` in its block is unknown, as in any block without it.
|
|
734
|
+
const { values: _values, ...blockFields } = this.elementBlock(stmt.block, this.profile.elementProperties, "Element", idToken, false);
|
|
735
|
+
this.addElement({
|
|
736
|
+
id: idToken.text,
|
|
737
|
+
kind: "pen",
|
|
738
|
+
label: "",
|
|
739
|
+
implicit: false,
|
|
740
|
+
...blockFields,
|
|
741
|
+
source: null,
|
|
742
|
+
sourceRange: null,
|
|
743
|
+
labelWritten: false,
|
|
744
|
+
entity: null,
|
|
745
|
+
technology: null,
|
|
746
|
+
external: false,
|
|
747
|
+
shape: null,
|
|
748
|
+
line: null,
|
|
749
|
+
pen: { points },
|
|
750
|
+
list: null,
|
|
751
|
+
attachedTo: null,
|
|
752
|
+
attachedToRange: null,
|
|
753
|
+
typeKind: null,
|
|
754
|
+
data: {},
|
|
755
|
+
groupPath,
|
|
756
|
+
range: this.range(stmt),
|
|
757
|
+
idRange: this.range(idToken),
|
|
758
|
+
idToken,
|
|
759
|
+
node: stmt,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* `sticky <id> "Text" [{ ... }]`: a square card whose text wraps to fit it. The text is required,
|
|
764
|
+
* `\n` breaking its lines; the block takes a box's: `description`, `class`, `layer`, `link`, `url`
|
|
765
|
+
* and ports. A sticky without its text is still declared, so that its uses resolve.
|
|
766
|
+
*/
|
|
767
|
+
declareSticky(stmt, groupPath) {
|
|
768
|
+
const form = '`sticky <id> "Text"`';
|
|
769
|
+
const m = messages();
|
|
770
|
+
const [idArg, textArg, extra] = stmt.args;
|
|
771
|
+
const idToken = singleIdent(idArg);
|
|
772
|
+
if (!idToken) {
|
|
773
|
+
this.invalid(m.diagnostics.common.expected(form), idArg ?? stmt.keyword, [
|
|
774
|
+
m.hints.example('sticky idea "Cache the catalogue"'),
|
|
775
|
+
]);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (textArg && textArg.kind !== "StringLiteral") {
|
|
779
|
+
return this.invalid(m.diagnostics.flow.quotedSource("text", idToken.text), textArg);
|
|
780
|
+
}
|
|
781
|
+
if (extra)
|
|
782
|
+
return this.invalid(m.diagnostics.flow.valueAfter("text", form), extra);
|
|
783
|
+
if (!textArg) {
|
|
784
|
+
this.invalid(m.diagnostics.flow.needsSource("sticky", idToken.text, form), stmt, [
|
|
785
|
+
m.hints.flow.form(form),
|
|
786
|
+
]);
|
|
787
|
+
}
|
|
788
|
+
const { values: _values, ...blockFields } = this.elementBlock(stmt.block, this.profile.elementProperties, "Element", idToken);
|
|
789
|
+
this.addElement({
|
|
790
|
+
id: idToken.text,
|
|
791
|
+
kind: "sticky",
|
|
792
|
+
label: textArg ? textArg.value : idToken.text,
|
|
793
|
+
implicit: false,
|
|
794
|
+
...blockFields,
|
|
795
|
+
source: null,
|
|
796
|
+
sourceRange: null,
|
|
797
|
+
labelWritten: textArg !== undefined,
|
|
798
|
+
entity: null,
|
|
799
|
+
technology: null,
|
|
800
|
+
external: false,
|
|
801
|
+
shape: null,
|
|
802
|
+
line: null,
|
|
803
|
+
pen: null,
|
|
804
|
+
list: null,
|
|
646
805
|
attachedTo: null,
|
|
647
806
|
attachedToRange: null,
|
|
648
807
|
typeKind: null,
|
|
@@ -694,7 +853,7 @@ export class FlowAnalyzer {
|
|
|
694
853
|
data: {
|
|
695
854
|
ids: [target],
|
|
696
855
|
candidates: this.elements
|
|
697
|
-
.filter((e) => e.id !== note && e.kind !== "line")
|
|
856
|
+
.filter((e) => e.id !== note && e.kind !== "line" && e.kind !== "pen")
|
|
698
857
|
.map((e) => e.id),
|
|
699
858
|
},
|
|
700
859
|
});
|
|
@@ -721,21 +880,36 @@ export class FlowAnalyzer {
|
|
|
721
880
|
return true;
|
|
722
881
|
}
|
|
723
882
|
/**
|
|
724
|
-
* Reads an element or relation block: its description, applied classes, named ports,
|
|
725
|
-
* `layer` and `link` statements the profile accepts
|
|
883
|
+
* Reads an element or relation block: its description, applied classes, named ports, the
|
|
884
|
+
* `layer` and `link` statements the profile accepts, and its `tag` statements where the table
|
|
885
|
+
* takes them (sequence messages and participants).
|
|
726
886
|
*/
|
|
727
|
-
elementBlock(block, table, owner, ownerToken
|
|
887
|
+
elementBlock(block, table, owner, ownerToken,
|
|
888
|
+
/** False for an element without ports (a pen): `port` is then unknown in its block. */
|
|
889
|
+
takesPorts = true) {
|
|
728
890
|
const empty = { layer: null, layerRange: null, link: null };
|
|
729
891
|
if (!block)
|
|
730
|
-
return {
|
|
892
|
+
return {
|
|
893
|
+
description: null,
|
|
894
|
+
url: null,
|
|
895
|
+
classes: [],
|
|
896
|
+
ports: [],
|
|
897
|
+
tags: [],
|
|
898
|
+
values: {},
|
|
899
|
+
...empty,
|
|
900
|
+
};
|
|
731
901
|
const ports = [];
|
|
902
|
+
const tags = [];
|
|
732
903
|
const extras = { ...empty };
|
|
733
904
|
const seen = new Map();
|
|
905
|
+
const portable = owner === "Element" && takesPorts && this.profile.ports.length > 0;
|
|
906
|
+
// Tags repeat, each an annotation of the owner: read apart, never a `prop.duplicate` keyword.
|
|
907
|
+
const tagged = table.tag?.kind === "tag";
|
|
734
908
|
const rest = block.statements.filter((stmt) => {
|
|
735
909
|
if (stmt.kind !== "KeywordStatement")
|
|
736
910
|
return true;
|
|
737
911
|
const keyword = stmt.keyword.text;
|
|
738
|
-
if (
|
|
912
|
+
if (portable && keyword === "port") {
|
|
739
913
|
if (!headerHasError(stmt))
|
|
740
914
|
this.port(stmt, ports);
|
|
741
915
|
return false;
|
|
@@ -745,22 +919,69 @@ export class FlowAnalyzer {
|
|
|
745
919
|
this.extra(stmt, extras, seen, ownerToken);
|
|
746
920
|
return false;
|
|
747
921
|
}
|
|
922
|
+
if (tagged && keyword === "tag") {
|
|
923
|
+
if (!headerHasError(stmt))
|
|
924
|
+
this.tag(stmt, tags);
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
748
927
|
return true;
|
|
749
928
|
});
|
|
750
|
-
const words = Object.keys(table).concat(
|
|
751
|
-
const { values, classes } = this.readProperties({ ...block, statements: rest }, table, () =>
|
|
752
|
-
const m = messages().hints.flow;
|
|
753
|
-
return owner === "Element" ? m.elementBlock(words) : m.relationBlock(words);
|
|
754
|
-
}, owner.toLowerCase(), words);
|
|
929
|
+
const words = Object.keys(table).concat(portable ? ["port"] : []);
|
|
930
|
+
const { values, classes } = this.readProperties({ ...block, statements: rest }, table, () => this.blockHint(owner, words), owner.toLowerCase(), words);
|
|
755
931
|
return {
|
|
756
932
|
description: typeof values.description === "string" ? values.description : null,
|
|
757
933
|
url: typeof values.url === "string" ? values.url : null,
|
|
758
934
|
classes,
|
|
759
935
|
ports,
|
|
936
|
+
tags,
|
|
760
937
|
values,
|
|
761
938
|
...extras,
|
|
762
939
|
};
|
|
763
940
|
}
|
|
941
|
+
/** The hint of an unknown statement in an element or relation block, in the interface words. */
|
|
942
|
+
blockHint(owner, words) {
|
|
943
|
+
const m = messages().hints.flow;
|
|
944
|
+
return owner === "Element" ? m.elementBlock(words) : m.relationBlock(words);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Reads `tag "Text"` into the tags of a block: one line of text, not empty, and not a text the
|
|
948
|
+
* owner already has.
|
|
949
|
+
*/
|
|
950
|
+
tag(stmt, tags) {
|
|
951
|
+
const m = messages();
|
|
952
|
+
const form = [m.hints.example('tag "PKCE"')];
|
|
953
|
+
if (stmt.block || stmt.chain) {
|
|
954
|
+
this.invalid(m.diagnostics.common.expected(TAG_FORM), stmt.block ?? stmt.chain, form);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const [arg, extra] = stmt.args;
|
|
958
|
+
// The first value that breaks the form: an id before the text, a number, a second string.
|
|
959
|
+
const wrong = !arg ? stmt.keyword : arg.kind !== "StringLiteral" ? arg : extra;
|
|
960
|
+
if (!arg || wrong) {
|
|
961
|
+
this.invalid(m.diagnostics.common.expected(TAG_FORM), wrong ?? stmt.keyword, form);
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const checked = checkArg(arg, { kind: "tag" }, "tag");
|
|
965
|
+
if (!checked.ok || typeof checked.value !== "string") {
|
|
966
|
+
if (!checked.ok)
|
|
967
|
+
this.invalid(checked.message, arg, checked.hints);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const text = checked.value;
|
|
971
|
+
const first = tags.find((t) => t.text === text);
|
|
972
|
+
if (first) {
|
|
973
|
+
this.error("prop.duplicate", m.diagnostics.sequence.tagRepeated(text), arg, {
|
|
974
|
+
related: [
|
|
975
|
+
{
|
|
976
|
+
message: m.related.firstWritten,
|
|
977
|
+
at: { start: first.textRange.start.offset, end: first.textRange.end.offset },
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
});
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
tags.push({ text, range: this.range(stmt), textRange: this.range(arg) });
|
|
984
|
+
}
|
|
764
985
|
/** Reads `layer <name>` or `link <diagram>.<element>` into the extras of a block. */
|
|
765
986
|
extra(stmt, extras, seen, ownerToken) {
|
|
766
987
|
const keyword = stmt.keyword.text;
|
|
@@ -859,8 +1080,8 @@ export class FlowAnalyzer {
|
|
|
859
1080
|
});
|
|
860
1081
|
}
|
|
861
1082
|
/**
|
|
862
|
-
* Reads the `description`, `class`, `layer` and `link` properties of a group block
|
|
863
|
-
* classified.
|
|
1083
|
+
* Reads the `description`, `class`, `layer` and `link` properties of a group block, and in free
|
|
1084
|
+
* diagrams the `layout`, `spacing` and `align` of a list; children are classified.
|
|
864
1085
|
*/
|
|
865
1086
|
groupBlock(block, ownerToken) {
|
|
866
1087
|
let description = null;
|
|
@@ -868,8 +1089,9 @@ export class FlowAnalyzer {
|
|
|
868
1089
|
let classes = [];
|
|
869
1090
|
const extras = { layer: null, layerRange: null, link: null };
|
|
870
1091
|
if (!block)
|
|
871
|
-
return { description, url, classes, ...extras };
|
|
1092
|
+
return { description, url, classes, list: null, ...extras };
|
|
872
1093
|
const seen = new Map();
|
|
1094
|
+
const listed = {};
|
|
873
1095
|
for (const stmt of block.statements) {
|
|
874
1096
|
if (stmt.kind !== "KeywordStatement" || headerHasError(stmt))
|
|
875
1097
|
continue;
|
|
@@ -878,8 +1100,14 @@ export class FlowAnalyzer {
|
|
|
878
1100
|
this.extra(stmt, extras, seen, ownerToken);
|
|
879
1101
|
continue;
|
|
880
1102
|
}
|
|
881
|
-
|
|
1103
|
+
// Other profiles refuse the list properties when the block is classified.
|
|
1104
|
+
const listProperty = isListProperty(name) && name in this.profile.groupProperties;
|
|
1105
|
+
if (!listProperty &&
|
|
1106
|
+
name !== "description" &&
|
|
1107
|
+
name !== "url" &&
|
|
1108
|
+
!(name === "class" && !stmt.block)) {
|
|
882
1109
|
continue;
|
|
1110
|
+
}
|
|
883
1111
|
const first = seen.get(name);
|
|
884
1112
|
if (first) {
|
|
885
1113
|
const m = messages();
|
|
@@ -889,6 +1117,18 @@ export class FlowAnalyzer {
|
|
|
889
1117
|
continue;
|
|
890
1118
|
}
|
|
891
1119
|
seen.set(name, stmt.keyword);
|
|
1120
|
+
if (listProperty) {
|
|
1121
|
+
const value = this.listValue(stmt, name);
|
|
1122
|
+
if (value === undefined)
|
|
1123
|
+
continue;
|
|
1124
|
+
if (name === "layout")
|
|
1125
|
+
listed.layout = value;
|
|
1126
|
+
else if (name === "spacing")
|
|
1127
|
+
listed.spacing = value;
|
|
1128
|
+
else
|
|
1129
|
+
listed.align = value;
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
892
1132
|
if (name === "class") {
|
|
893
1133
|
classes = readClasses(stmt, this.lines, this.report) ?? [];
|
|
894
1134
|
continue;
|
|
@@ -903,7 +1143,45 @@ export class FlowAnalyzer {
|
|
|
903
1143
|
if (typeof value === "string")
|
|
904
1144
|
description = value;
|
|
905
1145
|
}
|
|
906
|
-
return { description, url, classes, ...extras };
|
|
1146
|
+
return { description, url, classes, list: this.listOf(listed, seen), ...extras };
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Reads `layout column|row`, `spacing <n>` or `align start|center|end|stretch` of a free group
|
|
1150
|
+
* block. `layout auto`, `assisted` and `manual` are the diagram's statement, which the hint says.
|
|
1151
|
+
*/
|
|
1152
|
+
listValue(stmt, name) {
|
|
1153
|
+
const [arg] = stmt.args;
|
|
1154
|
+
const word = singleIdent(arg);
|
|
1155
|
+
if (name === "layout" &&
|
|
1156
|
+
word &&
|
|
1157
|
+
stmt.args.length === 1 &&
|
|
1158
|
+
!stmt.block &&
|
|
1159
|
+
!stmt.chain &&
|
|
1160
|
+
LAYOUT_MODES.includes(word.text)) {
|
|
1161
|
+
const m = messages();
|
|
1162
|
+
this.invalid(m.diagnostics.values.invalidEnum(describeArg(arg), name, LIST_LAYOUTS), arg, [
|
|
1163
|
+
m.hints.flow.groupLayout(word.text),
|
|
1164
|
+
]);
|
|
1165
|
+
return undefined;
|
|
1166
|
+
}
|
|
1167
|
+
return this.readValue(stmt, this.profile.groupProperties[name], name);
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* The list a group block writes, or null without a valid `layout`. `spacing` and `align` without
|
|
1171
|
+
* a `layout` are reported on their keywords.
|
|
1172
|
+
*/
|
|
1173
|
+
listOf(listed, seen) {
|
|
1174
|
+
if (!seen.has("layout")) {
|
|
1175
|
+
const m = messages();
|
|
1176
|
+
for (const name of ["spacing", "align"]) {
|
|
1177
|
+
const token = seen.get(name);
|
|
1178
|
+
if (token)
|
|
1179
|
+
this.invalid(m.diagnostics.flow.listNeedsLayout(name), token, [m.hints.flow.listLayout]);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (listed.layout === undefined)
|
|
1183
|
+
return null;
|
|
1184
|
+
return { layout: listed.layout, spacing: listed.spacing ?? null, align: listed.align ?? null };
|
|
907
1185
|
}
|
|
908
1186
|
// ------------------------------------------------------------------ relations
|
|
909
1187
|
relation(stmt, groupPath) {
|
|
@@ -941,7 +1219,7 @@ export class FlowAnalyzer {
|
|
|
941
1219
|
if (this.profile.arrows[stmt.arrow.text] === undefined) {
|
|
942
1220
|
this.invalid(messages().diagnostics.flow.arrowUnavailable(stmt.arrow.text, Object.keys(this.profile.arrows)), stmt.arrow);
|
|
943
1221
|
}
|
|
944
|
-
const { description, classes, layer, layerRange, values } = this.elementBlock(stmt.block, this.profile.relationProperties, "Relation", undefined);
|
|
1222
|
+
const { description, classes, tags, layer, layerRange, values } = this.elementBlock(stmt.block, this.profile.relationProperties, "Relation", undefined);
|
|
945
1223
|
const relation = {
|
|
946
1224
|
key: "",
|
|
947
1225
|
id,
|
|
@@ -951,6 +1229,7 @@ export class FlowAnalyzer {
|
|
|
951
1229
|
heads: RELATION_ARROWS[stmt.arrow.text]?.heads ?? "end",
|
|
952
1230
|
twoWay: RELATION_ARROWS[stmt.arrow.text]?.heads === "both",
|
|
953
1231
|
async: values.kind === "async",
|
|
1232
|
+
returns: null,
|
|
954
1233
|
technology: null,
|
|
955
1234
|
sources: [],
|
|
956
1235
|
derived: false,
|
|
@@ -959,6 +1238,7 @@ export class FlowAnalyzer {
|
|
|
959
1238
|
assumption: typeof values.assumption === "string" ? values.assumption : null,
|
|
960
1239
|
data: this.declared ? dataOf(values, this.declared.relations.properties) : {},
|
|
961
1240
|
classes,
|
|
1241
|
+
tags,
|
|
962
1242
|
layer,
|
|
963
1243
|
layerRange,
|
|
964
1244
|
groupPath,
|
|
@@ -997,6 +1277,7 @@ export class FlowAnalyzer {
|
|
|
997
1277
|
description: null,
|
|
998
1278
|
url: null,
|
|
999
1279
|
classes: [],
|
|
1280
|
+
tags: [],
|
|
1000
1281
|
ports: [],
|
|
1001
1282
|
layer: null,
|
|
1002
1283
|
layerRange: null,
|
|
@@ -1009,6 +1290,8 @@ export class FlowAnalyzer {
|
|
|
1009
1290
|
external: false,
|
|
1010
1291
|
shape: null,
|
|
1011
1292
|
line: null,
|
|
1293
|
+
pen: null,
|
|
1294
|
+
list: null,
|
|
1012
1295
|
attachedTo: null,
|
|
1013
1296
|
attachedToRange: null,
|
|
1014
1297
|
typeKind: null,
|
|
@@ -1092,6 +1375,9 @@ export class FlowAnalyzer {
|
|
|
1092
1375
|
case "autonumber":
|
|
1093
1376
|
this.properties.autonumber = Number(value);
|
|
1094
1377
|
break;
|
|
1378
|
+
case "spacing":
|
|
1379
|
+
this.properties.spacing = Number(value);
|
|
1380
|
+
break;
|
|
1095
1381
|
}
|
|
1096
1382
|
}
|
|
1097
1383
|
/** Reads `layers a, b, c`: back to front, `main` last unless listed. */
|
|
@@ -1218,8 +1504,9 @@ export class FlowAnalyzer {
|
|
|
1218
1504
|
}
|
|
1219
1505
|
return true;
|
|
1220
1506
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
1507
|
+
/** Whether a placement of the same kind already names the target (on the same side, if any). */
|
|
1508
|
+
duplicatePlacement(stmt, target, side = "") {
|
|
1509
|
+
const key = `${stmt.keyword.text}:${target}${side === "" ? "" : `:${side}`}`;
|
|
1223
1510
|
const first = this.placementSeen.get(key);
|
|
1224
1511
|
if (first) {
|
|
1225
1512
|
const m = messages();
|
|
@@ -1532,14 +1819,25 @@ export class FlowAnalyzer {
|
|
|
1532
1819
|
node: stmt,
|
|
1533
1820
|
});
|
|
1534
1821
|
}
|
|
1535
|
-
/**
|
|
1822
|
+
/**
|
|
1823
|
+
* A free line or a pen takes no `place` or `size` (and a pen no `rotation`): its points are in its
|
|
1824
|
+
* statement. Reports it.
|
|
1825
|
+
*/
|
|
1536
1826
|
lineRefused(id, statement, token) {
|
|
1537
|
-
|
|
1827
|
+
const kind = this.elementsById.get(id)?.kind;
|
|
1828
|
+
if (kind !== "line" && kind !== "pen")
|
|
1538
1829
|
return false;
|
|
1539
1830
|
const m = messages();
|
|
1540
|
-
|
|
1541
|
-
m.
|
|
1542
|
-
|
|
1831
|
+
if (kind === "pen") {
|
|
1832
|
+
this.invalid(m.diagnostics.flow.penHasPoints(id, statement), token, [
|
|
1833
|
+
m.hints.flow.penPointsWritten,
|
|
1834
|
+
]);
|
|
1835
|
+
}
|
|
1836
|
+
else {
|
|
1837
|
+
this.invalid(m.diagnostics.flow.lineHasPoints(id, statement), token, [
|
|
1838
|
+
m.hints.flow.linePointsWritten,
|
|
1839
|
+
]);
|
|
1840
|
+
}
|
|
1543
1841
|
return true;
|
|
1544
1842
|
}
|
|
1545
1843
|
resolveElement(token, statement, stmt) {
|
|
@@ -1599,7 +1897,7 @@ export class FlowAnalyzer {
|
|
|
1599
1897
|
const table = kind === "style" ? this.profile.styleProperties : this.profile.animateProperties;
|
|
1600
1898
|
const shapes = profileTakesShape(this.profile);
|
|
1601
1899
|
const words = kind === "style"
|
|
1602
|
-
? Object.keys(table).filter((p) => p === "shape" ? shapes : this.free || !(p
|
|
1900
|
+
? Object.keys(table).filter((p) => p === "shape" ? shapes : this.free || !isFreeStyleProperty(p))
|
|
1603
1901
|
: ["effect", "duration", "repeat"];
|
|
1604
1902
|
const hint = () => {
|
|
1605
1903
|
const m = messages().hints.flow;
|
|
@@ -1627,22 +1925,57 @@ export class FlowAnalyzer {
|
|
|
1627
1925
|
const resolved = this.resolveStyleTarget(targetToken, kind, stmt);
|
|
1628
1926
|
if (!resolved)
|
|
1629
1927
|
continue;
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1928
|
+
const element = resolved.kind === "element" ? this.elementsById.get(resolved.name) : undefined;
|
|
1929
|
+
// A free line takes a relation's properties, and none of a box's but `opacity`; a pen takes
|
|
1930
|
+
// its stroke's (`PEN_PROPERTIES`) and refuses the rest; a sticky takes a card's
|
|
1931
|
+
// (`STICKY_PROPERTIES`).
|
|
1932
|
+
const line = element?.kind === "line";
|
|
1933
|
+
const pen = element?.kind === "pen";
|
|
1934
|
+
const sticky = element?.kind === "sticky";
|
|
1935
|
+
// Icons and images draw no text: the text properties do not apply to them.
|
|
1936
|
+
const textless = element !== undefined && TEXTLESS_KINDS.includes(element.kind);
|
|
1937
|
+
const misfit = kind !== "style"
|
|
1938
|
+
? []
|
|
1939
|
+
: resolved.kind === "relation"
|
|
1634
1940
|
? ELEMENT_ONLY_PROPERTIES
|
|
1635
|
-
:
|
|
1636
|
-
|
|
1941
|
+
: line
|
|
1942
|
+
? LINE_REFUSED_PROPERTIES
|
|
1943
|
+
: pen
|
|
1944
|
+
? [...PEN_REFUSED_PROPERTIES, "rotation"]
|
|
1945
|
+
: sticky
|
|
1946
|
+
? STICKY_REFUSED_PROPERTIES
|
|
1947
|
+
: textless
|
|
1948
|
+
? [...RELATION_ONLY_PROPERTIES, ...TEXT_PROPERTIES]
|
|
1949
|
+
: RELATION_ONLY_PROPERTIES;
|
|
1637
1950
|
for (const name of misfit) {
|
|
1638
1951
|
const token = tokens[name];
|
|
1639
|
-
// `shape` is reported below, with the kinds that take it.
|
|
1640
|
-
if (!token || name === "shape" || misplaced.has(name))
|
|
1952
|
+
// `shape` is reported below, with the kinds that take it; a pen names what it takes.
|
|
1953
|
+
if (!token || (name === "shape" && !pen) || misplaced.has(name))
|
|
1641
1954
|
continue;
|
|
1642
1955
|
misplaced.add(name);
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1956
|
+
if (pen && name === "rotation") {
|
|
1957
|
+
this.lineRefused(resolved.name, "rotation", token);
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
const m = messages();
|
|
1961
|
+
this.invalid(pen
|
|
1962
|
+
? m.diagnostics.flow.notForPens(name, resolved.name, PEN_PROPERTIES)
|
|
1963
|
+
: line
|
|
1964
|
+
? m.diagnostics.flow.notForLines(name, resolved.name)
|
|
1965
|
+
: sticky
|
|
1966
|
+
? m.diagnostics.flow.notForStickies(name, resolved.name)
|
|
1967
|
+
: m.diagnostics.flow.notForTargets(name, resolved.kind === "relation", resolved.name), token, line || pen
|
|
1968
|
+
? []
|
|
1969
|
+
: sticky
|
|
1970
|
+
? [m.hints.values.stickyProperties(STICKY_PROPERTIES)]
|
|
1971
|
+
: [targetHint(name)]);
|
|
1972
|
+
}
|
|
1973
|
+
// A pen is drawn one pixel wide at least: `width 0` is refused on it, not on a box.
|
|
1974
|
+
const thin = kind === "style" && pen && values.width === 0;
|
|
1975
|
+
if (thin && !misplaced.has("width")) {
|
|
1976
|
+
misplaced.add("width");
|
|
1977
|
+
const m = messages();
|
|
1978
|
+
this.invalid(m.diagnostics.values.expects("width", describeSpec({ kind: "number", ...PEN_WIDTH_RANGE })), argumentOf(stmt.block, tokens.width) ?? tokens.width, [m.hints.values.penWidth]);
|
|
1646
1979
|
}
|
|
1647
1980
|
// `shape` is refused per target: a block may list a node and a decision.
|
|
1648
1981
|
const shapeless = kind === "style" && tokens.shape ? this.shapeRefusal(resolved) : null;
|
|
@@ -1661,6 +1994,7 @@ export class FlowAnalyzer {
|
|
|
1661
1994
|
const refused = [
|
|
1662
1995
|
...(shapeless === null ? [] : ["shape"]),
|
|
1663
1996
|
...(unturned === null ? [] : ["rotation"]),
|
|
1997
|
+
...(thin ? ["width"] : []),
|
|
1664
1998
|
];
|
|
1665
1999
|
const excluded = refused.length === 0 ? misfit : [...misfit, ...refused];
|
|
1666
2000
|
if (kind === "animate") {
|
|
@@ -1794,8 +2128,9 @@ export class FlowAnalyzer {
|
|
|
1794
2128
|
const element = resolved.kind === "element" ? this.elementsById.get(resolved.name) : undefined;
|
|
1795
2129
|
const m = messages().diagnostics.flow;
|
|
1796
2130
|
let reason = null;
|
|
1797
|
-
// A free line runs effects as a relation does: it is a connector without ends
|
|
1798
|
-
|
|
2131
|
+
// A free line or a pen runs effects as a relation does: it is a connector without ends, and its
|
|
2132
|
+
// stroke is the outline a dash follows.
|
|
2133
|
+
const connector = resolved.kind === "relation" || element?.kind === "line" || element?.kind === "pen";
|
|
1799
2134
|
if (effect === "flow" && !connector) {
|
|
1800
2135
|
reason = m.flowTarget;
|
|
1801
2136
|
}
|
|
@@ -1805,9 +2140,10 @@ export class FlowAnalyzer {
|
|
|
1805
2140
|
else if (effect === "dash") {
|
|
1806
2141
|
if (resolved.kind === "event")
|
|
1807
2142
|
reason = m.dashOnEvent;
|
|
1808
|
-
else if (element &&
|
|
1809
|
-
|
|
1810
|
-
//
|
|
2143
|
+
else if (element &&
|
|
2144
|
+
(element.kind === "note" || element.kind === "text" || element.kind === "sticky")) {
|
|
2145
|
+
// A note, a text or a sticky draws an outline when its styles say so, which a later
|
|
2146
|
+
// statement may write: checked once every block is read (`checkOutlinedDashes`).
|
|
1811
2147
|
if (stmt)
|
|
1812
2148
|
this.pendingDashes.push({ name: resolved.name, token, stmt });
|
|
1813
2149
|
}
|
|
@@ -1847,7 +2183,10 @@ export class FlowAnalyzer {
|
|
|
1847
2183
|
}
|
|
1848
2184
|
}
|
|
1849
2185
|
// -------------------------------------------------------------------- free helpers
|
|
1850
|
-
/**
|
|
2186
|
+
/**
|
|
2187
|
+
* The declarations and statements of flow and free diagrams, as their hints list them: the hints
|
|
2188
|
+
* of free diagrams are built from `FREE_DECLARATIONS`, so a new kind is one word here.
|
|
2189
|
+
*/
|
|
1851
2190
|
const FLOW_DECLARATIONS = ["node", "decision", "data", "group", "note", "tag"];
|
|
1852
2191
|
const FREE_DECLARATIONS = [
|
|
1853
2192
|
"box",
|
|
@@ -1855,7 +2194,9 @@ const FREE_DECLARATIONS = [
|
|
|
1855
2194
|
"icon",
|
|
1856
2195
|
"image",
|
|
1857
2196
|
"note",
|
|
2197
|
+
"sticky",
|
|
1858
2198
|
"line",
|
|
2199
|
+
"pen",
|
|
1859
2200
|
"group",
|
|
1860
2201
|
];
|
|
1861
2202
|
const FLOW_STATEMENTS = [
|
|
@@ -1879,16 +2220,13 @@ const FREE_STATEMENTS = [
|
|
|
1879
2220
|
"layers",
|
|
1880
2221
|
...FLOW_STATEMENTS.slice(8),
|
|
1881
2222
|
];
|
|
1882
|
-
/** Style properties that only free diagrams accept, for hints. */
|
|
1883
|
-
const FREE_PROFILE_ONLY = {
|
|
1884
|
-
fontSize: true,
|
|
1885
|
-
fontWeight: true,
|
|
1886
|
-
textAlign: true,
|
|
1887
|
-
verticalAlign: true,
|
|
1888
|
-
};
|
|
1889
2223
|
function isWord(arg, word) {
|
|
1890
2224
|
return singleIdent(arg)?.text === word;
|
|
1891
2225
|
}
|
|
2226
|
+
/** Whether a statement of a group block is one of the properties of a list. */
|
|
2227
|
+
function isListProperty(keyword) {
|
|
2228
|
+
return LIST_PROPERTIES.includes(keyword);
|
|
2229
|
+
}
|
|
1892
2230
|
/** A tuple of exactly two numbers without units. */
|
|
1893
2231
|
export function readPoint(arg) {
|
|
1894
2232
|
if (arg?.kind !== "Tuple" || arg.items.length !== 2)
|