@diagrammo/dgmo 0.26.0 → 0.27.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/README.md +3 -3
- package/dist/advanced.cjs +4182 -2704
- package/dist/advanced.d.cts +266 -58
- package/dist/advanced.d.ts +266 -58
- package/dist/advanced.js +4182 -2698
- package/dist/auto.cjs +4042 -2581
- package/dist/auto.js +124 -122
- package/dist/auto.mjs +4042 -2581
- package/dist/cli.cjs +172 -170
- package/dist/editor.cjs +4 -0
- package/dist/editor.js +4 -0
- package/dist/highlight.cjs +4 -0
- package/dist/highlight.js +4 -0
- package/dist/index.cjs +4067 -2583
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +4067 -2583
- package/dist/internal.cjs +4182 -2704
- package/dist/internal.d.cts +266 -58
- package/dist/internal.d.ts +266 -58
- package/dist/internal.js +4182 -2698
- package/dist/map-data/PROVENANCE.json +1 -1
- package/dist/map-data/airport-collisions.json +1 -0
- package/dist/map-data/airports.json +1 -0
- package/docs/language-reference.md +68 -18
- package/gallery/fixtures/boxes-and-lines-diverging.dgmo +15 -0
- package/gallery/fixtures/map-choropleth-diverging.dgmo +9 -0
- package/gallery/fixtures/map-region-values.dgmo +13 -0
- package/gallery/fixtures/map-subnational-zoom.dgmo +12 -0
- package/gallery/fixtures/map-tagged-legs.dgmo +16 -0
- package/gallery/fixtures/map-undirected-edges.dgmo +12 -0
- package/package.json +1 -1
- package/src/advanced.ts +1 -6
- package/src/auto/index.ts +1 -1
- package/src/boxes-and-lines/layout.ts +146 -26
- package/src/boxes-and-lines/parser.ts +43 -8
- package/src/boxes-and-lines/renderer.ts +223 -96
- package/src/boxes-and-lines/types.ts +9 -2
- package/src/c4/layout.ts +14 -32
- package/src/c4/parser.ts +9 -5
- package/src/c4/renderer.ts +34 -39
- package/src/class/layout.ts +118 -18
- package/src/class/parser.ts +35 -0
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion.ts +26 -12
- package/src/cycle/layout.ts +55 -72
- package/src/cycle/renderer.ts +11 -6
- package/src/d3.ts +78 -117
- package/src/diagnostics.ts +16 -0
- package/src/echarts.ts +46 -33
- package/src/editor/keywords.ts +4 -0
- package/src/er/layout.ts +114 -22
- package/src/er/parser.ts +28 -0
- package/src/er/renderer.ts +55 -2
- package/src/er/types.ts +3 -0
- package/src/gantt/renderer.ts +46 -38
- package/src/gantt/resolver.ts +9 -2
- package/src/graph/edge-spline.ts +29 -0
- package/src/graph/flowchart-parser.ts +34 -1
- package/src/graph/flowchart-renderer.ts +78 -64
- package/src/graph/layout.ts +206 -23
- package/src/graph/notes.ts +21 -0
- package/src/graph/state-parser.ts +26 -1
- package/src/graph/state-renderer.ts +78 -64
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- package/src/infra/renderer.ts +16 -7
- package/src/journey-map/layout.ts +38 -49
- package/src/journey-map/renderer.ts +22 -45
- package/src/kanban/renderer.ts +15 -6
- package/src/label-layout.ts +3 -3
- package/src/map/completion.ts +77 -22
- package/src/map/context-labels.ts +57 -12
- package/src/map/data/PROVENANCE.json +1 -1
- package/src/map/data/airport-collisions.json +1 -0
- package/src/map/data/airports.json +1 -0
- package/src/map/data/types.ts +19 -0
- package/src/map/layout.ts +1196 -90
- package/src/map/legend-band.ts +2 -2
- package/src/map/load-data.ts +10 -1
- package/src/map/parser.ts +61 -32
- package/src/map/renderer.ts +284 -12
- package/src/map/resolved-types.ts +15 -1
- package/src/map/resolver.ts +132 -12
- package/src/map/types.ts +28 -8
- package/src/migrate/embedded.ts +9 -7
- package/src/mindmap/text-wrap.ts +13 -14
- package/src/org/layout.ts +19 -17
- package/src/org/renderer.ts +11 -4
- package/src/palettes/color-utils.ts +82 -21
- package/src/palettes/index.ts +0 -19
- package/src/palettes/registry.ts +1 -1
- package/src/palettes/types.ts +2 -2
- package/src/pert/layout.ts +48 -40
- package/src/pert/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +34 -68
- package/src/render.ts +1 -1
- package/src/ring/renderer.ts +1 -2
- package/src/sequence/parser.ts +100 -22
- package/src/sequence/renderer.ts +75 -50
- package/src/sitemap/layout.ts +27 -19
- package/src/sitemap/renderer.ts +12 -5
- package/src/tech-radar/renderer.ts +11 -35
- package/src/utils/arrow-markers.ts +51 -0
- package/src/utils/fit-canvas.ts +64 -0
- package/src/utils/legend-constants.ts +8 -54
- package/src/utils/legend-d3.ts +10 -7
- package/src/utils/legend-layout.ts +7 -4
- package/src/utils/legend-types.ts +10 -4
- package/src/utils/note-box/constants.ts +25 -0
- package/src/utils/note-box/index.ts +11 -0
- package/src/utils/note-box/metrics.ts +90 -0
- package/src/utils/note-box/svg.ts +331 -0
- package/src/utils/notes/bounds.ts +30 -0
- package/src/utils/notes/build.ts +131 -0
- package/src/utils/notes/index.ts +18 -0
- package/src/utils/notes/model.ts +19 -0
- package/src/utils/notes/parse.ts +131 -0
- package/src/utils/notes/place.ts +177 -0
- package/src/utils/notes/resolve.ts +88 -0
- package/src/utils/number-format.ts +36 -0
- package/src/utils/parsing.ts +41 -0
- package/src/utils/reserved-key-registry.ts +4 -0
- package/src/utils/text-measure.ts +122 -0
- package/src/wireframe/layout.ts +4 -2
- package/src/wireframe/renderer.ts +8 -6
- package/src/palettes/dracula.ts +0 -68
- package/src/palettes/gruvbox.ts +0 -85
- package/src/palettes/monokai.ts +0 -68
- package/src/palettes/one-dark.ts +0 -70
- package/src/palettes/rose-pine.ts +0 -84
- package/src/palettes/solarized.ts +0 -77
package/src/raci/renderer.ts
CHANGED
|
@@ -43,6 +43,11 @@ import type {
|
|
|
43
43
|
import { allTasks } from './parser';
|
|
44
44
|
import { VARIANTS } from './variants';
|
|
45
45
|
import { ScaleContext } from '../utils/scaling';
|
|
46
|
+
import {
|
|
47
|
+
measureText,
|
|
48
|
+
truncateText,
|
|
49
|
+
wrapTextToWidth,
|
|
50
|
+
} from '../utils/text-measure';
|
|
46
51
|
|
|
47
52
|
/**
|
|
48
53
|
* Group `parsed.diagnostics` per task, so the renderer can paint a
|
|
@@ -253,7 +258,8 @@ const ROLE_HEADER_FONT = 12;
|
|
|
253
258
|
const PHASE_FONT = 13;
|
|
254
259
|
|
|
255
260
|
// Cell-fill tint percentage of the marker color over surface bg.
|
|
256
|
-
|
|
261
|
+
// 25 = the canonical `shapeFill` tint used by every other chart type.
|
|
262
|
+
const TINT_PCT = 25;
|
|
257
263
|
/**
|
|
258
264
|
* Marker rect stroke + radius — matches the codebase's node styling
|
|
259
265
|
* convention (flowchart `NODE_STROKE_WIDTH = 1.5`, kanban
|
|
@@ -550,20 +556,21 @@ export function renderRaci(
|
|
|
550
556
|
|
|
551
557
|
// Size full-mode chips to actually fit the longest label without truncation.
|
|
552
558
|
// chip = 4 pad + 24 slab + 8 gap + label + 8 right pad.
|
|
553
|
-
const
|
|
554
|
-
(n, m) =>
|
|
559
|
+
const longestLabelPx = legendMarkers.reduce(
|
|
560
|
+
(n, m) =>
|
|
561
|
+
Math.max(n, measureText(variantLabels[m] ?? '', sLegendLabelFont)),
|
|
555
562
|
0
|
|
556
563
|
);
|
|
557
564
|
const fullChipW = Math.max(
|
|
558
565
|
sLegendChipLabelMin,
|
|
559
|
-
Math.ceil(4 + 24 + 8 +
|
|
566
|
+
Math.ceil(4 + 24 + 8 + longestLabelPx + 8)
|
|
560
567
|
);
|
|
561
568
|
const fullLegendW = numChips * fullChipW + chipGapTotal;
|
|
562
569
|
const letterLegendW = numChips * sLegendLetterChipW + chipGapTotal;
|
|
563
570
|
|
|
564
|
-
// Estimate title pixel width
|
|
571
|
+
// Estimate title pixel width using the shared glyph measurer.
|
|
565
572
|
const titleEstW =
|
|
566
|
-
parsed.title && !hideTitle ? parsed.title
|
|
573
|
+
parsed.title && !hideTitle ? measureText(parsed.title, sTitleFontSize) : 0;
|
|
567
574
|
|
|
568
575
|
// Pick legend mode: full chips if everything fits, else letter-only.
|
|
569
576
|
// If even letters won't fit beside the title, the title is truncated below.
|
|
@@ -596,7 +603,7 @@ export function renderRaci(
|
|
|
596
603
|
.attr('font-size', sTitleFontSize)
|
|
597
604
|
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
598
605
|
.attr('fill', palette.text)
|
|
599
|
-
.text(
|
|
606
|
+
.text(truncateText(parsed.title, sTitleFontSize, titleMaxW));
|
|
600
607
|
}
|
|
601
608
|
|
|
602
609
|
// Legend — right-aligned in the same row.
|
|
@@ -714,10 +721,10 @@ export function renderRaci(
|
|
|
714
721
|
.attr('font-weight', 600)
|
|
715
722
|
.attr('fill', palette.text)
|
|
716
723
|
.text(
|
|
717
|
-
|
|
724
|
+
truncateText(
|
|
718
725
|
parsed.roleDisplayNames[i] ?? '',
|
|
719
|
-
|
|
720
|
-
|
|
726
|
+
sRoleHeaderFont,
|
|
727
|
+
roleColW - 2 * sCellPad
|
|
721
728
|
)
|
|
722
729
|
);
|
|
723
730
|
});
|
|
@@ -931,7 +938,7 @@ function renderLegend(
|
|
|
931
938
|
if (mode === 'letters') {
|
|
932
939
|
// Compact: a single colored pill with just the marker letter.
|
|
933
940
|
// The full label moves to a native tooltip so hover still teaches it.
|
|
934
|
-
const fill = solid ? rawColor : mix(rawColor, surfaceBg,
|
|
941
|
+
const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
|
|
935
942
|
const stroke = solid ? mix(rawColor, surfaceBg, 70) : rawColor;
|
|
936
943
|
chipG
|
|
937
944
|
.append('rect')
|
|
@@ -966,7 +973,7 @@ function renderLegend(
|
|
|
966
973
|
}
|
|
967
974
|
|
|
968
975
|
// Full mode: bordered chip with a letter slab on the left and label text.
|
|
969
|
-
const fill = solid ? rawColor : mix(rawColor, surfaceBg,
|
|
976
|
+
const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
|
|
970
977
|
const stroke = mix(rawColor, surfaceBg, 70);
|
|
971
978
|
|
|
972
979
|
chipG
|
|
@@ -1026,10 +1033,10 @@ function renderLegend(
|
|
|
1026
1033
|
.attr('font-weight', 600)
|
|
1027
1034
|
.attr('fill', palette.text)
|
|
1028
1035
|
.text(
|
|
1029
|
-
|
|
1036
|
+
truncateText(
|
|
1030
1037
|
labelText,
|
|
1031
|
-
|
|
1032
|
-
|
|
1038
|
+
sLegendLabelFont,
|
|
1039
|
+
chipW - slabW - slabPad * 2 - 12
|
|
1033
1040
|
)
|
|
1034
1041
|
);
|
|
1035
1042
|
});
|
|
@@ -1123,7 +1130,7 @@ function renderPhaseBar(
|
|
|
1123
1130
|
// can see at a glance what's in the rolled-up phase.
|
|
1124
1131
|
const taskCount = phase.tasks.length;
|
|
1125
1132
|
if (taskCount > 0) {
|
|
1126
|
-
const labelTextWidth = phase.displayName
|
|
1133
|
+
const labelTextWidth = measureText(phase.displayName, sPhaseFont);
|
|
1127
1134
|
phaseG
|
|
1128
1135
|
.append('text')
|
|
1129
1136
|
.attr('x', x + 26 + labelTextWidth + 10)
|
|
@@ -1172,7 +1179,7 @@ function renderPhaseBar(
|
|
|
1172
1179
|
// corner radius. Stroke width is a touch thinner than
|
|
1173
1180
|
// NODE_STROKE_WIDTH because at the smaller summary scale the
|
|
1174
1181
|
// full 1.5 reads as too heavy.
|
|
1175
|
-
const fill = solid ? rawColor : mix(rawColor, surfaceBg,
|
|
1182
|
+
const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
|
|
1176
1183
|
const stroke = solid ? mix(rawColor, surfaceBg, 70) : rawColor;
|
|
1177
1184
|
const chipG = phaseG.append('g').attr('class', 'raci-phase-summary');
|
|
1178
1185
|
chipG
|
|
@@ -1458,7 +1465,7 @@ function renderTaskRow(
|
|
|
1458
1465
|
// ("Responsible") instead of the bare letter. Same primitive as the
|
|
1459
1466
|
// legend chip, so cells and legend read as the same UI element.
|
|
1460
1467
|
const fullLabel = variantLabels[m] ?? m;
|
|
1461
|
-
const labelPx = fullLabel
|
|
1468
|
+
const labelPx = measureText(fullLabel, sLegendLabelFont);
|
|
1462
1469
|
const showFullLabel = labelPx + 16 <= sliceW;
|
|
1463
1470
|
const textContent = showFullLabel ? fullLabel : m;
|
|
1464
1471
|
const textFont = showFullLabel ? sLegendLabelFont : sMarkerFont;
|
|
@@ -1511,45 +1518,6 @@ function renderTaskRow(
|
|
|
1511
1518
|
|
|
1512
1519
|
// ── Helpers ──────────────────────────────────────────────────
|
|
1513
1520
|
|
|
1514
|
-
/**
|
|
1515
|
-
* Greedy word-wrap to a per-line character cap. Whitespace runs that
|
|
1516
|
-
* happen to fall at a wrap point are dropped (no leading-space lines).
|
|
1517
|
-
* Words longer than the cap are hard-split so the output never exceeds.
|
|
1518
|
-
*/
|
|
1519
|
-
function wordWrap(s: string, charsPerLine: number): string[] {
|
|
1520
|
-
if (charsPerLine <= 0 || s.length <= charsPerLine) return [s];
|
|
1521
|
-
const out: string[] = [];
|
|
1522
|
-
const tokens = s.split(/(\s+)/);
|
|
1523
|
-
let cur = '';
|
|
1524
|
-
for (const tok of tokens) {
|
|
1525
|
-
if (!tok) continue;
|
|
1526
|
-
const isSpace = /^\s+$/.test(tok);
|
|
1527
|
-
if (cur.length + tok.length <= charsPerLine) {
|
|
1528
|
-
cur += tok;
|
|
1529
|
-
continue;
|
|
1530
|
-
}
|
|
1531
|
-
if (cur.trimEnd().length > 0) out.push(cur.trimEnd());
|
|
1532
|
-
if (!isSpace && tok.length > charsPerLine) {
|
|
1533
|
-
let chunk = tok;
|
|
1534
|
-
while (chunk.length > charsPerLine) {
|
|
1535
|
-
out.push(chunk.slice(0, charsPerLine));
|
|
1536
|
-
chunk = chunk.slice(charsPerLine);
|
|
1537
|
-
}
|
|
1538
|
-
cur = chunk;
|
|
1539
|
-
} else {
|
|
1540
|
-
cur = isSpace ? '' : tok;
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
if (cur.trimEnd().length > 0) out.push(cur.trimEnd());
|
|
1544
|
-
return out.length > 0 ? out : [''];
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
function wrapText(s: string, maxPx: number, fontSize: number): string[] {
|
|
1548
|
-
const charPx = fontSize * 0.6;
|
|
1549
|
-
const cap = Math.max(8, Math.floor(maxPx / charPx));
|
|
1550
|
-
return wordWrap(s, cap);
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
1521
|
/**
|
|
1554
1522
|
* Wrapped text content for a task row plus the height that fits it.
|
|
1555
1523
|
* Pre-computed once per task and reused by both the y-cursor layout
|
|
@@ -1581,13 +1549,17 @@ function prepareRowContent(
|
|
|
1581
1549
|
sStackTopGap = STACK_TOP_GAP,
|
|
1582
1550
|
sViolationLineHeight = VIOLATION_LINE_HEIGHT
|
|
1583
1551
|
): RowContent {
|
|
1584
|
-
const nameLines =
|
|
1552
|
+
const nameLines = wrapTextToWidth(task.displayName, sLabelFont, labelMaxW, {
|
|
1553
|
+
hardBreak: true,
|
|
1554
|
+
});
|
|
1585
1555
|
const description = task.description?.trim() ?? '';
|
|
1586
1556
|
const descLines =
|
|
1587
1557
|
description.length > 0
|
|
1588
1558
|
? description
|
|
1589
1559
|
.split('\n')
|
|
1590
|
-
.flatMap((line) =>
|
|
1560
|
+
.flatMap((line) =>
|
|
1561
|
+
wrapTextToWidth(line, sDescFont, labelMaxW, { hardBreak: true })
|
|
1562
|
+
)
|
|
1591
1563
|
: [];
|
|
1592
1564
|
const violations: RowContent['violations'] = [];
|
|
1593
1565
|
if (bucket) {
|
|
@@ -1601,7 +1573,9 @@ function prepareRowContent(
|
|
|
1601
1573
|
severity,
|
|
1602
1574
|
sourceLine: e.line,
|
|
1603
1575
|
text,
|
|
1604
|
-
lines:
|
|
1576
|
+
lines: wrapTextToWidth(text, sLabelFont - 2, labelMaxW, {
|
|
1577
|
+
hardBreak: true,
|
|
1578
|
+
}),
|
|
1605
1579
|
});
|
|
1606
1580
|
}
|
|
1607
1581
|
};
|
|
@@ -1627,14 +1601,6 @@ function prepareRowContent(
|
|
|
1627
1601
|
return { nameLines, descLines, violations, rowHeight };
|
|
1628
1602
|
}
|
|
1629
1603
|
|
|
1630
|
-
function truncateForWidth(s: string, maxPx: number, fontSize: number): string {
|
|
1631
|
-
// Conservative: 0.6 em per char for sans-serif at this weight.
|
|
1632
|
-
const charPx = fontSize * 0.6;
|
|
1633
|
-
const cap = Math.max(3, Math.floor(maxPx / charPx));
|
|
1634
|
-
if (s.length <= cap) return s;
|
|
1635
|
-
return s.substring(0, cap - 1) + '…';
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
1604
|
/**
|
|
1639
1605
|
* Split a diagnostic message at single-quoted spans so the renderer can
|
|
1640
1606
|
* paint the quoted name (role / task) as a bold tspan with no quotes.
|
package/src/render.ts
CHANGED
|
@@ -130,7 +130,7 @@ export async function render(
|
|
|
130
130
|
}
|
|
131
131
|
): Promise<{ svg: string; diagnostics: DgmoError[] }> {
|
|
132
132
|
const theme = options?.theme ?? 'light';
|
|
133
|
-
const paletteName = options?.palette ?? '
|
|
133
|
+
const paletteName = options?.palette ?? 'slate';
|
|
134
134
|
|
|
135
135
|
const paletteColors =
|
|
136
136
|
getPalette(paletteName)[theme === 'dark' ? 'dark' : 'light'];
|
package/src/ring/renderer.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../palettes/color-utils';
|
|
17
17
|
import { resolveColor } from '../colors';
|
|
18
18
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
19
|
+
import { CHAR_WIDTH_RATIO } from '../utils/text-measure';
|
|
19
20
|
import {
|
|
20
21
|
wrapDescriptionLines,
|
|
21
22
|
type WrappedDescLine,
|
|
@@ -41,8 +42,6 @@ const DESC_GAP = 28;
|
|
|
41
42
|
const DESC_ACCENT_WIDTH = 3;
|
|
42
43
|
/** Gap between accent bar and description text. */
|
|
43
44
|
const DESC_ACCENT_GAP = 12;
|
|
44
|
-
/** Approximate ratio of average glyph width to font size (sans-serif). */
|
|
45
|
-
const CHAR_WIDTH_RATIO = 0.55;
|
|
46
45
|
/** Pixel offset between bullet glyph column and body-text column. */
|
|
47
46
|
const BULLET_BODY_INDENT = 10;
|
|
48
47
|
/** Outer-edge stroke width per ring (Decision 14 — adjacent-ring contrast). */
|
package/src/sequence/parser.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { inferParticipantType } from './participant-inference';
|
|
6
6
|
import type { Brand, Writable } from '../utils/brand';
|
|
7
7
|
import type { DgmoError } from '../diagnostics';
|
|
8
|
+
import type { PaletteColors } from '../palettes/types';
|
|
8
9
|
import {
|
|
9
10
|
akaRemovedMessage,
|
|
10
11
|
formatDgmoError,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
nameMergedMessage,
|
|
15
16
|
participantTypeRemovedMessage,
|
|
16
17
|
pipeOperatorRemovedMessage,
|
|
18
|
+
sequenceBarePositionRemovedMessage,
|
|
17
19
|
suggest,
|
|
18
20
|
} from '../diagnostics';
|
|
19
21
|
import { normalizeName, displayName } from '../utils/name-normalize';
|
|
@@ -245,11 +247,14 @@ export interface ParsedSequenceDgmo {
|
|
|
245
247
|
// "Name is a type" pattern — e.g. "Auth Server is a database"
|
|
246
248
|
// Participant names may contain spaces; [^:]+? stops at colons so that
|
|
247
249
|
// note lines like "note right of A: this is an actor" are not falsely matched.
|
|
248
|
-
// Remainder after type
|
|
250
|
+
// Remainder after type carries the optional `as <alias>` modifier.
|
|
249
251
|
const IS_A_PATTERN = /^([^:]+?)\s+is\s+an?\s+(\w+)(?:\s+(.+))?$/i;
|
|
250
252
|
|
|
251
|
-
//
|
|
252
|
-
|
|
253
|
+
// Legacy bare-keyword "Name position N" detector — e.g. "DB position -1".
|
|
254
|
+
// Position is now colon-keyed metadata (`position: N`, §2.2); a surviving
|
|
255
|
+
// bare `position N` raises E_SEQUENCE_BARE_POSITION_REMOVED. Group 1 is the
|
|
256
|
+
// participant name (so it can still register), group 2 the offending number.
|
|
257
|
+
const BARE_POSITION_PATTERN = /^([^:]+?)\s+position\s+(-?\d+)$/i;
|
|
253
258
|
|
|
254
259
|
// Colored participant declaration — e.g. "Tapin2(green)", "API(blue)"
|
|
255
260
|
// Scoped to recognized 11-name palette colors only (§1.5) so legitimate
|
|
@@ -488,7 +493,10 @@ function resolveParticipantAndText(
|
|
|
488
493
|
/**
|
|
489
494
|
* Parse a .dgmo file with `chart: sequence` into a structured representation.
|
|
490
495
|
*/
|
|
491
|
-
export function parseSequenceDgmo(
|
|
496
|
+
export function parseSequenceDgmo(
|
|
497
|
+
content: string,
|
|
498
|
+
palette?: PaletteColors
|
|
499
|
+
): ParsedSequenceDgmo {
|
|
492
500
|
// Diagram-level options accumulator (Readonly<Record<...>> on the public
|
|
493
501
|
// type; mutated locally during parse and assigned back via `result.options`).
|
|
494
502
|
const optionsAccumulator: Record<string, string> = {};
|
|
@@ -637,7 +645,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
637
645
|
type: extras?.type ?? inferParticipantType(name),
|
|
638
646
|
lineNumber,
|
|
639
647
|
...(extras?.position !== undefined ? { position: extras.position } : {}),
|
|
640
|
-
|
|
648
|
+
// takePosition() may have emptied the metadata record (position was
|
|
649
|
+
// its only key) — don't attach an empty object.
|
|
650
|
+
...(extras?.metadata && Object.keys(extras.metadata).length > 0
|
|
651
|
+
? { metadata: extras.metadata }
|
|
652
|
+
: {}),
|
|
641
653
|
});
|
|
642
654
|
return trimmed;
|
|
643
655
|
};
|
|
@@ -674,7 +686,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
674
686
|
const splitPipe = (
|
|
675
687
|
text: string,
|
|
676
688
|
ln?: number
|
|
677
|
-
): { core: string; meta?: Record<string, string
|
|
689
|
+
): { core: string; meta?: Record<string, string>; alias?: string } => {
|
|
678
690
|
const idx = text.indexOf('|');
|
|
679
691
|
if (idx >= 0) {
|
|
680
692
|
if (ln !== undefined) {
|
|
@@ -709,11 +721,41 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
709
721
|
);
|
|
710
722
|
}
|
|
711
723
|
if (Object.keys(split.meta).length > 0) {
|
|
712
|
-
|
|
724
|
+
// splitNameAndMeta peels a trailing `as <alias>` off the name region
|
|
725
|
+
// when same-line metadata is present (the alias must precede the meta,
|
|
726
|
+
// which runs to EOL). Propagate it so callers can register the alias —
|
|
727
|
+
// the no-meta branch below leaves `as <alias>` in `core` for the
|
|
728
|
+
// caller's own peeling.
|
|
729
|
+
return {
|
|
730
|
+
core: split.name,
|
|
731
|
+
meta: split.meta,
|
|
732
|
+
...(split.alias !== undefined && { alias: split.alias }),
|
|
733
|
+
};
|
|
713
734
|
}
|
|
714
735
|
return { core: text };
|
|
715
736
|
};
|
|
716
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Pull the layout-order `position:` key out of a participant's
|
|
740
|
+
* parsed metadata (§2.2). Position is a layout directive, not display
|
|
741
|
+
* metadata, so it is *removed* from the record after extraction. A
|
|
742
|
+
* non-integer value raises an error and yields no position.
|
|
743
|
+
*/
|
|
744
|
+
const takePosition = (
|
|
745
|
+
meta: Record<string, string> | undefined,
|
|
746
|
+
ln: number
|
|
747
|
+
): number | undefined => {
|
|
748
|
+
if (meta?.['position'] === undefined) return undefined;
|
|
749
|
+
const raw = meta['position'];
|
|
750
|
+
delete meta['position'];
|
|
751
|
+
const n = parseInt(raw, 10);
|
|
752
|
+
if (!/^-?\d+$/.test(raw.trim()) || Number.isNaN(n)) {
|
|
753
|
+
pushError(ln, `'position: ${raw}' must be an integer slot index`);
|
|
754
|
+
return undefined;
|
|
755
|
+
}
|
|
756
|
+
return n;
|
|
757
|
+
};
|
|
758
|
+
|
|
717
759
|
// Block parsing state — blocks are built mutably during parse, then
|
|
718
760
|
// assigned back into the readonly-typed `ParsedSequenceDgmo`.
|
|
719
761
|
const blockStack: {
|
|
@@ -906,7 +948,7 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
906
948
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
907
949
|
const { label, color } = extractColor(
|
|
908
950
|
cleanEntry,
|
|
909
|
-
|
|
951
|
+
palette,
|
|
910
952
|
result.diagnostics,
|
|
911
953
|
lineNumber
|
|
912
954
|
);
|
|
@@ -1070,7 +1112,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1070
1112
|
// Handling makes aliasing unnecessary because forgiving normalization
|
|
1071
1113
|
// handles casing/whitespace differences automatically.
|
|
1072
1114
|
// Skip lines starting with 'note' — handled by note parsing below.
|
|
1073
|
-
const {
|
|
1115
|
+
const {
|
|
1116
|
+
core: isACore,
|
|
1117
|
+
meta: isAMeta,
|
|
1118
|
+
alias: isAAlias,
|
|
1119
|
+
} = splitPipe(trimmed, lineNumber);
|
|
1074
1120
|
const isAMatch = !/^note(\s|$)/i.test(trimmed)
|
|
1075
1121
|
? isACore.match(IS_A_PATTERN)
|
|
1076
1122
|
: null;
|
|
@@ -1115,11 +1161,14 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1115
1161
|
continue;
|
|
1116
1162
|
}
|
|
1117
1163
|
|
|
1118
|
-
// TD-18: extract trailing `as <alias>`
|
|
1119
|
-
//
|
|
1120
|
-
//
|
|
1121
|
-
//
|
|
1122
|
-
//
|
|
1164
|
+
// TD-18: extract trailing `as <alias>` and register it. Order on the
|
|
1165
|
+
// line is `Name is a TYPE as <alias> [position: N]`. When same-line
|
|
1166
|
+
// metadata is present, splitPipe already peeled the alias (it must
|
|
1167
|
+
// precede the metadata) and handed it back as `isAAlias`; otherwise
|
|
1168
|
+
// the alias is still in `remainder` for us to peel here.
|
|
1169
|
+
if (isAAlias !== undefined) {
|
|
1170
|
+
nameAliasMap.set(isAAlias, id);
|
|
1171
|
+
}
|
|
1123
1172
|
const asInRemainder = remainder.match(
|
|
1124
1173
|
/^(.*?)\s*\bas\s+([A-Za-z][A-Za-z0-9_]{0,11})\s*$/
|
|
1125
1174
|
);
|
|
@@ -1129,9 +1178,21 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1129
1178
|
remainder = asInRemainder[1]!.trim();
|
|
1130
1179
|
}
|
|
1131
1180
|
|
|
1132
|
-
|
|
1133
|
-
//
|
|
1134
|
-
|
|
1181
|
+
// Position is colon-keyed metadata (§2.2) — pull it from the parsed
|
|
1182
|
+
// meta. A bare `position N` surviving in the remainder is the retired
|
|
1183
|
+
// form: flag it and drop the token (participant still registers).
|
|
1184
|
+
const position = takePosition(isAMeta, lineNumber);
|
|
1185
|
+
const baretail = remainder.match(/^position\s+(-?\d+)$/i);
|
|
1186
|
+
if (baretail) {
|
|
1187
|
+
result.diagnostics.push(
|
|
1188
|
+
makeDgmoError(
|
|
1189
|
+
lineNumber,
|
|
1190
|
+
sequenceBarePositionRemovedMessage(baretail[1]!),
|
|
1191
|
+
'error',
|
|
1192
|
+
METADATA_DIAGNOSTIC_CODES.SEQUENCE_BARE_POSITION_REMOVED
|
|
1193
|
+
)
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1135
1196
|
|
|
1136
1197
|
// Avoid duplicate participant declarations
|
|
1137
1198
|
const key = addParticipant(id, lineNumber, {
|
|
@@ -1157,17 +1218,26 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1157
1218
|
continue;
|
|
1158
1219
|
}
|
|
1159
1220
|
|
|
1160
|
-
//
|
|
1221
|
+
// Reject the retired standalone bare-keyword "Name position N"
|
|
1222
|
+
// (no "is a" type). Position is now colon-keyed `position: N` (§2.2),
|
|
1223
|
+
// which flows through the bare-participant path below. The participant
|
|
1224
|
+
// still registers (without an order override) so message refs resolve.
|
|
1161
1225
|
const { core: posCore, meta: posMeta } = splitPipe(trimmed, lineNumber);
|
|
1162
|
-
const posOnlyMatch = posCore.match(
|
|
1226
|
+
const posOnlyMatch = posCore.match(BARE_POSITION_PATTERN);
|
|
1163
1227
|
if (posOnlyMatch) {
|
|
1164
1228
|
contentStarted = true;
|
|
1165
1229
|
// Capture groups 1 and 2 guaranteed present after successful match.
|
|
1166
1230
|
const id = posOnlyMatch[1]!;
|
|
1167
|
-
|
|
1231
|
+
result.diagnostics.push(
|
|
1232
|
+
makeDgmoError(
|
|
1233
|
+
lineNumber,
|
|
1234
|
+
sequenceBarePositionRemovedMessage(posOnlyMatch[2]!),
|
|
1235
|
+
'error',
|
|
1236
|
+
METADATA_DIAGNOSTIC_CODES.SEQUENCE_BARE_POSITION_REMOVED
|
|
1237
|
+
)
|
|
1238
|
+
);
|
|
1168
1239
|
|
|
1169
1240
|
const key = addParticipant(id, lineNumber, {
|
|
1170
|
-
position,
|
|
1171
1241
|
...(posMeta !== undefined && { metadata: posMeta }),
|
|
1172
1242
|
});
|
|
1173
1243
|
// Track group membership
|
|
@@ -1222,7 +1292,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1222
1292
|
// Bare participant name — either inside an active group (indented) or top-level declaration
|
|
1223
1293
|
// Supports pipe metadata: " API | c: Gateway" or "Tapin2 | l:Park"
|
|
1224
1294
|
{
|
|
1225
|
-
const {
|
|
1295
|
+
const {
|
|
1296
|
+
core: bareCore,
|
|
1297
|
+
meta: bareMeta,
|
|
1298
|
+
alias: bareAlias,
|
|
1299
|
+
} = splitPipe(trimmed, lineNumber);
|
|
1226
1300
|
const inGroup = activeGroup && measureIndent(raw) > 0;
|
|
1227
1301
|
if (
|
|
1228
1302
|
/^\S+$/.test(bareCore) &&
|
|
@@ -1231,7 +1305,11 @@ export function parseSequenceDgmo(content: string): ParsedSequenceDgmo {
|
|
|
1231
1305
|
) {
|
|
1232
1306
|
contentStarted = true;
|
|
1233
1307
|
const id = bareCore;
|
|
1308
|
+
if (bareAlias !== undefined) nameAliasMap.set(bareAlias, id);
|
|
1309
|
+
// Colon-keyed `position: N` (§2.2) arrives as metadata here.
|
|
1310
|
+
const position = takePosition(bareMeta, lineNumber);
|
|
1234
1311
|
const key = addParticipant(id, lineNumber, {
|
|
1312
|
+
...(position !== undefined && { position }),
|
|
1235
1313
|
...(bareMeta !== undefined && { metadata: bareMeta }),
|
|
1236
1314
|
});
|
|
1237
1315
|
if (activeGroup && !activeGroup.participantIds.includes(key)) {
|