@meltemi/diagrams-model 1.9.0 → 1.10.17
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 +115 -36
- package/dist/c4.js.map +1 -1
- package/dist/diagram-types.d.ts.map +1 -1
- package/dist/diagram-types.js +10 -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 +27 -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 +100 -8
- package/dist/flow.d.ts.map +1 -1
- package/dist/flow.js +614 -66
- package/dist/flow.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts +48 -0
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +28 -1
- package/dist/json.js.map +1 -1
- package/dist/messages/changes.d.ts +2 -0
- package/dist/messages/changes.d.ts.map +1 -1
- package/dist/messages/changes.js +4 -0
- package/dist/messages/changes.js.map +1 -1
- package/dist/messages/en.d.ts +83 -9
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +92 -14
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/es.d.ts.map +1 -1
- package/dist/messages/es.js +52 -11
- package/dist/messages/es.js.map +1 -1
- package/dist/messages/index.d.ts +44 -6
- 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 +27 -0
- package/dist/outline.js.map +1 -0
- 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 +102 -11
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +168 -13
- 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 +123 -12
- 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, 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
|
+
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 = [];
|
|
@@ -148,6 +153,7 @@ export class FlowAnalyzer {
|
|
|
148
153
|
routing: "auto",
|
|
149
154
|
layers: [MAIN_LAYER],
|
|
150
155
|
autonumber: null,
|
|
156
|
+
spacing: null,
|
|
151
157
|
};
|
|
152
158
|
}
|
|
153
159
|
/** Sequence diagrams: the ordered events, their index and the scenarios. */
|
|
@@ -198,6 +204,8 @@ export class FlowAnalyzer {
|
|
|
198
204
|
this.declare(decl.stmt, decl.groupPath);
|
|
199
205
|
for (const rel of classified.relations)
|
|
200
206
|
this.relation(rel.stmt, rel.groupPath);
|
|
207
|
+
// After the relations, which may declare elements that only they mention.
|
|
208
|
+
this.resolveAttachments();
|
|
201
209
|
this.assignKeys();
|
|
202
210
|
this.afterRelations(classified);
|
|
203
211
|
for (const stmt of classified.properties)
|
|
@@ -216,6 +224,7 @@ export class FlowAnalyzer {
|
|
|
216
224
|
this.styleOrAnimate(stmt, keyword);
|
|
217
225
|
}
|
|
218
226
|
this.checkLayers();
|
|
227
|
+
this.checkOutlinedDashes();
|
|
219
228
|
this.finish();
|
|
220
229
|
if (this.declared)
|
|
221
230
|
this.checkDeclared(this.declared);
|
|
@@ -254,6 +263,33 @@ export class FlowAnalyzer {
|
|
|
254
263
|
hasErrors: this.node.hasError || this.errors > 0,
|
|
255
264
|
};
|
|
256
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* A dash follows an outline: a note or a free text that no style statement, class or rule of the
|
|
268
|
+
* diagram gives a `fill`, `stroke` or `pattern` has none, nor a sticky without a `stroke`, and
|
|
269
|
+
* the dash is refused and dropped.
|
|
270
|
+
*/
|
|
271
|
+
checkOutlinedDashes() {
|
|
272
|
+
const source = {
|
|
273
|
+
styles: this.styles,
|
|
274
|
+
classes: this.definitions.classes,
|
|
275
|
+
rules: this.definitions.rules,
|
|
276
|
+
};
|
|
277
|
+
for (const { name, token, stmt } of this.pendingDashes) {
|
|
278
|
+
const element = this.elementsById.get(name);
|
|
279
|
+
if (!element || outlined(source, element))
|
|
280
|
+
continue;
|
|
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
|
+
});
|
|
288
|
+
const at = this.animations.findIndex((a) => a.node === stmt && a.target === name && a.effect === "dash");
|
|
289
|
+
if (at >= 0)
|
|
290
|
+
this.animations.splice(at, 1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
257
293
|
/** A declared type's rules: required properties, and the kinds relations may join. */
|
|
258
294
|
checkDeclared(definition) {
|
|
259
295
|
for (const element of this.elements) {
|
|
@@ -335,17 +371,32 @@ export class FlowAnalyzer {
|
|
|
335
371
|
keyword in this.profile.groupProperties) {
|
|
336
372
|
continue;
|
|
337
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
|
+
}
|
|
338
382
|
const m = messages();
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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];
|
|
344
391
|
const candidates = otherKind
|
|
345
392
|
? this.declarationWords()
|
|
346
|
-
: diagramStatements(this.profile).includes(keyword)
|
|
393
|
+
: misplaced || diagramStatements(this.profile).includes(keyword)
|
|
347
394
|
? []
|
|
348
|
-
: [
|
|
395
|
+
: [
|
|
396
|
+
"description",
|
|
397
|
+
"url",
|
|
398
|
+
...LIST_PROPERTIES.filter((name) => name in this.profile.groupProperties),
|
|
399
|
+
];
|
|
349
400
|
this.error("prop.unknown", m.diagnostics.flow.unknownInGroup(keyword), stmt.keyword, {
|
|
350
401
|
hints,
|
|
351
402
|
data: { candidates },
|
|
@@ -364,6 +415,14 @@ export class FlowAnalyzer {
|
|
|
364
415
|
this.error("prop.unknown", messages().diagnostics.flow.unknownInDiagram(keyword, this.profile.name), stmt.keyword, { hints, data: { candidates: this.statementCandidates(keyword) } });
|
|
365
416
|
}
|
|
366
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
|
+
}
|
|
367
426
|
/** Hints for an unknown statement at the diagram level, in the interface language. */
|
|
368
427
|
statementHints(keyword, stmt) {
|
|
369
428
|
const otherKind = this.otherProfileKind(keyword);
|
|
@@ -374,7 +433,7 @@ export class FlowAnalyzer {
|
|
|
374
433
|
if (stmt.args.length === 0 && !stmt.block) {
|
|
375
434
|
hints.push(m.declareElement(keyword, this.profile.shorthand.box));
|
|
376
435
|
}
|
|
377
|
-
hints.push(this.free ? m.freeStatements : m.flowStatements);
|
|
436
|
+
hints.push(this.free ? m.freeStatements(FREE_DECLARATIONS) : m.flowStatements);
|
|
378
437
|
return hints;
|
|
379
438
|
}
|
|
380
439
|
/** The statements an unknown one at the diagram level may have meant, for fixes. */
|
|
@@ -399,7 +458,7 @@ export class FlowAnalyzer {
|
|
|
399
458
|
return undefined;
|
|
400
459
|
}
|
|
401
460
|
const m = messages().hints.flow;
|
|
402
|
-
return this.free ? m.freeDeclarations : m.flowDeclarations(keyword);
|
|
461
|
+
return this.free ? m.freeDeclarations(FREE_DECLARATIONS) : m.flowDeclarations(keyword);
|
|
403
462
|
}
|
|
404
463
|
// ------------------------------------------------------------------ elements
|
|
405
464
|
declare(stmt, groupPath) {
|
|
@@ -420,6 +479,11 @@ export class FlowAnalyzer {
|
|
|
420
479
|
technology: null,
|
|
421
480
|
external: false,
|
|
422
481
|
shape: data ? "cylinder" : null,
|
|
482
|
+
line: null,
|
|
483
|
+
pen: null,
|
|
484
|
+
list: null,
|
|
485
|
+
attachedTo: null,
|
|
486
|
+
attachedToRange: null,
|
|
423
487
|
typeKind: null,
|
|
424
488
|
data: {},
|
|
425
489
|
groupPath,
|
|
@@ -435,19 +499,29 @@ export class FlowAnalyzer {
|
|
|
435
499
|
const typeKind = declaredKind ? stmt.keyword.text : null;
|
|
436
500
|
const kind = (declaredKind ? "box" : stmt.keyword.text);
|
|
437
501
|
const sourced = kind === "text" || kind === "icon" || kind === "image";
|
|
502
|
+
const note = kind === "note" && !declaredKind;
|
|
438
503
|
const second = { text: '"Text"', icon: '"<pack>:<name>"', image: '"<path>"' }[kind];
|
|
439
|
-
const form =
|
|
440
|
-
?
|
|
441
|
-
:
|
|
504
|
+
const form = note
|
|
505
|
+
? '`note <id> "Text" [on <element>]`'
|
|
506
|
+
: sourced
|
|
507
|
+
? `\`${kind} <id> ${second}\``
|
|
508
|
+
: `\`${kind} <id> ["Label"]${kind === "group" ? " { ... }" : ""}\``;
|
|
442
509
|
const example = {
|
|
443
510
|
text: 'text heading "Architecture"',
|
|
444
511
|
icon: 'icon server "tabler:server"',
|
|
445
512
|
image: 'image logo "logo.png"',
|
|
513
|
+
note: 'note limits "500 rps" on api',
|
|
446
514
|
}[kind];
|
|
447
515
|
if (stmt.chain) {
|
|
448
516
|
this.invalid(messages().diagnostics.common.singleBlock(kind), stmt.chain.clauses[1].keyword);
|
|
449
517
|
return;
|
|
450
518
|
}
|
|
519
|
+
if (kind === "line" && !declaredKind)
|
|
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);
|
|
451
525
|
const [idArg, labelArg, ...extra] = stmt.args;
|
|
452
526
|
const idToken = singleIdent(idArg);
|
|
453
527
|
if (!idToken) {
|
|
@@ -491,22 +565,32 @@ export class FlowAnalyzer {
|
|
|
491
565
|
sourceRange = this.range(labelArg);
|
|
492
566
|
}
|
|
493
567
|
}
|
|
494
|
-
else if (sourced) {
|
|
568
|
+
else if (sourced || (note && this.notesNeedText)) {
|
|
495
569
|
const m = messages();
|
|
496
570
|
this.invalid(m.diagnostics.flow.needsSource(kind, idToken.text, form), stmt, [
|
|
497
571
|
m.hints.example(example),
|
|
498
572
|
]);
|
|
499
573
|
}
|
|
574
|
+
// A note may end with `on <element>`; nothing else follows a label.
|
|
575
|
+
let attachment;
|
|
500
576
|
if (extra.length > 0) {
|
|
501
|
-
|
|
502
|
-
|
|
577
|
+
attachment = note ? this.attachmentOf(extra, form) : undefined;
|
|
578
|
+
if (!attachment) {
|
|
579
|
+
if (!note || !isWord(extra[0], "on")) {
|
|
580
|
+
this.invalid(messages().diagnostics.flow.valueAfter(note ? "text" : sourced ? kind : "label", form), extra[0]);
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
503
584
|
}
|
|
504
585
|
const block = kind === "group"
|
|
505
|
-
? { ...this.groupBlock(stmt.block, idToken), ports: [], values: {} }
|
|
506
|
-
:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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;
|
|
510
594
|
const added = this.addElement({
|
|
511
595
|
id: idToken.text,
|
|
512
596
|
kind,
|
|
@@ -520,6 +604,11 @@ export class FlowAnalyzer {
|
|
|
520
604
|
technology: null,
|
|
521
605
|
external: false,
|
|
522
606
|
shape: null,
|
|
607
|
+
line: null,
|
|
608
|
+
pen: null,
|
|
609
|
+
list,
|
|
610
|
+
attachedTo: attachment?.target ?? null,
|
|
611
|
+
attachedToRange: attachment ? this.range(attachment.arg) : null,
|
|
523
612
|
typeKind,
|
|
524
613
|
data: declaredKind ? dataOf(blockValues, declaredKind.properties) : {},
|
|
525
614
|
groupPath,
|
|
@@ -528,6 +617,8 @@ export class FlowAnalyzer {
|
|
|
528
617
|
idToken,
|
|
529
618
|
node: stmt,
|
|
530
619
|
});
|
|
620
|
+
if (added && attachment)
|
|
621
|
+
this.attachments.push({ note: idToken.text, ...attachment, stmt });
|
|
531
622
|
if (!added || source === null || !sourceRange)
|
|
532
623
|
return;
|
|
533
624
|
if (kind === "icon")
|
|
@@ -535,6 +626,240 @@ export class FlowAnalyzer {
|
|
|
535
626
|
else
|
|
536
627
|
this.images.push({ element: idToken.text, path: source, range: sourceRange });
|
|
537
628
|
}
|
|
629
|
+
/**
|
|
630
|
+
* `line <id> ["Label"] from (x, y) to (x, y) [via [(x, y), ...]] [{ ... }]`: a free line that
|
|
631
|
+
* joins no element, its points in the interior coordinates of its innermost group. The label is
|
|
632
|
+
* empty unless written: a line has no name to show.
|
|
633
|
+
*/
|
|
634
|
+
declareLine(stmt, groupPath) {
|
|
635
|
+
const form = '`line <id> ["Label"] from (x, y) to (x, y) [via [(x, y), ...]]`';
|
|
636
|
+
const m = messages();
|
|
637
|
+
const [idArg, ...rest] = stmt.args;
|
|
638
|
+
const idToken = singleIdent(idArg);
|
|
639
|
+
if (!idToken) {
|
|
640
|
+
this.invalid(m.diagnostics.common.expected(form), idArg ?? stmt.keyword, [
|
|
641
|
+
m.hints.example("line divider from (40, 200) to (520, 200)"),
|
|
642
|
+
]);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const refuse = (at) => this.invalid(m.diagnostics.flow.linePoints, at ?? stmt, [m.hints.flow.form(form)]);
|
|
646
|
+
const labelArg = rest[0]?.kind === "StringLiteral" ? rest[0] : undefined;
|
|
647
|
+
const [fromWord, fromArg, toWord, toArg, viaWord, viaList, extra] = labelArg
|
|
648
|
+
? rest.slice(1)
|
|
649
|
+
: rest;
|
|
650
|
+
if (!isWord(fromWord, "from"))
|
|
651
|
+
return refuse(fromWord);
|
|
652
|
+
const from = readPoint(fromArg);
|
|
653
|
+
if (!from)
|
|
654
|
+
return refuse(fromArg);
|
|
655
|
+
if (!isWord(toWord, "to"))
|
|
656
|
+
return refuse(toWord);
|
|
657
|
+
const to = readPoint(toArg);
|
|
658
|
+
if (!to)
|
|
659
|
+
return refuse(toArg);
|
|
660
|
+
const via = [];
|
|
661
|
+
if (viaWord) {
|
|
662
|
+
if (!isWord(viaWord, "via") || viaList?.kind !== "List")
|
|
663
|
+
return refuse(viaList ?? viaWord);
|
|
664
|
+
for (const item of viaList.items) {
|
|
665
|
+
const point = readPoint(item);
|
|
666
|
+
if (!point)
|
|
667
|
+
return refuse(item);
|
|
668
|
+
via.push(point);
|
|
669
|
+
}
|
|
670
|
+
if (extra)
|
|
671
|
+
return refuse(extra);
|
|
672
|
+
}
|
|
673
|
+
const { values: _values, ...blockFields } = this.elementBlock(stmt.block, this.profile.elementProperties, "Element", idToken);
|
|
674
|
+
this.addElement({
|
|
675
|
+
id: idToken.text,
|
|
676
|
+
kind: "line",
|
|
677
|
+
label: labelArg?.kind === "StringLiteral" ? labelArg.value : "",
|
|
678
|
+
implicit: false,
|
|
679
|
+
...blockFields,
|
|
680
|
+
source: null,
|
|
681
|
+
sourceRange: null,
|
|
682
|
+
labelWritten: labelArg !== undefined,
|
|
683
|
+
entity: null,
|
|
684
|
+
technology: null,
|
|
685
|
+
external: false,
|
|
686
|
+
shape: null,
|
|
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,
|
|
805
|
+
attachedTo: null,
|
|
806
|
+
attachedToRange: null,
|
|
807
|
+
typeKind: null,
|
|
808
|
+
data: {},
|
|
809
|
+
groupPath,
|
|
810
|
+
range: this.range(stmt),
|
|
811
|
+
idRange: this.range(idToken),
|
|
812
|
+
idToken,
|
|
813
|
+
node: stmt,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/** Whether a note must write its text: free compositions and views, where a note is no node. */
|
|
817
|
+
get notesNeedText() {
|
|
818
|
+
return this.free;
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* The target of a note's `on <element>`: `extra` holds the word `on` and one identifier (C4
|
|
822
|
+
* views read an entity path), nothing else. Undefined when it does not; a lone `on` is reported
|
|
823
|
+
* here, as the form is wanted.
|
|
824
|
+
*/
|
|
825
|
+
attachmentOf(extra, form) {
|
|
826
|
+
if (!isWord(extra[0], "on"))
|
|
827
|
+
return undefined;
|
|
828
|
+
const arg = extra[1];
|
|
829
|
+
const token = singleIdent(arg);
|
|
830
|
+
if (!token || extra.length > 2) {
|
|
831
|
+
this.invalid(messages().diagnostics.common.expected(form), extra[1] ?? extra[0]);
|
|
832
|
+
return undefined;
|
|
833
|
+
}
|
|
834
|
+
return { arg: arg, target: token.text };
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Resolves the targets of the notes' `on <element>`: an element of the diagram other than the
|
|
838
|
+
* note itself, of any kind. Renames follow the reference this records.
|
|
839
|
+
*/
|
|
840
|
+
resolveAttachments() {
|
|
841
|
+
const m = messages();
|
|
842
|
+
for (const { note, arg, target, stmt } of this.attachments) {
|
|
843
|
+
const token = singleIdent(arg);
|
|
844
|
+
if (target === note) {
|
|
845
|
+
this.invalid(m.diagnostics.flow.attachSelf(note), token);
|
|
846
|
+
}
|
|
847
|
+
else if (this.elementsById.has(target)) {
|
|
848
|
+
this.reference(token, target, "element", "attachment", stmt);
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
this.error("ref.unknown-element", m.diagnostics.common.unknownElement(target), token, {
|
|
852
|
+
hints: [m.hints.flow.attachTarget(note)],
|
|
853
|
+
data: {
|
|
854
|
+
ids: [target],
|
|
855
|
+
candidates: this.elements
|
|
856
|
+
.filter((e) => e.id !== note && e.kind !== "line" && e.kind !== "pen")
|
|
857
|
+
.map((e) => e.id),
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
538
863
|
addElement(element) {
|
|
539
864
|
const existing = this.elementsById.get(element.id);
|
|
540
865
|
if (existing) {
|
|
@@ -555,21 +880,36 @@ export class FlowAnalyzer {
|
|
|
555
880
|
return true;
|
|
556
881
|
}
|
|
557
882
|
/**
|
|
558
|
-
* Reads an element or relation block: its description, applied classes, named ports,
|
|
559
|
-
* `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).
|
|
560
886
|
*/
|
|
561
|
-
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) {
|
|
562
890
|
const empty = { layer: null, layerRange: null, link: null };
|
|
563
891
|
if (!block)
|
|
564
|
-
return {
|
|
892
|
+
return {
|
|
893
|
+
description: null,
|
|
894
|
+
url: null,
|
|
895
|
+
classes: [],
|
|
896
|
+
ports: [],
|
|
897
|
+
tags: [],
|
|
898
|
+
values: {},
|
|
899
|
+
...empty,
|
|
900
|
+
};
|
|
565
901
|
const ports = [];
|
|
902
|
+
const tags = [];
|
|
566
903
|
const extras = { ...empty };
|
|
567
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";
|
|
568
908
|
const rest = block.statements.filter((stmt) => {
|
|
569
909
|
if (stmt.kind !== "KeywordStatement")
|
|
570
910
|
return true;
|
|
571
911
|
const keyword = stmt.keyword.text;
|
|
572
|
-
if (
|
|
912
|
+
if (portable && keyword === "port") {
|
|
573
913
|
if (!headerHasError(stmt))
|
|
574
914
|
this.port(stmt, ports);
|
|
575
915
|
return false;
|
|
@@ -579,22 +919,69 @@ export class FlowAnalyzer {
|
|
|
579
919
|
this.extra(stmt, extras, seen, ownerToken);
|
|
580
920
|
return false;
|
|
581
921
|
}
|
|
922
|
+
if (tagged && keyword === "tag") {
|
|
923
|
+
if (!headerHasError(stmt))
|
|
924
|
+
this.tag(stmt, tags);
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
582
927
|
return true;
|
|
583
928
|
});
|
|
584
|
-
const words = Object.keys(table).concat(
|
|
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);
|
|
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);
|
|
589
931
|
return {
|
|
590
932
|
description: typeof values.description === "string" ? values.description : null,
|
|
591
933
|
url: typeof values.url === "string" ? values.url : null,
|
|
592
934
|
classes,
|
|
593
935
|
ports,
|
|
936
|
+
tags,
|
|
594
937
|
values,
|
|
595
938
|
...extras,
|
|
596
939
|
};
|
|
597
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
|
+
}
|
|
598
985
|
/** Reads `layer <name>` or `link <diagram>.<element>` into the extras of a block. */
|
|
599
986
|
extra(stmt, extras, seen, ownerToken) {
|
|
600
987
|
const keyword = stmt.keyword.text;
|
|
@@ -693,8 +1080,8 @@ export class FlowAnalyzer {
|
|
|
693
1080
|
});
|
|
694
1081
|
}
|
|
695
1082
|
/**
|
|
696
|
-
* Reads the `description`, `class`, `layer` and `link` properties of a group block
|
|
697
|
-
* 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.
|
|
698
1085
|
*/
|
|
699
1086
|
groupBlock(block, ownerToken) {
|
|
700
1087
|
let description = null;
|
|
@@ -702,8 +1089,9 @@ export class FlowAnalyzer {
|
|
|
702
1089
|
let classes = [];
|
|
703
1090
|
const extras = { layer: null, layerRange: null, link: null };
|
|
704
1091
|
if (!block)
|
|
705
|
-
return { description, url, classes, ...extras };
|
|
1092
|
+
return { description, url, classes, list: null, ...extras };
|
|
706
1093
|
const seen = new Map();
|
|
1094
|
+
const listed = {};
|
|
707
1095
|
for (const stmt of block.statements) {
|
|
708
1096
|
if (stmt.kind !== "KeywordStatement" || headerHasError(stmt))
|
|
709
1097
|
continue;
|
|
@@ -712,8 +1100,14 @@ export class FlowAnalyzer {
|
|
|
712
1100
|
this.extra(stmt, extras, seen, ownerToken);
|
|
713
1101
|
continue;
|
|
714
1102
|
}
|
|
715
|
-
|
|
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)) {
|
|
716
1109
|
continue;
|
|
1110
|
+
}
|
|
717
1111
|
const first = seen.get(name);
|
|
718
1112
|
if (first) {
|
|
719
1113
|
const m = messages();
|
|
@@ -723,6 +1117,18 @@ export class FlowAnalyzer {
|
|
|
723
1117
|
continue;
|
|
724
1118
|
}
|
|
725
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
|
+
}
|
|
726
1132
|
if (name === "class") {
|
|
727
1133
|
classes = readClasses(stmt, this.lines, this.report) ?? [];
|
|
728
1134
|
continue;
|
|
@@ -737,7 +1143,45 @@ export class FlowAnalyzer {
|
|
|
737
1143
|
if (typeof value === "string")
|
|
738
1144
|
description = value;
|
|
739
1145
|
}
|
|
740
|
-
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 };
|
|
741
1185
|
}
|
|
742
1186
|
// ------------------------------------------------------------------ relations
|
|
743
1187
|
relation(stmt, groupPath) {
|
|
@@ -775,15 +1219,17 @@ export class FlowAnalyzer {
|
|
|
775
1219
|
if (this.profile.arrows[stmt.arrow.text] === undefined) {
|
|
776
1220
|
this.invalid(messages().diagnostics.flow.arrowUnavailable(stmt.arrow.text, Object.keys(this.profile.arrows)), stmt.arrow);
|
|
777
1221
|
}
|
|
778
|
-
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);
|
|
779
1223
|
const relation = {
|
|
780
1224
|
key: "",
|
|
781
1225
|
id,
|
|
782
1226
|
from,
|
|
783
1227
|
to,
|
|
784
1228
|
line: this.profile.arrows[stmt.arrow.text] ?? "solid",
|
|
785
|
-
|
|
1229
|
+
heads: RELATION_ARROWS[stmt.arrow.text]?.heads ?? "end",
|
|
1230
|
+
twoWay: RELATION_ARROWS[stmt.arrow.text]?.heads === "both",
|
|
786
1231
|
async: values.kind === "async",
|
|
1232
|
+
returns: null,
|
|
787
1233
|
technology: null,
|
|
788
1234
|
sources: [],
|
|
789
1235
|
derived: false,
|
|
@@ -792,6 +1238,7 @@ export class FlowAnalyzer {
|
|
|
792
1238
|
assumption: typeof values.assumption === "string" ? values.assumption : null,
|
|
793
1239
|
data: this.declared ? dataOf(values, this.declared.relations.properties) : {},
|
|
794
1240
|
classes,
|
|
1241
|
+
tags,
|
|
795
1242
|
layer,
|
|
796
1243
|
layerRange,
|
|
797
1244
|
groupPath,
|
|
@@ -830,6 +1277,7 @@ export class FlowAnalyzer {
|
|
|
830
1277
|
description: null,
|
|
831
1278
|
url: null,
|
|
832
1279
|
classes: [],
|
|
1280
|
+
tags: [],
|
|
833
1281
|
ports: [],
|
|
834
1282
|
layer: null,
|
|
835
1283
|
layerRange: null,
|
|
@@ -841,6 +1289,11 @@ export class FlowAnalyzer {
|
|
|
841
1289
|
technology: null,
|
|
842
1290
|
external: false,
|
|
843
1291
|
shape: null,
|
|
1292
|
+
line: null,
|
|
1293
|
+
pen: null,
|
|
1294
|
+
list: null,
|
|
1295
|
+
attachedTo: null,
|
|
1296
|
+
attachedToRange: null,
|
|
844
1297
|
typeKind: null,
|
|
845
1298
|
data: {},
|
|
846
1299
|
groupPath,
|
|
@@ -922,6 +1375,9 @@ export class FlowAnalyzer {
|
|
|
922
1375
|
case "autonumber":
|
|
923
1376
|
this.properties.autonumber = Number(value);
|
|
924
1377
|
break;
|
|
1378
|
+
case "spacing":
|
|
1379
|
+
this.properties.spacing = Number(value);
|
|
1380
|
+
break;
|
|
925
1381
|
}
|
|
926
1382
|
}
|
|
927
1383
|
/** Reads `layers a, b, c`: back to front, `main` last unless listed. */
|
|
@@ -1048,8 +1504,9 @@ export class FlowAnalyzer {
|
|
|
1048
1504
|
}
|
|
1049
1505
|
return true;
|
|
1050
1506
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
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}`}`;
|
|
1053
1510
|
const first = this.placementSeen.get(key);
|
|
1054
1511
|
if (first) {
|
|
1055
1512
|
const m = messages();
|
|
@@ -1075,7 +1532,9 @@ export class FlowAnalyzer {
|
|
|
1075
1532
|
if (extra)
|
|
1076
1533
|
return this.shapeError(stmt, form, extra);
|
|
1077
1534
|
const element = this.resolveElement(targetToken, "place", stmt);
|
|
1078
|
-
if (!element || this.
|
|
1535
|
+
if (!element || this.lineRefused(element, "place", targetToken))
|
|
1536
|
+
return;
|
|
1537
|
+
if (this.duplicatePlacement(stmt, element))
|
|
1079
1538
|
return;
|
|
1080
1539
|
this.placement.push({
|
|
1081
1540
|
statement: "place",
|
|
@@ -1102,7 +1561,9 @@ export class FlowAnalyzer {
|
|
|
1102
1561
|
if (extra)
|
|
1103
1562
|
return this.shapeError(stmt, form, extra);
|
|
1104
1563
|
const element = this.resolveElement(targetToken, "size", stmt);
|
|
1105
|
-
if (!element || this.
|
|
1564
|
+
if (!element || this.lineRefused(element, "size", targetToken))
|
|
1565
|
+
return;
|
|
1566
|
+
if (this.duplicatePlacement(stmt, element))
|
|
1106
1567
|
return;
|
|
1107
1568
|
this.placement.push({
|
|
1108
1569
|
statement: "size",
|
|
@@ -1340,12 +1801,17 @@ export class FlowAnalyzer {
|
|
|
1340
1801
|
if (extra)
|
|
1341
1802
|
return this.shapeError(stmt, form, extra);
|
|
1342
1803
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1804
|
+
// A free line's label pill moves as a relation's does.
|
|
1805
|
+
const line = this.elementsById.get(targetToken.text);
|
|
1806
|
+
if (line?.kind === "line")
|
|
1807
|
+
this.reference(targetToken, line.id, "element", "label", stmt);
|
|
1808
|
+
const relation = line?.kind === "line" ? undefined : this.resolveRelation(targetToken, "label", stmt);
|
|
1809
|
+
const labelled = line?.kind === "line" ? line.id : relation?.key;
|
|
1810
|
+
if (labelled === undefined || this.duplicatePlacement(stmt, labelled))
|
|
1345
1811
|
return;
|
|
1346
1812
|
this.placement.push({
|
|
1347
1813
|
statement: "label",
|
|
1348
|
-
target:
|
|
1814
|
+
target: labelled,
|
|
1349
1815
|
at: at.value,
|
|
1350
1816
|
offset: point,
|
|
1351
1817
|
range: this.range(stmt),
|
|
@@ -1353,6 +1819,27 @@ export class FlowAnalyzer {
|
|
|
1353
1819
|
node: stmt,
|
|
1354
1820
|
});
|
|
1355
1821
|
}
|
|
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
|
+
*/
|
|
1826
|
+
lineRefused(id, statement, token) {
|
|
1827
|
+
const kind = this.elementsById.get(id)?.kind;
|
|
1828
|
+
if (kind !== "line" && kind !== "pen")
|
|
1829
|
+
return false;
|
|
1830
|
+
const m = messages();
|
|
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
|
+
}
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1356
1843
|
resolveElement(token, statement, stmt) {
|
|
1357
1844
|
const name = token.text;
|
|
1358
1845
|
if (this.elementsById.has(name)) {
|
|
@@ -1410,7 +1897,7 @@ export class FlowAnalyzer {
|
|
|
1410
1897
|
const table = kind === "style" ? this.profile.styleProperties : this.profile.animateProperties;
|
|
1411
1898
|
const shapes = profileTakesShape(this.profile);
|
|
1412
1899
|
const words = kind === "style"
|
|
1413
|
-
? Object.keys(table).filter((p) => p === "shape" ? shapes : this.free || !(p
|
|
1900
|
+
? Object.keys(table).filter((p) => p === "shape" ? shapes : this.free || !isFreeStyleProperty(p))
|
|
1414
1901
|
: ["effect", "duration", "repeat"];
|
|
1415
1902
|
const hint = () => {
|
|
1416
1903
|
const m = messages().hints.flow;
|
|
@@ -1438,18 +1925,57 @@ export class FlowAnalyzer {
|
|
|
1438
1925
|
const resolved = this.resolveStyleTarget(targetToken, kind, stmt);
|
|
1439
1926
|
if (!resolved)
|
|
1440
1927
|
continue;
|
|
1441
|
-
const
|
|
1442
|
-
|
|
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"
|
|
1443
1940
|
? ELEMENT_ONLY_PROPERTIES
|
|
1444
|
-
:
|
|
1445
|
-
|
|
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;
|
|
1446
1950
|
for (const name of misfit) {
|
|
1447
1951
|
const token = tokens[name];
|
|
1448
|
-
// `shape` is reported below, with the kinds that take it.
|
|
1449
|
-
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))
|
|
1450
1954
|
continue;
|
|
1451
1955
|
misplaced.add(name);
|
|
1452
|
-
|
|
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]);
|
|
1453
1979
|
}
|
|
1454
1980
|
// `shape` is refused per target: a block may list a node and a decision.
|
|
1455
1981
|
const shapeless = kind === "style" && tokens.shape ? this.shapeRefusal(resolved) : null;
|
|
@@ -1468,12 +1994,13 @@ export class FlowAnalyzer {
|
|
|
1468
1994
|
const refused = [
|
|
1469
1995
|
...(shapeless === null ? [] : ["shape"]),
|
|
1470
1996
|
...(unturned === null ? [] : ["rotation"]),
|
|
1997
|
+
...(thin ? ["width"] : []),
|
|
1471
1998
|
];
|
|
1472
1999
|
const excluded = refused.length === 0 ? misfit : [...misfit, ...refused];
|
|
1473
2000
|
if (kind === "animate") {
|
|
1474
2001
|
// One effect per channel and target; different channels may share a target.
|
|
1475
2002
|
const effect = values.effect;
|
|
1476
|
-
if (effect && !this.animationTargetFits(effect, resolved, targetToken))
|
|
2003
|
+
if (effect && !this.animationTargetFits(effect, resolved, targetToken, stmt))
|
|
1477
2004
|
continue;
|
|
1478
2005
|
if (effect) {
|
|
1479
2006
|
const key = `${resolved.name}:${effect}`;
|
|
@@ -1597,19 +2124,29 @@ export class FlowAnalyzer {
|
|
|
1597
2124
|
return { profile: false, kind: element.kind };
|
|
1598
2125
|
}
|
|
1599
2126
|
/** Whether an effect can run on a target; reports `animate.invalid-target` when not. */
|
|
1600
|
-
animationTargetFits(effect, resolved, token) {
|
|
2127
|
+
animationTargetFits(effect, resolved, token, stmt) {
|
|
1601
2128
|
const element = resolved.kind === "element" ? this.elementsById.get(resolved.name) : undefined;
|
|
1602
2129
|
const m = messages().diagnostics.flow;
|
|
1603
2130
|
let reason = null;
|
|
1604
|
-
|
|
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";
|
|
2134
|
+
if (effect === "flow" && !connector) {
|
|
1605
2135
|
reason = m.flowTarget;
|
|
1606
2136
|
}
|
|
1607
|
-
else if (effect === "trace" &&
|
|
2137
|
+
else if (effect === "trace" && !connector) {
|
|
1608
2138
|
reason = m.traceTarget;
|
|
1609
2139
|
}
|
|
1610
2140
|
else if (effect === "dash") {
|
|
1611
2141
|
if (resolved.kind === "event")
|
|
1612
2142
|
reason = m.dashOnEvent;
|
|
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`).
|
|
2147
|
+
if (stmt)
|
|
2148
|
+
this.pendingDashes.push({ name: resolved.name, token, stmt });
|
|
2149
|
+
}
|
|
1613
2150
|
else if (element && NO_OUTLINE_KINDS.has(element.kind)) {
|
|
1614
2151
|
reason = m.dashWithoutOutline(resolved.name, element.kind);
|
|
1615
2152
|
}
|
|
@@ -1646,9 +2183,22 @@ export class FlowAnalyzer {
|
|
|
1646
2183
|
}
|
|
1647
2184
|
}
|
|
1648
2185
|
// -------------------------------------------------------------------- free helpers
|
|
1649
|
-
/**
|
|
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
|
+
*/
|
|
1650
2190
|
const FLOW_DECLARATIONS = ["node", "decision", "data", "group", "note", "tag"];
|
|
1651
|
-
const FREE_DECLARATIONS = [
|
|
2191
|
+
const FREE_DECLARATIONS = [
|
|
2192
|
+
"box",
|
|
2193
|
+
"text",
|
|
2194
|
+
"icon",
|
|
2195
|
+
"image",
|
|
2196
|
+
"note",
|
|
2197
|
+
"sticky",
|
|
2198
|
+
"line",
|
|
2199
|
+
"pen",
|
|
2200
|
+
"group",
|
|
2201
|
+
];
|
|
1652
2202
|
const FLOW_STATEMENTS = [
|
|
1653
2203
|
"direction",
|
|
1654
2204
|
"layout",
|
|
@@ -1670,15 +2220,13 @@ const FREE_STATEMENTS = [
|
|
|
1670
2220
|
"layers",
|
|
1671
2221
|
...FLOW_STATEMENTS.slice(8),
|
|
1672
2222
|
];
|
|
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
2223
|
function isWord(arg, word) {
|
|
1680
2224
|
return singleIdent(arg)?.text === word;
|
|
1681
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
|
+
}
|
|
1682
2230
|
/** A tuple of exactly two numbers without units. */
|
|
1683
2231
|
export function readPoint(arg) {
|
|
1684
2232
|
if (arg?.kind !== "Tuple" || arg.items.length !== 2)
|