@diagrammo/dgmo 0.31.0 → 0.32.1
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/.cursorrules +4 -1
- package/.github/copilot-instructions.md +4 -1
- package/.windsurfrules +4 -1
- package/SKILL.md +4 -1
- package/dist/advanced.cjs +1297 -358
- package/dist/advanced.d.cts +117 -15
- package/dist/advanced.d.ts +117 -15
- package/dist/advanced.js +1291 -358
- package/dist/auto.cjs +1087 -316
- package/dist/auto.js +98 -98
- package/dist/auto.mjs +1087 -316
- package/dist/cli.cjs +140 -140
- package/dist/index.cjs +1090 -397
- package/dist/index.js +1090 -397
- package/docs/ai-integration.md +4 -1
- package/docs/language-reference.md +282 -27
- package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
- package/gallery/fixtures/c4-full.dgmo +4 -5
- package/gallery/fixtures/c4.dgmo +2 -3
- package/package.json +7 -1
- package/src/advanced.ts +7 -0
- package/src/boxes-and-lines/focus.ts +257 -0
- package/src/boxes-and-lines/layout-search.ts +131 -65
- package/src/boxes-and-lines/layout.ts +7 -1
- package/src/boxes-and-lines/parser.ts +19 -4
- package/src/boxes-and-lines/renderer.ts +54 -3
- package/src/c4/parser.ts +8 -7
- package/src/chart-type-registry.ts +129 -4
- package/src/chart-types.ts +4 -4
- package/src/chart.ts +18 -1
- package/src/colors.ts +225 -2
- package/src/cycle/parser.ts +2 -7
- package/src/d3.ts +67 -54
- package/src/diagnostics.ts +17 -0
- package/src/dimensions.ts +9 -13
- package/src/echarts.ts +42 -14
- package/src/er/parser.ts +6 -1
- package/src/gantt/parser.ts +44 -7
- package/src/graph/flowchart-parser.ts +77 -3
- package/src/graph/state-renderer.ts +2 -2
- package/src/infra/parser.ts +80 -0
- package/src/journey-map/parser.ts +8 -7
- package/src/kanban/parser.ts +8 -7
- package/src/map/context-labels.ts +134 -27
- package/src/map/geo.ts +10 -2
- package/src/map/layout.ts +259 -4
- package/src/map/parser.ts +2 -0
- package/src/map/renderer.ts +22 -11
- package/src/map/resolver.ts +68 -19
- package/src/mindmap/parser.ts +15 -7
- package/src/mindmap/renderer.ts +50 -12
- package/src/org/parser.ts +8 -7
- package/src/org/renderer.ts +22 -7
- package/src/palettes/color-utils.ts +12 -2
- package/src/palettes/index.ts +1 -0
- package/src/pert/renderer.ts +2 -2
- package/src/pyramid/parser.ts +2 -7
- package/src/quadrant/renderer.ts +2 -2
- package/src/raci/parser.ts +2 -7
- package/src/raci/renderer.ts +4 -4
- package/src/ring/parser.ts +2 -7
- package/src/sequence/parser.ts +18 -7
- package/src/sequence/renderer.ts +4 -4
- package/src/sitemap/parser.ts +8 -7
- package/src/sitemap/renderer.ts +2 -2
- package/src/tech-radar/parser.ts +2 -7
- package/src/timeline/renderer.ts +15 -5
- package/src/utils/parsing.ts +13 -1
- package/src/utils/scaling.ts +38 -81
- package/src/utils/tag-groups.ts +38 -0
- package/src/visualizations/parse.ts +6 -1
- package/src/wireframe/parser.ts +6 -1
package/src/sequence/renderer.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as d3Selection from 'd3-selection';
|
|
6
6
|
import type { PaletteColors } from '../palettes';
|
|
7
|
-
import { contrastText, mix, shapeFill } from '../palettes/color-utils';
|
|
7
|
+
import { contrastText, mix, shapeFill, themeBaseBg } from '../palettes/color-utils';
|
|
8
8
|
import {
|
|
9
9
|
parseInlineMarkdown,
|
|
10
10
|
truncateBareUrl,
|
|
@@ -2010,7 +2010,7 @@ export function renderSequenceDiagram(
|
|
|
2010
2010
|
const fillColor = groupTagColor
|
|
2011
2011
|
? mix(
|
|
2012
2012
|
groupTagColor,
|
|
2013
|
-
|
|
2013
|
+
themeBaseBg(palette, isDark),
|
|
2014
2014
|
isDark ? 15 : 20
|
|
2015
2015
|
)
|
|
2016
2016
|
: isDark
|
|
@@ -2139,7 +2139,7 @@ export function renderSequenceDiagram(
|
|
|
2139
2139
|
const pFill = effectiveTagColor
|
|
2140
2140
|
? mix(
|
|
2141
2141
|
effectiveTagColor,
|
|
2142
|
-
|
|
2142
|
+
themeBaseBg(palette, isDark),
|
|
2143
2143
|
isDark ? 30 : 40
|
|
2144
2144
|
)
|
|
2145
2145
|
: isDark
|
|
@@ -2507,7 +2507,7 @@ export function renderSequenceDiagram(
|
|
|
2507
2507
|
.attr('y', y1)
|
|
2508
2508
|
.attr('width', sActivationWidth)
|
|
2509
2509
|
.attr('height', y2 - y1)
|
|
2510
|
-
.attr('fill',
|
|
2510
|
+
.attr('fill', themeBaseBg(palette, isDark));
|
|
2511
2511
|
|
|
2512
2512
|
// Canonical 25% tint via shapeFill() (or full intent when solid-fill is on).
|
|
2513
2513
|
const actFill = shapeFill(palette, actBaseColor, isDark, { solid });
|
package/src/sitemap/parser.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
descriptionBareRemovedMessage,
|
|
9
9
|
formatDgmoError,
|
|
10
10
|
makeDgmoError,
|
|
11
|
+
makeFail,
|
|
11
12
|
METADATA_DIAGNOSTIC_CODES,
|
|
12
13
|
pipeOperatorRemovedMessage,
|
|
13
14
|
suggest,
|
|
@@ -165,12 +166,7 @@ export function parseSitemap(
|
|
|
165
166
|
error: null,
|
|
166
167
|
};
|
|
167
168
|
|
|
168
|
-
const fail = (
|
|
169
|
-
const diag = makeDgmoError(line, message);
|
|
170
|
-
result.diagnostics.push(diag);
|
|
171
|
-
result.error = formatDgmoError(diag);
|
|
172
|
-
return result;
|
|
173
|
-
};
|
|
169
|
+
const fail = makeFail(result);
|
|
174
170
|
|
|
175
171
|
const pushError = (line: number, message: string): void => {
|
|
176
172
|
const diag = makeDgmoError(line, message);
|
|
@@ -324,7 +320,12 @@ export function parseSitemap(
|
|
|
324
320
|
const indent = measureIndent(line);
|
|
325
321
|
if (indent > 0) {
|
|
326
322
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
327
|
-
const { label, color } = extractColor(
|
|
323
|
+
const { label, color } = extractColor(
|
|
324
|
+
cleanEntry,
|
|
325
|
+
palette,
|
|
326
|
+
result.diagnostics,
|
|
327
|
+
lineNumber
|
|
328
|
+
);
|
|
328
329
|
// Bare value (no explicit color) → keep it; finalized below.
|
|
329
330
|
currentTagGroup.entries.push({
|
|
330
331
|
value: label,
|
package/src/sitemap/renderer.ts
CHANGED
|
@@ -6,7 +6,7 @@ import * as d3Selection from 'd3-selection';
|
|
|
6
6
|
import * as d3Shape from 'd3-shape';
|
|
7
7
|
import { FONT_FAMILY } from '../fonts';
|
|
8
8
|
import type { PaletteColors } from '../palettes';
|
|
9
|
-
import { contrastText, mix, shapeFill } from '../palettes/color-utils';
|
|
9
|
+
import { contrastText, mix, shapeFill, themeBaseBg } from '../palettes/color-utils';
|
|
10
10
|
import type { ParsedSitemap } from './types';
|
|
11
11
|
import type { SitemapLayoutResult, SitemapLegendGroup } from './layout';
|
|
12
12
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
@@ -83,7 +83,7 @@ function containerFill(
|
|
|
83
83
|
nodeColor?: string
|
|
84
84
|
): string {
|
|
85
85
|
if (nodeColor) {
|
|
86
|
-
return mix(nodeColor,
|
|
86
|
+
return mix(nodeColor, themeBaseBg(palette, isDark), 10);
|
|
87
87
|
}
|
|
88
88
|
return mix(palette.surface, palette.bg, 40);
|
|
89
89
|
}
|
package/src/tech-radar/parser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
formatDgmoError,
|
|
3
2
|
makeDgmoError,
|
|
3
|
+
makeFail,
|
|
4
4
|
METADATA_DIAGNOSTIC_CODES,
|
|
5
5
|
pipeOperatorRemovedMessage,
|
|
6
6
|
suggest,
|
|
@@ -72,12 +72,7 @@ export function parseTechRadar(content: string): ParsedTechRadar {
|
|
|
72
72
|
error: null,
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
const fail = (
|
|
76
|
-
const diag = makeDgmoError(line, message);
|
|
77
|
-
result.diagnostics.push(diag);
|
|
78
|
-
result.error = formatDgmoError(diag);
|
|
79
|
-
return result;
|
|
80
|
-
};
|
|
75
|
+
const fail = makeFail(result);
|
|
81
76
|
|
|
82
77
|
const warn = (line: number, message: string): void => {
|
|
83
78
|
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
package/src/timeline/renderer.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
import { parseTimelineDate } from '../timeline/parser';
|
|
25
25
|
import type { PaletteColors } from '../palettes';
|
|
26
26
|
import { getSeriesColors } from '../palettes';
|
|
27
|
-
import { mix, shapeFill } from '../palettes/color-utils';
|
|
27
|
+
import { mix, shapeFill, themeBaseBg } from '../palettes/color-utils';
|
|
28
28
|
import { resolveTagColor } from '../utils/tag-groups';
|
|
29
29
|
import type { TagGroup } from '../utils/tag-groups';
|
|
30
30
|
import {
|
|
@@ -808,7 +808,7 @@ function setupTimeline(
|
|
|
808
808
|
const textColor = palette.text;
|
|
809
809
|
const mutedColor = palette.border;
|
|
810
810
|
const bgColor = palette.bg;
|
|
811
|
-
const bg =
|
|
811
|
+
const bg = themeBaseBg(palette, isDark);
|
|
812
812
|
const colors = getSeriesColors(palette);
|
|
813
813
|
|
|
814
814
|
const groupColorMap = new Map<string, string>();
|
|
@@ -1451,7 +1451,7 @@ function renderTimelineHorizontalTimeSort(
|
|
|
1451
1451
|
setup: TimelineSetup,
|
|
1452
1452
|
hovers: TimelineHoverHelpers,
|
|
1453
1453
|
onClickItem: ((lineNumber: number) => void) | undefined,
|
|
1454
|
-
|
|
1454
|
+
exportDims: D3ExportDimensions | undefined,
|
|
1455
1455
|
_swimlaneTagGroup: string | null | undefined,
|
|
1456
1456
|
_activeTagGroup: string | null | undefined,
|
|
1457
1457
|
_onTagStateChange:
|
|
@@ -1461,6 +1461,7 @@ function renderTimelineHorizontalTimeSort(
|
|
|
1461
1461
|
): void {
|
|
1462
1462
|
const {
|
|
1463
1463
|
width,
|
|
1464
|
+
height,
|
|
1464
1465
|
tooltip,
|
|
1465
1466
|
solid,
|
|
1466
1467
|
textColor,
|
|
@@ -1538,6 +1539,15 @@ function renderTimelineHorizontalTimeSort(
|
|
|
1538
1539
|
const innerHeight = rowH * sorted.length;
|
|
1539
1540
|
const usedHeight = margin.top + innerHeight + margin.bottom;
|
|
1540
1541
|
|
|
1542
|
+
// On-screen (non-export) the content can be taller than the host pane. Rather
|
|
1543
|
+
// than overflow it (forcing the user to scroll), scale the whole SVG down to
|
|
1544
|
+
// fit the available height: the viewBox keeps the natural content geometry
|
|
1545
|
+
// while the rendered height is clamped to the container, and
|
|
1546
|
+
// preserveAspectRatio uniformly shrinks + centers it. Export keeps the full
|
|
1547
|
+
// natural height so the image is never compressed.
|
|
1548
|
+
const fitToContainer = !exportDims && height > 0 && usedHeight > height;
|
|
1549
|
+
const svgHeight = fitToContainer ? height : usedHeight;
|
|
1550
|
+
|
|
1541
1551
|
const xScale = d3Scale
|
|
1542
1552
|
.scaleLinear()
|
|
1543
1553
|
.domain([minDate - datePadding, maxDate + datePadding])
|
|
@@ -1547,12 +1557,12 @@ function renderTimelineHorizontalTimeSort(
|
|
|
1547
1557
|
.select(container)
|
|
1548
1558
|
.append('svg')
|
|
1549
1559
|
.attr('width', width)
|
|
1550
|
-
.attr('height',
|
|
1560
|
+
.attr('height', svgHeight)
|
|
1551
1561
|
.attr('viewBox', `0 0 ${width} ${usedHeight}`)
|
|
1552
1562
|
.attr('preserveAspectRatio', 'xMidYMin meet')
|
|
1553
1563
|
.style('background', bgColor);
|
|
1554
1564
|
|
|
1555
|
-
if (ctx.isBelowFloor) {
|
|
1565
|
+
if (ctx.isBelowFloor && !fitToContainer) {
|
|
1556
1566
|
svg.attr('width', '100%');
|
|
1557
1567
|
}
|
|
1558
1568
|
|
package/src/utils/parsing.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
RECOGNIZED_COLOR_NAMES,
|
|
9
9
|
resolveColor,
|
|
10
10
|
resolveColorWithDiagnostic,
|
|
11
|
+
invalidColorDiagnostic,
|
|
11
12
|
} from '../colors';
|
|
12
13
|
import {
|
|
13
14
|
emptyMetadataValueMessage,
|
|
@@ -122,7 +123,18 @@ export function extractColor(
|
|
|
122
123
|
if (lastSpaceIdx < 0) return { label };
|
|
123
124
|
const trailing = label.substring(lastSpaceIdx + 1);
|
|
124
125
|
// Case-sensitive lowercase match against the closed 11-name palette.
|
|
125
|
-
if (!RECOGNIZED_COLOR_SET.has(trailing))
|
|
126
|
+
if (!RECOGNIZED_COLOR_SET.has(trailing)) {
|
|
127
|
+
// Not a valid color. If it nonetheless LOOKS like an intended color (a hex
|
|
128
|
+
// literal or a CSS color name like `pink`), flag it — otherwise the
|
|
129
|
+
// trailing-token rule would silently swallow it into the label with no
|
|
130
|
+
// diagnostic, and the MCP color gate would have nothing to block on. A
|
|
131
|
+
// genuine label word (`Zinfandel`) is not color-like, so it stays as-is.
|
|
132
|
+
if (diagnostics && line !== undefined) {
|
|
133
|
+
const diag = invalidColorDiagnostic(trailing, line);
|
|
134
|
+
if (diag) diagnostics.push(diag);
|
|
135
|
+
}
|
|
136
|
+
return { label };
|
|
137
|
+
}
|
|
126
138
|
let color: string | undefined;
|
|
127
139
|
if (diagnostics && line !== undefined) {
|
|
128
140
|
color = resolveColorWithDiagnostic(trailing, line, diagnostics, palette);
|
package/src/utils/scaling.ts
CHANGED
|
@@ -21,6 +21,41 @@ export class ScaleContext {
|
|
|
21
21
|
return new ScaleContext(clamped, minScaleFactor);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Fit content into a bounding box, scaling by whichever dimension is more
|
|
26
|
+
* constraining (the smaller of the width- and height-fit ratios) so the
|
|
27
|
+
* diagram never overflows the canvas in either axis. Like {@link from}, the
|
|
28
|
+
* factor is clamped to `[minScaleFactor, 1]` (content is never enlarged, and
|
|
29
|
+
* never shrunk past the readability floor).
|
|
30
|
+
*/
|
|
31
|
+
static fromBox(
|
|
32
|
+
containerWidth: number,
|
|
33
|
+
idealWidth: number,
|
|
34
|
+
containerHeight: number,
|
|
35
|
+
idealHeight: number,
|
|
36
|
+
minScaleFactor = DEFAULT_MIN_SCALE_FACTOR
|
|
37
|
+
): ScaleContext {
|
|
38
|
+
const wRaw = idealWidth > 0 ? containerWidth / idealWidth : 1;
|
|
39
|
+
const hRaw = idealHeight > 0 ? containerHeight / idealHeight : 1;
|
|
40
|
+
const raw = Math.min(wRaw, hRaw);
|
|
41
|
+
const clamped = Math.max(Math.min(raw, 1), minScaleFactor);
|
|
42
|
+
return new ScaleContext(clamped, minScaleFactor);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build a context from an explicit raw factor (clamped to
|
|
47
|
+
* `[minScaleFactor, 1]`). Used to refine a fit iteratively: layout scaling is
|
|
48
|
+
* non-linear (gaps shrink faster than floored text), so the first-pass factor
|
|
49
|
+
* can still overflow — re-measure the laid-out result and tighten.
|
|
50
|
+
*/
|
|
51
|
+
static fromFactor(
|
|
52
|
+
rawFactor: number,
|
|
53
|
+
minScaleFactor = DEFAULT_MIN_SCALE_FACTOR
|
|
54
|
+
): ScaleContext {
|
|
55
|
+
const clamped = Math.max(Math.min(rawFactor, 1), minScaleFactor);
|
|
56
|
+
return new ScaleContext(clamped, minScaleFactor);
|
|
57
|
+
}
|
|
58
|
+
|
|
24
59
|
static identity(): ScaleContext {
|
|
25
60
|
return new ScaleContext(1, DEFAULT_MIN_SCALE_FACTOR);
|
|
26
61
|
}
|
|
@@ -42,7 +77,9 @@ export class ScaleContext {
|
|
|
42
77
|
}
|
|
43
78
|
|
|
44
79
|
// ============================================================
|
|
45
|
-
//
|
|
80
|
+
// ContentCounts — per-chart-type content tallies that feed the registry's
|
|
81
|
+
// `minDims` formulas (chart-type-registry.ts). The formulas themselves moved
|
|
82
|
+
// there (Story 111.5) so a chart type's sizing is defined in one descriptor.
|
|
46
83
|
// ============================================================
|
|
47
84
|
|
|
48
85
|
export interface ContentCounts {
|
|
@@ -57,83 +94,3 @@ export interface ContentCounts {
|
|
|
57
94
|
roles?: number;
|
|
58
95
|
blips?: number;
|
|
59
96
|
}
|
|
60
|
-
|
|
61
|
-
const DEFAULT_MIN = { width: 300, height: 200 };
|
|
62
|
-
|
|
63
|
-
export function computeMinDimensions(
|
|
64
|
-
chartType: string,
|
|
65
|
-
counts: ContentCounts
|
|
66
|
-
): { width: number; height: number } {
|
|
67
|
-
switch (chartType) {
|
|
68
|
-
case 'sequence':
|
|
69
|
-
return {
|
|
70
|
-
width: Math.max((counts.participants ?? 2) * 80, 320),
|
|
71
|
-
height: Math.max((counts.messages ?? 1) * 20 + 120, 200),
|
|
72
|
-
};
|
|
73
|
-
case 'raci':
|
|
74
|
-
return {
|
|
75
|
-
width: Math.max((counts.roles ?? 2) * 50 + 180, 300),
|
|
76
|
-
height: Math.max((counts.tasks ?? 1) * 28 + 80, 200),
|
|
77
|
-
};
|
|
78
|
-
case 'mindmap':
|
|
79
|
-
return {
|
|
80
|
-
width: Math.max((counts.nodes ?? 3) * 30, 300),
|
|
81
|
-
height: Math.max((counts.depth ?? 2) * 60, 200),
|
|
82
|
-
};
|
|
83
|
-
case 'tech-radar':
|
|
84
|
-
return { width: 360, height: 400 };
|
|
85
|
-
case 'heatmap':
|
|
86
|
-
return {
|
|
87
|
-
width: Math.max((counts.columns ?? 3) * 40, 300),
|
|
88
|
-
height: Math.max((counts.rows ?? 3) * 30 + 60, 200),
|
|
89
|
-
};
|
|
90
|
-
case 'arc':
|
|
91
|
-
return {
|
|
92
|
-
width: 300,
|
|
93
|
-
height: Math.max((counts.nodes ?? 3) * 20 + 120, 200),
|
|
94
|
-
};
|
|
95
|
-
case 'org':
|
|
96
|
-
return {
|
|
97
|
-
width: Math.max((counts.nodes ?? 3) * 60, 300),
|
|
98
|
-
height: Math.max((counts.depth ?? 2) * 80, 200),
|
|
99
|
-
};
|
|
100
|
-
case 'gantt':
|
|
101
|
-
return {
|
|
102
|
-
width: 400,
|
|
103
|
-
height: Math.max((counts.tasks ?? 3) * 24 + 80, 200),
|
|
104
|
-
};
|
|
105
|
-
case 'kanban':
|
|
106
|
-
return {
|
|
107
|
-
width: Math.max((counts.columns ?? 3) * 120, 360),
|
|
108
|
-
height: 300,
|
|
109
|
-
};
|
|
110
|
-
case 'er':
|
|
111
|
-
return {
|
|
112
|
-
width: Math.max((counts.nodes ?? 2) * 140, 300),
|
|
113
|
-
height: Math.max((counts.nodes ?? 2) * 80, 200),
|
|
114
|
-
};
|
|
115
|
-
case 'class':
|
|
116
|
-
return {
|
|
117
|
-
width: Math.max((counts.nodes ?? 2) * 140, 300),
|
|
118
|
-
height: Math.max((counts.nodes ?? 2) * 80, 200),
|
|
119
|
-
};
|
|
120
|
-
case 'flowchart':
|
|
121
|
-
case 'state':
|
|
122
|
-
return {
|
|
123
|
-
width: Math.max((counts.nodes ?? 3) * 60, 300),
|
|
124
|
-
height: Math.max((counts.nodes ?? 3) * 50, 200),
|
|
125
|
-
};
|
|
126
|
-
case 'pert':
|
|
127
|
-
return {
|
|
128
|
-
width: Math.max((counts.tasks ?? 3) * 80, 340),
|
|
129
|
-
height: Math.max((counts.tasks ?? 3) * 40 + 80, 200),
|
|
130
|
-
};
|
|
131
|
-
case 'infra':
|
|
132
|
-
return {
|
|
133
|
-
width: Math.max((counts.nodes ?? 3) * 80, 300),
|
|
134
|
-
height: Math.max((counts.nodes ?? 3) * 60, 200),
|
|
135
|
-
};
|
|
136
|
-
default:
|
|
137
|
-
return { ...DEFAULT_MIN };
|
|
138
|
-
}
|
|
139
|
-
}
|
package/src/utils/tag-groups.ts
CHANGED
|
@@ -572,6 +572,44 @@ export function validateTagGroupNames(
|
|
|
572
572
|
}
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
+
// ── Parent → Child Tag Cascade ────────────────────────────
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Cascade explicit tag values down a node tree: a child that has no value of
|
|
579
|
+
* its own for a given tag group inherits the value of its nearest ancestor
|
|
580
|
+
* that does. A child's own explicit value always wins and becomes the new
|
|
581
|
+
* inherited value for its subtree.
|
|
582
|
+
*
|
|
583
|
+
* Run this on the parsed tree BEFORE {@link injectDefaultTagMetadata} so that
|
|
584
|
+
* an inherited ancestor value takes precedence over the group's global
|
|
585
|
+
* default — only nodes with no tagged ancestor fall through to the default.
|
|
586
|
+
* Idempotent and mutates `metadata` in place.
|
|
587
|
+
*
|
|
588
|
+
* @param roots Root nodes of the tree (each with mutable `metadata` + `children`)
|
|
589
|
+
* @param tagGroups Declared tag groups (only `.name` is used)
|
|
590
|
+
*/
|
|
591
|
+
export function cascadeTagMetadata<
|
|
592
|
+
T extends { metadata: Record<string, string>; children: readonly T[] },
|
|
593
|
+
>(roots: readonly T[], tagGroups: ReadonlyArray<{ name: string }>): void {
|
|
594
|
+
const keys = tagGroups.map((g) => g.name.toLowerCase());
|
|
595
|
+
if (keys.length === 0) return;
|
|
596
|
+
|
|
597
|
+
const walk = (node: T, inherited: Record<string, string>): void => {
|
|
598
|
+
const childInherited = { ...inherited };
|
|
599
|
+
for (const key of keys) {
|
|
600
|
+
const own = node.metadata[key];
|
|
601
|
+
if (own) {
|
|
602
|
+
childInherited[key] = own; // own explicit value propagates downward
|
|
603
|
+
} else if (inherited[key]) {
|
|
604
|
+
node.metadata[key] = inherited[key]; // inherit from nearest ancestor
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for (const child of node.children) walk(child, childInherited);
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
for (const root of roots) walk(root, {});
|
|
611
|
+
}
|
|
612
|
+
|
|
575
613
|
// ── Default Metadata Injection ────────────────────────────
|
|
576
614
|
|
|
577
615
|
/**
|
|
@@ -215,7 +215,12 @@ function parseVisualizationFull(
|
|
|
215
215
|
// Timeline tag group entries (indented under tag heading)
|
|
216
216
|
if (currentTimelineTagGroup && indent > 0) {
|
|
217
217
|
const { text: entryText, isDefault } = stripDefaultModifier(line);
|
|
218
|
-
const { label, color } = extractColor(
|
|
218
|
+
const { label, color } = extractColor(
|
|
219
|
+
entryText,
|
|
220
|
+
palette,
|
|
221
|
+
result.diagnostics,
|
|
222
|
+
lineNumber
|
|
223
|
+
);
|
|
219
224
|
if (color) {
|
|
220
225
|
if (isDefault) {
|
|
221
226
|
currentTimelineTagGroup.defaultValue = label;
|
package/src/wireframe/parser.ts
CHANGED
|
@@ -895,7 +895,12 @@ export function parseWireframe(content: string): ParsedWireframe {
|
|
|
895
895
|
// Indented tag entry: `Value color` or `Value color default`
|
|
896
896
|
if (indent > 0 && currentTagGroup) {
|
|
897
897
|
const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
|
|
898
|
-
const { label, color } = extractColor(
|
|
898
|
+
const { label, color } = extractColor(
|
|
899
|
+
cleanEntry,
|
|
900
|
+
undefined,
|
|
901
|
+
diagnostics,
|
|
902
|
+
lineNumber
|
|
903
|
+
);
|
|
899
904
|
// Bare value (no explicit color) → keep it; finalized below.
|
|
900
905
|
currentTagGroup.entries.push({
|
|
901
906
|
value: label,
|