@diagrammo/dgmo 0.25.5 → 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 +4255 -2756
- package/dist/advanced.d.cts +285 -59
- package/dist/advanced.d.ts +285 -59
- package/dist/advanced.js +4253 -2750
- package/dist/auto.cjs +4051 -2589
- package/dist/auto.js +124 -122
- package/dist/auto.mjs +4051 -2589
- 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 +4076 -2591
- package/dist/index.d.cts +33 -8
- package/dist/index.d.ts +33 -8
- package/dist/index.js +4076 -2591
- package/dist/internal.cjs +4255 -2756
- package/dist/internal.d.cts +285 -59
- package/dist/internal.d.ts +285 -59
- package/dist/internal.js +4253 -2750
- 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 +3 -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 -1
- package/src/class/renderer.ts +58 -2
- package/src/class/types.ts +3 -0
- package/src/cli.ts +4 -4
- package/src/completion-types.ts +0 -1
- package/src/completion.ts +106 -51
- 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 -1
- 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 +35 -2
- package/src/graph/flowchart-renderer.ts +80 -52
- 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 +80 -52
- package/src/graph/types.ts +13 -0
- package/src/index.ts +1 -1
- package/src/infra/layout.ts +46 -26
- package/src/infra/parser.ts +1 -1
- 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/parser.ts +0 -14
- package/src/pert/renderer.ts +30 -43
- package/src/pyramid/renderer.ts +4 -5
- package/src/raci/renderer.ts +42 -70
- 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
|
|
@@ -515,6 +521,12 @@ export function renderRaci(
|
|
|
515
521
|
const colBottomY = cursorY + sColumnBottomPad;
|
|
516
522
|
const totalHeight = colBottomY + sVMargin;
|
|
517
523
|
|
|
524
|
+
// Export renders a fixed canvas (e.g. 1200×800); a short matrix would
|
|
525
|
+
// otherwise reserve a tall band of dead space below the last row. Size the
|
|
526
|
+
// export canvas to the content height. The interactive preview keeps the
|
|
527
|
+
// `max(pane, content)` behaviour so a short matrix still fills the pane.
|
|
528
|
+
const svgHeight = exportDims ? totalHeight : Math.max(height, totalHeight);
|
|
529
|
+
|
|
518
530
|
// ── SVG root ───────────────────────────────────────────────
|
|
519
531
|
|
|
520
532
|
const svg = d3Selection
|
|
@@ -522,8 +534,8 @@ export function renderRaci(
|
|
|
522
534
|
.append('svg')
|
|
523
535
|
.attr('xmlns', 'http://www.w3.org/2000/svg')
|
|
524
536
|
.attr('width', width)
|
|
525
|
-
.attr('height',
|
|
526
|
-
.attr('viewBox', `0 0 ${width} ${
|
|
537
|
+
.attr('height', svgHeight)
|
|
538
|
+
.attr('viewBox', `0 0 ${width} ${svgHeight}`)
|
|
527
539
|
.attr('preserveAspectRatio', 'xMidYMin meet')
|
|
528
540
|
.attr('font-family', FONT_FAMILY)
|
|
529
541
|
.style('background', 'transparent');
|
|
@@ -544,20 +556,21 @@ export function renderRaci(
|
|
|
544
556
|
|
|
545
557
|
// Size full-mode chips to actually fit the longest label without truncation.
|
|
546
558
|
// chip = 4 pad + 24 slab + 8 gap + label + 8 right pad.
|
|
547
|
-
const
|
|
548
|
-
(n, m) =>
|
|
559
|
+
const longestLabelPx = legendMarkers.reduce(
|
|
560
|
+
(n, m) =>
|
|
561
|
+
Math.max(n, measureText(variantLabels[m] ?? '', sLegendLabelFont)),
|
|
549
562
|
0
|
|
550
563
|
);
|
|
551
564
|
const fullChipW = Math.max(
|
|
552
565
|
sLegendChipLabelMin,
|
|
553
|
-
Math.ceil(4 + 24 + 8 +
|
|
566
|
+
Math.ceil(4 + 24 + 8 + longestLabelPx + 8)
|
|
554
567
|
);
|
|
555
568
|
const fullLegendW = numChips * fullChipW + chipGapTotal;
|
|
556
569
|
const letterLegendW = numChips * sLegendLetterChipW + chipGapTotal;
|
|
557
570
|
|
|
558
|
-
// Estimate title pixel width
|
|
571
|
+
// Estimate title pixel width using the shared glyph measurer.
|
|
559
572
|
const titleEstW =
|
|
560
|
-
parsed.title && !hideTitle ? parsed.title
|
|
573
|
+
parsed.title && !hideTitle ? measureText(parsed.title, sTitleFontSize) : 0;
|
|
561
574
|
|
|
562
575
|
// Pick legend mode: full chips if everything fits, else letter-only.
|
|
563
576
|
// If even letters won't fit beside the title, the title is truncated below.
|
|
@@ -590,7 +603,7 @@ export function renderRaci(
|
|
|
590
603
|
.attr('font-size', sTitleFontSize)
|
|
591
604
|
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
592
605
|
.attr('fill', palette.text)
|
|
593
|
-
.text(
|
|
606
|
+
.text(truncateText(parsed.title, sTitleFontSize, titleMaxW));
|
|
594
607
|
}
|
|
595
608
|
|
|
596
609
|
// Legend — right-aligned in the same row.
|
|
@@ -708,10 +721,10 @@ export function renderRaci(
|
|
|
708
721
|
.attr('font-weight', 600)
|
|
709
722
|
.attr('fill', palette.text)
|
|
710
723
|
.text(
|
|
711
|
-
|
|
724
|
+
truncateText(
|
|
712
725
|
parsed.roleDisplayNames[i] ?? '',
|
|
713
|
-
|
|
714
|
-
|
|
726
|
+
sRoleHeaderFont,
|
|
727
|
+
roleColW - 2 * sCellPad
|
|
715
728
|
)
|
|
716
729
|
);
|
|
717
730
|
});
|
|
@@ -925,7 +938,7 @@ function renderLegend(
|
|
|
925
938
|
if (mode === 'letters') {
|
|
926
939
|
// Compact: a single colored pill with just the marker letter.
|
|
927
940
|
// The full label moves to a native tooltip so hover still teaches it.
|
|
928
|
-
const fill = solid ? rawColor : mix(rawColor, surfaceBg,
|
|
941
|
+
const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
|
|
929
942
|
const stroke = solid ? mix(rawColor, surfaceBg, 70) : rawColor;
|
|
930
943
|
chipG
|
|
931
944
|
.append('rect')
|
|
@@ -960,7 +973,7 @@ function renderLegend(
|
|
|
960
973
|
}
|
|
961
974
|
|
|
962
975
|
// Full mode: bordered chip with a letter slab on the left and label text.
|
|
963
|
-
const fill = solid ? rawColor : mix(rawColor, surfaceBg,
|
|
976
|
+
const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
|
|
964
977
|
const stroke = mix(rawColor, surfaceBg, 70);
|
|
965
978
|
|
|
966
979
|
chipG
|
|
@@ -1020,10 +1033,10 @@ function renderLegend(
|
|
|
1020
1033
|
.attr('font-weight', 600)
|
|
1021
1034
|
.attr('fill', palette.text)
|
|
1022
1035
|
.text(
|
|
1023
|
-
|
|
1036
|
+
truncateText(
|
|
1024
1037
|
labelText,
|
|
1025
|
-
|
|
1026
|
-
|
|
1038
|
+
sLegendLabelFont,
|
|
1039
|
+
chipW - slabW - slabPad * 2 - 12
|
|
1027
1040
|
)
|
|
1028
1041
|
);
|
|
1029
1042
|
});
|
|
@@ -1117,7 +1130,7 @@ function renderPhaseBar(
|
|
|
1117
1130
|
// can see at a glance what's in the rolled-up phase.
|
|
1118
1131
|
const taskCount = phase.tasks.length;
|
|
1119
1132
|
if (taskCount > 0) {
|
|
1120
|
-
const labelTextWidth = phase.displayName
|
|
1133
|
+
const labelTextWidth = measureText(phase.displayName, sPhaseFont);
|
|
1121
1134
|
phaseG
|
|
1122
1135
|
.append('text')
|
|
1123
1136
|
.attr('x', x + 26 + labelTextWidth + 10)
|
|
@@ -1166,7 +1179,7 @@ function renderPhaseBar(
|
|
|
1166
1179
|
// corner radius. Stroke width is a touch thinner than
|
|
1167
1180
|
// NODE_STROKE_WIDTH because at the smaller summary scale the
|
|
1168
1181
|
// full 1.5 reads as too heavy.
|
|
1169
|
-
const fill = solid ? rawColor : mix(rawColor, surfaceBg,
|
|
1182
|
+
const fill = solid ? rawColor : mix(rawColor, surfaceBg, TINT_PCT);
|
|
1170
1183
|
const stroke = solid ? mix(rawColor, surfaceBg, 70) : rawColor;
|
|
1171
1184
|
const chipG = phaseG.append('g').attr('class', 'raci-phase-summary');
|
|
1172
1185
|
chipG
|
|
@@ -1452,7 +1465,7 @@ function renderTaskRow(
|
|
|
1452
1465
|
// ("Responsible") instead of the bare letter. Same primitive as the
|
|
1453
1466
|
// legend chip, so cells and legend read as the same UI element.
|
|
1454
1467
|
const fullLabel = variantLabels[m] ?? m;
|
|
1455
|
-
const labelPx = fullLabel
|
|
1468
|
+
const labelPx = measureText(fullLabel, sLegendLabelFont);
|
|
1456
1469
|
const showFullLabel = labelPx + 16 <= sliceW;
|
|
1457
1470
|
const textContent = showFullLabel ? fullLabel : m;
|
|
1458
1471
|
const textFont = showFullLabel ? sLegendLabelFont : sMarkerFont;
|
|
@@ -1505,45 +1518,6 @@ function renderTaskRow(
|
|
|
1505
1518
|
|
|
1506
1519
|
// ── Helpers ──────────────────────────────────────────────────
|
|
1507
1520
|
|
|
1508
|
-
/**
|
|
1509
|
-
* Greedy word-wrap to a per-line character cap. Whitespace runs that
|
|
1510
|
-
* happen to fall at a wrap point are dropped (no leading-space lines).
|
|
1511
|
-
* Words longer than the cap are hard-split so the output never exceeds.
|
|
1512
|
-
*/
|
|
1513
|
-
function wordWrap(s: string, charsPerLine: number): string[] {
|
|
1514
|
-
if (charsPerLine <= 0 || s.length <= charsPerLine) return [s];
|
|
1515
|
-
const out: string[] = [];
|
|
1516
|
-
const tokens = s.split(/(\s+)/);
|
|
1517
|
-
let cur = '';
|
|
1518
|
-
for (const tok of tokens) {
|
|
1519
|
-
if (!tok) continue;
|
|
1520
|
-
const isSpace = /^\s+$/.test(tok);
|
|
1521
|
-
if (cur.length + tok.length <= charsPerLine) {
|
|
1522
|
-
cur += tok;
|
|
1523
|
-
continue;
|
|
1524
|
-
}
|
|
1525
|
-
if (cur.trimEnd().length > 0) out.push(cur.trimEnd());
|
|
1526
|
-
if (!isSpace && tok.length > charsPerLine) {
|
|
1527
|
-
let chunk = tok;
|
|
1528
|
-
while (chunk.length > charsPerLine) {
|
|
1529
|
-
out.push(chunk.slice(0, charsPerLine));
|
|
1530
|
-
chunk = chunk.slice(charsPerLine);
|
|
1531
|
-
}
|
|
1532
|
-
cur = chunk;
|
|
1533
|
-
} else {
|
|
1534
|
-
cur = isSpace ? '' : tok;
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
if (cur.trimEnd().length > 0) out.push(cur.trimEnd());
|
|
1538
|
-
return out.length > 0 ? out : [''];
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
function wrapText(s: string, maxPx: number, fontSize: number): string[] {
|
|
1542
|
-
const charPx = fontSize * 0.6;
|
|
1543
|
-
const cap = Math.max(8, Math.floor(maxPx / charPx));
|
|
1544
|
-
return wordWrap(s, cap);
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
1521
|
/**
|
|
1548
1522
|
* Wrapped text content for a task row plus the height that fits it.
|
|
1549
1523
|
* Pre-computed once per task and reused by both the y-cursor layout
|
|
@@ -1575,13 +1549,17 @@ function prepareRowContent(
|
|
|
1575
1549
|
sStackTopGap = STACK_TOP_GAP,
|
|
1576
1550
|
sViolationLineHeight = VIOLATION_LINE_HEIGHT
|
|
1577
1551
|
): RowContent {
|
|
1578
|
-
const nameLines =
|
|
1552
|
+
const nameLines = wrapTextToWidth(task.displayName, sLabelFont, labelMaxW, {
|
|
1553
|
+
hardBreak: true,
|
|
1554
|
+
});
|
|
1579
1555
|
const description = task.description?.trim() ?? '';
|
|
1580
1556
|
const descLines =
|
|
1581
1557
|
description.length > 0
|
|
1582
1558
|
? description
|
|
1583
1559
|
.split('\n')
|
|
1584
|
-
.flatMap((line) =>
|
|
1560
|
+
.flatMap((line) =>
|
|
1561
|
+
wrapTextToWidth(line, sDescFont, labelMaxW, { hardBreak: true })
|
|
1562
|
+
)
|
|
1585
1563
|
: [];
|
|
1586
1564
|
const violations: RowContent['violations'] = [];
|
|
1587
1565
|
if (bucket) {
|
|
@@ -1595,7 +1573,9 @@ function prepareRowContent(
|
|
|
1595
1573
|
severity,
|
|
1596
1574
|
sourceLine: e.line,
|
|
1597
1575
|
text,
|
|
1598
|
-
lines:
|
|
1576
|
+
lines: wrapTextToWidth(text, sLabelFont - 2, labelMaxW, {
|
|
1577
|
+
hardBreak: true,
|
|
1578
|
+
}),
|
|
1599
1579
|
});
|
|
1600
1580
|
}
|
|
1601
1581
|
};
|
|
@@ -1621,14 +1601,6 @@ function prepareRowContent(
|
|
|
1621
1601
|
return { nameLines, descLines, violations, rowHeight };
|
|
1622
1602
|
}
|
|
1623
1603
|
|
|
1624
|
-
function truncateForWidth(s: string, maxPx: number, fontSize: number): string {
|
|
1625
|
-
// Conservative: 0.6 em per char for sans-serif at this weight.
|
|
1626
|
-
const charPx = fontSize * 0.6;
|
|
1627
|
-
const cap = Math.max(3, Math.floor(maxPx / charPx));
|
|
1628
|
-
if (s.length <= cap) return s;
|
|
1629
|
-
return s.substring(0, cap - 1) + '…';
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
1604
|
/**
|
|
1633
1605
|
* Split a diagnostic message at single-quoted spans so the renderer can
|
|
1634
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)) {
|