@diagrammo/dgmo 0.15.1 → 0.16.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 +9 -9
- package/dist/advanced.cjs +479 -454
- package/dist/advanced.d.cts +34 -35
- package/dist/advanced.d.ts +34 -35
- package/dist/advanced.js +479 -453
- package/dist/auto.cjs +374 -352
- package/dist/auto.js +103 -103
- package/dist/auto.mjs +374 -352
- package/dist/cli.cjs +140 -140
- package/dist/editor.cjs +8 -9
- package/dist/editor.js +8 -9
- package/dist/highlight.cjs +8 -9
- package/dist/highlight.js +8 -9
- package/dist/index.cjs +365 -342
- package/dist/index.js +365 -342
- package/dist/internal.cjs +479 -454
- package/dist/internal.d.cts +34 -35
- package/dist/internal.d.ts +34 -35
- package/dist/internal.js +479 -453
- package/dist/pert.d.cts +2 -2
- package/dist/pert.d.ts +2 -2
- package/docs/language-reference.md +83 -66
- package/gallery/fixtures/area.dgmo +3 -3
- package/gallery/fixtures/bar-stacked.dgmo +5 -5
- package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
- package/gallery/fixtures/c4-full.dgmo +8 -8
- package/gallery/fixtures/class-full.dgmo +2 -2
- package/gallery/fixtures/doughnut.dgmo +6 -6
- package/gallery/fixtures/flowchart-colors.dgmo +3 -3
- package/gallery/fixtures/function.dgmo +3 -3
- package/gallery/fixtures/gantt-full.dgmo +9 -9
- package/gallery/fixtures/gantt.dgmo +7 -7
- package/gallery/fixtures/infra-full.dgmo +6 -6
- package/gallery/fixtures/infra.dgmo +2 -2
- package/gallery/fixtures/kanban.dgmo +9 -9
- package/gallery/fixtures/line.dgmo +2 -2
- package/gallery/fixtures/multi-line.dgmo +3 -3
- package/gallery/fixtures/org-full.dgmo +6 -6
- package/gallery/fixtures/quadrant.dgmo +2 -2
- package/gallery/fixtures/sankey.dgmo +9 -9
- package/gallery/fixtures/scatter.dgmo +3 -3
- package/gallery/fixtures/sequence-tags-protocols.dgmo +8 -8
- package/gallery/fixtures/sequence-tags.dgmo +7 -7
- package/gallery/fixtures/sitemap-full.dgmo +7 -7
- package/gallery/fixtures/slope.dgmo +5 -5
- package/gallery/fixtures/spr-eras.dgmo +9 -9
- package/gallery/fixtures/timeline.dgmo +3 -3
- package/gallery/fixtures/venn.dgmo +3 -3
- package/package.json +1 -1
- package/src/advanced.ts +0 -1
- package/src/boxes-and-lines/renderer.ts +5 -1
- package/src/c4/parser.ts +1 -1
- package/src/c4/renderer.ts +15 -8
- package/src/chart.ts +18 -9
- package/src/class/parser.ts +7 -6
- package/src/class/renderer.ts +17 -6
- package/src/cli.ts +6 -6
- package/src/completion.ts +13 -3
- package/src/cycle/parser.ts +14 -0
- package/src/cycle/renderer.ts +6 -3
- package/src/d3.ts +86 -46
- package/src/echarts.ts +26 -9
- package/src/editor/dgmo.grammar +1 -3
- package/src/editor/dgmo.grammar.js +8 -8
- package/src/editor/dgmo.grammar.terms.js +11 -12
- package/src/editor/highlight-api.ts +0 -1
- package/src/editor/highlight.ts +0 -1
- package/src/er/parser.ts +18 -11
- package/src/er/renderer.ts +19 -7
- package/src/gantt/parser.ts +1 -1
- package/src/gantt/renderer.ts +7 -4
- package/src/graph/flowchart-parser.ts +18 -84
- package/src/graph/flowchart-renderer.ts +3 -8
- package/src/graph/layout.ts +0 -2
- package/src/graph/state-parser.ts +17 -62
- package/src/graph/state-renderer.ts +3 -8
- package/src/infra/parser.ts +21 -11
- package/src/infra/renderer.ts +7 -4
- package/src/journey-map/parser.ts +10 -3
- package/src/journey-map/renderer.ts +3 -1
- package/src/kanban/parser.ts +10 -6
- package/src/kanban/renderer.ts +3 -1
- package/src/mindmap/parser.ts +2 -2
- package/src/mindmap/renderer.ts +2 -1
- package/src/org/parser.ts +2 -2
- package/src/org/renderer.ts +4 -3
- package/src/pert/parser.ts +7 -7
- package/src/pert/renderer.ts +7 -2
- package/src/pert/types.ts +1 -1
- package/src/pyramid/parser.ts +12 -0
- package/src/raci/parser.ts +40 -10
- package/src/raci/renderer.ts +2 -1
- package/src/raci/types.ts +4 -3
- package/src/ring/parser.ts +12 -0
- package/src/sequence/parser.ts +15 -9
- package/src/sequence/renderer.ts +1 -1
- package/src/sitemap/layout.ts +0 -2
- package/src/sitemap/parser.ts +11 -37
- package/src/sitemap/renderer.ts +13 -13
- package/src/sitemap/types.ts +0 -1
- package/src/tech-radar/renderer.ts +5 -3
- package/src/tech-radar/types.ts +2 -0
- package/src/utils/arrows.ts +3 -28
- package/src/utils/legend-d3.ts +12 -6
- package/src/utils/legend-layout.ts +1 -1
- package/src/utils/legend-types.ts +1 -1
- package/src/utils/parsing.ts +64 -35
- package/src/utils/tag-groups.ts +98 -18
- package/src/wireframe/parser.ts +2 -2
package/src/sitemap/parser.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
5
|
import type { PaletteColors } from '../palettes';
|
|
6
|
-
import { resolveColorWithDiagnostic } from '../colors';
|
|
7
6
|
import type { DgmoError } from '../diagnostics';
|
|
8
7
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
9
8
|
import { normalizeName } from '../utils/name-normalize';
|
|
@@ -20,7 +19,6 @@ import {
|
|
|
20
19
|
measureIndent,
|
|
21
20
|
extractColor,
|
|
22
21
|
parsePipeMetadata,
|
|
23
|
-
inferArrowColor,
|
|
24
22
|
MULTIPLE_PIPE_ERROR,
|
|
25
23
|
parseFirstLine,
|
|
26
24
|
OPTION_NOCOLON_RE,
|
|
@@ -39,10 +37,11 @@ const CONTAINER_RE = /^\[([^\]]+)\]\s*(?:\|\s*(.+))?$/;
|
|
|
39
37
|
const METADATA_RE = /^([^:]+):\s*(.+)$/;
|
|
40
38
|
|
|
41
39
|
/**
|
|
42
|
-
* Arrow line: `-label
|
|
43
|
-
*
|
|
40
|
+
* Arrow line: `-label->` or `->` followed by target label.
|
|
41
|
+
* Edges have no color slot (spec §1.7).
|
|
42
|
+
* Captures: [1] label, [2] target
|
|
44
43
|
*/
|
|
45
|
-
const ARROW_RE = /^-([
|
|
44
|
+
const ARROW_RE = /^-([^>][^>]*?)?\s*->\s*(.+)$/;
|
|
46
45
|
const BARE_ARROW_RE = /^->\s*(.+)$/;
|
|
47
46
|
|
|
48
47
|
// ============================================================
|
|
@@ -51,12 +50,11 @@ const BARE_ARROW_RE = /^->\s*(.+)$/;
|
|
|
51
50
|
|
|
52
51
|
function parseArrowLine(
|
|
53
52
|
trimmed: string,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
_palette: PaletteColors | undefined,
|
|
54
|
+
_lineNumber: number,
|
|
55
|
+
_diagnostics: DgmoError[]
|
|
57
56
|
): {
|
|
58
57
|
label?: string;
|
|
59
|
-
color?: string;
|
|
60
58
|
target: string;
|
|
61
59
|
targetIsGroup: boolean;
|
|
62
60
|
} | null {
|
|
@@ -71,33 +69,14 @@ function parseArrowLine(
|
|
|
71
69
|
};
|
|
72
70
|
}
|
|
73
71
|
|
|
74
|
-
// Labeled
|
|
72
|
+
// Labeled arrow: -label-> Target
|
|
75
73
|
const arrowMatch = trimmed.match(ARROW_RE);
|
|
76
74
|
if (arrowMatch) {
|
|
77
75
|
const label = arrowMatch[1]?.trim() || undefined;
|
|
78
|
-
|
|
79
|
-
? resolveColorWithDiagnostic(
|
|
80
|
-
arrowMatch[2].trim(),
|
|
81
|
-
lineNumber,
|
|
82
|
-
diagnostics,
|
|
83
|
-
palette
|
|
84
|
-
)
|
|
85
|
-
: undefined;
|
|
86
|
-
if (label && !color) {
|
|
87
|
-
const inferred = inferArrowColor(label);
|
|
88
|
-
if (inferred)
|
|
89
|
-
color = resolveColorWithDiagnostic(
|
|
90
|
-
inferred,
|
|
91
|
-
lineNumber,
|
|
92
|
-
diagnostics,
|
|
93
|
-
palette
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
const rawTarget = arrowMatch[3].trim();
|
|
76
|
+
const rawTarget = arrowMatch[2].trim();
|
|
97
77
|
const groupMatch = rawTarget.match(/^\[(.+)\]$/);
|
|
98
78
|
return {
|
|
99
79
|
label,
|
|
100
|
-
color,
|
|
101
80
|
target: groupMatch ? groupMatch[1].trim() : rawTarget,
|
|
102
81
|
targetIsGroup: !!groupMatch,
|
|
103
82
|
};
|
|
@@ -216,7 +195,6 @@ export function parseSitemap(
|
|
|
216
195
|
targetLabel: string;
|
|
217
196
|
targetIsGroup: boolean;
|
|
218
197
|
label?: string;
|
|
219
|
-
color?: string;
|
|
220
198
|
lineNumber: number;
|
|
221
199
|
}[] = [];
|
|
222
200
|
|
|
@@ -309,7 +287,7 @@ export function parseSitemap(
|
|
|
309
287
|
}
|
|
310
288
|
}
|
|
311
289
|
|
|
312
|
-
// Tag group entries (indented Value
|
|
290
|
+
// Tag group entries (indented `Value color` under tag heading; §1.5)
|
|
313
291
|
// First entry is the default unless another is marked `default`
|
|
314
292
|
if (currentTagGroup && !contentStarted) {
|
|
315
293
|
const indent = measureIndent(line);
|
|
@@ -319,7 +297,7 @@ export function parseSitemap(
|
|
|
319
297
|
if (!color) {
|
|
320
298
|
pushError(
|
|
321
299
|
lineNumber,
|
|
322
|
-
`Expected 'Value
|
|
300
|
+
`Expected 'Value color' in tag group '${currentTagGroup.name}'`
|
|
323
301
|
);
|
|
324
302
|
continue;
|
|
325
303
|
}
|
|
@@ -365,7 +343,6 @@ export function parseSitemap(
|
|
|
365
343
|
targetLabel: arrowInfo.target,
|
|
366
344
|
targetIsGroup: arrowInfo.targetIsGroup,
|
|
367
345
|
label: arrowInfo.label,
|
|
368
|
-
color: arrowInfo.color,
|
|
369
346
|
lineNumber,
|
|
370
347
|
});
|
|
371
348
|
}
|
|
@@ -486,7 +463,6 @@ export function parseSitemap(
|
|
|
486
463
|
sourceId: arrow.sourceNode.id,
|
|
487
464
|
targetId: aliasHit,
|
|
488
465
|
label: arrow.label,
|
|
489
|
-
color: arrow.color,
|
|
490
466
|
lineNumber: arrow.lineNumber,
|
|
491
467
|
});
|
|
492
468
|
continue;
|
|
@@ -508,7 +484,6 @@ export function parseSitemap(
|
|
|
508
484
|
sourceId: arrow.sourceNode.id,
|
|
509
485
|
targetId: targetContainer.id,
|
|
510
486
|
label: arrow.label,
|
|
511
|
-
color: arrow.color,
|
|
512
487
|
lineNumber: arrow.lineNumber,
|
|
513
488
|
});
|
|
514
489
|
} else {
|
|
@@ -526,7 +501,6 @@ export function parseSitemap(
|
|
|
526
501
|
sourceId: arrow.sourceNode.id,
|
|
527
502
|
targetId: targetNode.id,
|
|
528
503
|
label: arrow.label,
|
|
529
|
-
color: arrow.color,
|
|
530
504
|
lineNumber: arrow.lineNumber,
|
|
531
505
|
});
|
|
532
506
|
}
|
package/src/sitemap/renderer.ts
CHANGED
|
@@ -116,7 +116,8 @@ export function renderSitemap(
|
|
|
116
116
|
onClickItem?: (lineNumber: number) => void,
|
|
117
117
|
exportDims?: { width?: number; height?: number },
|
|
118
118
|
activeTagGroup?: string | null,
|
|
119
|
-
hiddenAttributes?: Set<string
|
|
119
|
+
hiddenAttributes?: Set<string>,
|
|
120
|
+
exportMode?: boolean
|
|
120
121
|
): void {
|
|
121
122
|
// Clear existing content
|
|
122
123
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
@@ -184,11 +185,9 @@ export function renderSitemap(
|
|
|
184
185
|
.attr('points', `0,0 ${ARROWHEAD_W},${ARROWHEAD_H / 2} 0,${ARROWHEAD_H}`)
|
|
185
186
|
.attr('fill', palette.textMuted);
|
|
186
187
|
|
|
187
|
-
//
|
|
188
|
+
// Edges have no color slot (spec §1.7); keep empty set so the marker-setup
|
|
189
|
+
// loop is a no-op but the symbol stays available for future color sources.
|
|
188
190
|
const edgeColors = new Set<string>();
|
|
189
|
-
for (const edge of layout.edges) {
|
|
190
|
-
if (edge.color) edgeColors.add(edge.color);
|
|
191
|
-
}
|
|
192
191
|
for (const color of edgeColors) {
|
|
193
192
|
const id = `sm-arrow-${color.replace('#', '')}`;
|
|
194
193
|
defs
|
|
@@ -379,10 +378,8 @@ export function renderSitemap(
|
|
|
379
378
|
.attr('class', 'sitemap-edge-group')
|
|
380
379
|
.attr('data-line-number', String(edge.lineNumber));
|
|
381
380
|
|
|
382
|
-
const edgeColor =
|
|
383
|
-
const markerId =
|
|
384
|
-
? `sm-arrow-${edge.color.replace('#', '')}`
|
|
385
|
-
: 'sm-arrow';
|
|
381
|
+
const edgeColor = palette.textMuted;
|
|
382
|
+
const markerId = 'sm-arrow';
|
|
386
383
|
|
|
387
384
|
const gen = edge.deferred ? lineGeneratorLinear : lineGenerator;
|
|
388
385
|
const pathD = gen(edge.points);
|
|
@@ -602,7 +599,8 @@ export function renderSitemap(
|
|
|
602
599
|
isDark,
|
|
603
600
|
activeTagGroup,
|
|
604
601
|
undefined,
|
|
605
|
-
hiddenAttributes
|
|
602
|
+
hiddenAttributes,
|
|
603
|
+
exportMode
|
|
606
604
|
);
|
|
607
605
|
}
|
|
608
606
|
|
|
@@ -647,7 +645,8 @@ export function renderSitemap(
|
|
|
647
645
|
isDark,
|
|
648
646
|
activeTagGroup,
|
|
649
647
|
width,
|
|
650
|
-
hiddenAttributes
|
|
648
|
+
hiddenAttributes,
|
|
649
|
+
exportMode
|
|
651
650
|
);
|
|
652
651
|
}
|
|
653
652
|
}
|
|
@@ -663,7 +662,8 @@ function renderLegend(
|
|
|
663
662
|
isDark: boolean,
|
|
664
663
|
activeTagGroup?: string | null,
|
|
665
664
|
fixedWidth?: number,
|
|
666
|
-
hiddenAttributes?: Set<string
|
|
665
|
+
hiddenAttributes?: Set<string>,
|
|
666
|
+
exportMode?: boolean
|
|
667
667
|
): void {
|
|
668
668
|
if (legendGroups.length === 0) return;
|
|
669
669
|
|
|
@@ -678,7 +678,7 @@ function renderLegend(
|
|
|
678
678
|
const legendConfig: LegendConfig = {
|
|
679
679
|
groups,
|
|
680
680
|
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
681
|
-
mode: '
|
|
681
|
+
mode: exportMode ? 'export' : 'preview',
|
|
682
682
|
capsulePillAddonWidth: eyeAddonWidth,
|
|
683
683
|
};
|
|
684
684
|
const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
|
package/src/sitemap/types.ts
CHANGED
|
@@ -163,7 +163,7 @@ export function renderTechRadar(
|
|
|
163
163
|
},
|
|
164
164
|
],
|
|
165
165
|
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
166
|
-
mode: '
|
|
166
|
+
mode: options?.exportMode ? 'export' : 'preview',
|
|
167
167
|
controlsGroup: {
|
|
168
168
|
toggles: [
|
|
169
169
|
{
|
|
@@ -1195,7 +1195,8 @@ export function renderTechRadarForExport(
|
|
|
1195
1195
|
palette: PaletteColors,
|
|
1196
1196
|
isDark: boolean,
|
|
1197
1197
|
exportDims?: D3ExportDimensions,
|
|
1198
|
-
viewState?: CompactViewState
|
|
1198
|
+
viewState?: CompactViewState,
|
|
1199
|
+
exportMode?: boolean
|
|
1199
1200
|
): void {
|
|
1200
1201
|
renderTechRadar(
|
|
1201
1202
|
container,
|
|
@@ -1204,6 +1205,7 @@ export function renderTechRadarForExport(
|
|
|
1204
1205
|
isDark,
|
|
1205
1206
|
undefined,
|
|
1206
1207
|
exportDims,
|
|
1207
|
-
viewState
|
|
1208
|
+
viewState,
|
|
1209
|
+
{ exportMode }
|
|
1208
1210
|
);
|
|
1209
1211
|
}
|
package/src/tech-radar/types.ts
CHANGED
|
@@ -78,4 +78,6 @@ export interface TechRadarRenderOptions {
|
|
|
78
78
|
onLegendGroupToggle?: (groupName: string) => void;
|
|
79
79
|
/** Active line from the editor cursor — triggers popover/expansion for that blip. */
|
|
80
80
|
activeLine?: number | null;
|
|
81
|
+
/** True when rendering for export (PNG/SVG/PDF) — controls whether collapsed legend pills and cog are stripped. */
|
|
82
|
+
exportMode?: boolean;
|
|
81
83
|
}
|
package/src/utils/arrows.ts
CHANGED
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
|
|
23
23
|
import type { DgmoError } from '../diagnostics';
|
|
24
24
|
import { makeDgmoError } from '../diagnostics';
|
|
25
|
-
import { RECOGNIZED_COLOR_NAMES } from '../colors';
|
|
26
25
|
|
|
27
26
|
interface ParsedArrow {
|
|
28
27
|
from: string;
|
|
@@ -140,10 +139,9 @@ export interface ParseInArrowLabelResult {
|
|
|
140
139
|
*
|
|
141
140
|
* This helper is intentionally chart-agnostic: it operates on an already
|
|
142
141
|
* extracted label string, leaving each chart's existing arrow-finding
|
|
143
|
-
* tokenization in place.
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* `matchColorParens()` from this module for the shared lookup.
|
|
142
|
+
* tokenization in place. Edges no longer have a color slot on any chart
|
|
143
|
+
* type (see spec §1.7 "Edge color is not a feature"); arrow content is
|
|
144
|
+
* pure label text.
|
|
147
145
|
*/
|
|
148
146
|
export function parseInArrowLabel(
|
|
149
147
|
rawLabel: string,
|
|
@@ -162,29 +160,6 @@ export function parseInArrowLabel(
|
|
|
162
160
|
return { label: trimmed, diagnostics };
|
|
163
161
|
}
|
|
164
162
|
|
|
165
|
-
// ============================================================
|
|
166
|
-
// matchColorParens — shared TD-11 helper for flowchart and state
|
|
167
|
-
// ============================================================
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Test whether a string matches the TD-11 color-parens form `(colorName)`
|
|
171
|
-
* where `colorName` is one of the 11 recognized palette color names from
|
|
172
|
-
* `src/colors.ts:RECOGNIZED_COLOR_NAMES`. Returns the lowercase color name
|
|
173
|
-
* on a match, or `null` on fall-through (whole string becomes a label).
|
|
174
|
-
*
|
|
175
|
-
* Used by flowchart and state parsers to keep the color-parens recognition
|
|
176
|
-
* rule in one place — do NOT re-implement the regex in chart parsers.
|
|
177
|
-
*/
|
|
178
|
-
export function matchColorParens(content: string): string | null {
|
|
179
|
-
const m = content.match(/^\(([A-Za-z]+)\)$/);
|
|
180
|
-
if (!m) return null;
|
|
181
|
-
const candidate = m[1].toLowerCase();
|
|
182
|
-
if ((RECOGNIZED_COLOR_NAMES as readonly string[]).includes(candidate)) {
|
|
183
|
-
return candidate;
|
|
184
|
-
}
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
163
|
// Forward (call) patterns — participant names may contain spaces, so use non-greedy (.+?)
|
|
189
164
|
const SYNC_LABELED_RE = /^(.+?)\s*-(.+)->\s*(.+)$/;
|
|
190
165
|
const ASYNC_LABELED_RE = /^(.+?)\s*~(.+)~>\s*(.+)$/;
|
package/src/utils/legend-d3.ts
CHANGED
|
@@ -46,7 +46,14 @@ export function renderLegendD3(
|
|
|
46
46
|
let currentState = { ...state };
|
|
47
47
|
let currentLayout: LegendLayout;
|
|
48
48
|
|
|
49
|
-
const legendG = container
|
|
49
|
+
const legendG = container
|
|
50
|
+
.append('g')
|
|
51
|
+
.attr('class', 'dgmo-legend')
|
|
52
|
+
.attr('data-legend-title-relation', config.position.titleRelation)
|
|
53
|
+
.attr(
|
|
54
|
+
'data-legend-capsule-addon-width',
|
|
55
|
+
String(config.capsulePillAddonWidth ?? 0)
|
|
56
|
+
);
|
|
50
57
|
|
|
51
58
|
function render() {
|
|
52
59
|
currentLayout = computeLegendLayout(config, currentState, width);
|
|
@@ -270,11 +277,10 @@ function renderPill(
|
|
|
270
277
|
groupBg: string,
|
|
271
278
|
callbacks?: LegendCallbacks
|
|
272
279
|
): void {
|
|
273
|
-
// Collapsed tag-group pills
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// pills
|
|
277
|
-
// `data-export-ignore` separately.
|
|
280
|
+
// Collapsed tag-group pills are hidden in export mode
|
|
281
|
+
// (`LegendConfig.mode === 'export'`) — the layout engine filters them
|
|
282
|
+
// in `computeLegendLayout`. See
|
|
283
|
+
// tech-spec-hide-inactive-tag-pills-in-exports.md.
|
|
278
284
|
const g = parent
|
|
279
285
|
.append('g')
|
|
280
286
|
.attr('transform', `translate(${pill.x},${pill.y})`)
|
|
@@ -235,7 +235,7 @@ export function computeLegendLayout(
|
|
|
235
235
|
containerWidth: number
|
|
236
236
|
): LegendLayout {
|
|
237
237
|
const { groups, controls: configControls, mode } = config;
|
|
238
|
-
const isExport = mode === '
|
|
238
|
+
const isExport = mode === 'export';
|
|
239
239
|
|
|
240
240
|
// Filter groups for export: only active group shown
|
|
241
241
|
const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
|
|
@@ -37,7 +37,7 @@ export interface LegendPosition {
|
|
|
37
37
|
titleRelation: 'below-title' | 'inline-with-title';
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export type LegendMode = '
|
|
40
|
+
export type LegendMode = 'preview' | 'export';
|
|
41
41
|
|
|
42
42
|
export type LegendControlExportBehavior = 'include' | 'strip' | 'static';
|
|
43
43
|
|
package/src/utils/parsing.ts
CHANGED
|
@@ -4,10 +4,18 @@
|
|
|
4
4
|
* pipe-metadata parsing.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
RECOGNIZED_COLOR_NAMES,
|
|
9
|
+
resolveColor,
|
|
10
|
+
resolveColorWithDiagnostic,
|
|
11
|
+
} from '../colors';
|
|
8
12
|
import type { DgmoError } from '../diagnostics';
|
|
9
13
|
import type { PaletteColors } from '../palettes';
|
|
10
14
|
|
|
15
|
+
const RECOGNIZED_COLOR_SET: ReadonlySet<string> = new Set(
|
|
16
|
+
RECOGNIZED_COLOR_NAMES
|
|
17
|
+
);
|
|
18
|
+
|
|
11
19
|
// ── All known chart types ────────────────────────────────────
|
|
12
20
|
/** Complete set of recognized chart type identifiers. */
|
|
13
21
|
export const ALL_CHART_TYPES = new Set([
|
|
@@ -87,27 +95,47 @@ export function measureIndent(line: string): number {
|
|
|
87
95
|
return indent;
|
|
88
96
|
}
|
|
89
97
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Trailing-token color rule (see docs/dgmo-language-spec.md §1.5).
|
|
100
|
+
*
|
|
101
|
+
* Caller contract: `label` must be a pre-split LABEL REGION — the parser is
|
|
102
|
+
* responsible for stripping structural terminators (`as <alias>`, `| pipe
|
|
103
|
+
* metadata`, numeric values, date ranges, brackets, arrow constructs) BEFORE
|
|
104
|
+
* invoking this function. The color rule operates only on what remains.
|
|
105
|
+
*
|
|
106
|
+
* Algorithm: split the label on whitespace; if the final token is exactly one
|
|
107
|
+
* of `RECOGNIZED_COLOR_NAMES` (case-sensitive, lowercase only), peel it off
|
|
108
|
+
* as color and return the rest as the label. Otherwise the entire input
|
|
109
|
+
* stays as the label, no color.
|
|
110
|
+
*
|
|
111
|
+
* Case-sensitivity is deliberate: it provides the escape hatch (`Red`,
|
|
112
|
+
* `Yellow`, `Green` stay as labels — useful for traffic-light tag groups).
|
|
113
|
+
*
|
|
114
|
+
* Returns `{ label, color? }` where `color` is the palette-resolved hex string
|
|
115
|
+
* (or undefined if no color word matched).
|
|
116
|
+
*/
|
|
94
117
|
export function extractColor(
|
|
95
118
|
label: string,
|
|
96
119
|
palette?: PaletteColors,
|
|
97
120
|
diagnostics?: DgmoError[],
|
|
98
121
|
line?: number
|
|
99
122
|
): { label: string; color?: string } {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
const lastSpaceIdx = Math.max(
|
|
124
|
+
label.lastIndexOf(' '),
|
|
125
|
+
label.lastIndexOf('\t')
|
|
126
|
+
);
|
|
127
|
+
if (lastSpaceIdx < 0) return { label };
|
|
128
|
+
const trailing = label.substring(lastSpaceIdx + 1);
|
|
129
|
+
// Case-sensitive lowercase match against the closed 11-name palette.
|
|
130
|
+
if (!RECOGNIZED_COLOR_SET.has(trailing)) return { label };
|
|
103
131
|
let color: string | undefined;
|
|
104
132
|
if (diagnostics && line !== undefined) {
|
|
105
|
-
color = resolveColorWithDiagnostic(
|
|
133
|
+
color = resolveColorWithDiagnostic(trailing, line, diagnostics, palette);
|
|
106
134
|
} else {
|
|
107
|
-
color = resolveColor(
|
|
135
|
+
color = resolveColor(trailing, palette) ?? undefined;
|
|
108
136
|
}
|
|
109
137
|
return {
|
|
110
|
-
label: label.substring(0,
|
|
138
|
+
label: label.substring(0, lastSpaceIdx).trimEnd(),
|
|
111
139
|
color,
|
|
112
140
|
};
|
|
113
141
|
}
|
|
@@ -457,31 +485,32 @@ export function parseSeriesNames(
|
|
|
457
485
|
}
|
|
458
486
|
|
|
459
487
|
/**
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
488
|
+
* Peel a trailing recognized color name from a label region, returning the
|
|
489
|
+
* raw color name (not a resolved hex). Used by chart types that pair the
|
|
490
|
+
* universal trailing-token shortcut with their own pipe-metadata `color: …`
|
|
491
|
+
* long form (cycle, pyramid, ring, raci, boxes-and-lines).
|
|
492
|
+
*
|
|
493
|
+
* Caller contract: `label` must already have pipe metadata stripped — this
|
|
494
|
+
* function operates only on the label region.
|
|
495
|
+
*
|
|
496
|
+
* Returns `{ label, colorName? }`. If the trailing token is not a recognized
|
|
497
|
+
* lowercase color, returns the original label and `colorName: undefined`.
|
|
463
498
|
*/
|
|
464
|
-
export function
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
lower === 'false'
|
|
480
|
-
)
|
|
481
|
-
return 'red';
|
|
482
|
-
// Orange: uncertain/warning
|
|
483
|
-
if (lower === 'maybe' || lower === 'warning') return 'orange';
|
|
484
|
-
return undefined;
|
|
499
|
+
export function peelTrailingColorName(label: string): {
|
|
500
|
+
label: string;
|
|
501
|
+
colorName?: string;
|
|
502
|
+
} {
|
|
503
|
+
const lastSpaceIdx = Math.max(
|
|
504
|
+
label.lastIndexOf(' '),
|
|
505
|
+
label.lastIndexOf('\t')
|
|
506
|
+
);
|
|
507
|
+
if (lastSpaceIdx < 0) return { label };
|
|
508
|
+
const trailing = label.substring(lastSpaceIdx + 1);
|
|
509
|
+
if (!RECOGNIZED_COLOR_SET.has(trailing)) return { label };
|
|
510
|
+
return {
|
|
511
|
+
label: label.substring(0, lastSpaceIdx).trimEnd(),
|
|
512
|
+
colorName: trailing,
|
|
513
|
+
};
|
|
485
514
|
}
|
|
486
515
|
|
|
487
516
|
/** Error message for multiple pipes on a single line. */
|
package/src/utils/tag-groups.ts
CHANGED
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
tagShorthandRemovedMessage,
|
|
10
10
|
type DgmoError,
|
|
11
11
|
} from '../diagnostics';
|
|
12
|
+
import { RECOGNIZED_COLOR_NAMES } from '../colors';
|
|
12
13
|
|
|
13
|
-
/** A single entry inside a tag group: `Value
|
|
14
|
+
/** A single entry inside a tag group: `Value color` */
|
|
14
15
|
export interface TagEntry {
|
|
15
16
|
value: string;
|
|
16
17
|
color: string;
|
|
@@ -32,7 +33,7 @@ interface TagBlockMatch {
|
|
|
32
33
|
name: string;
|
|
33
34
|
alias: string | undefined;
|
|
34
35
|
colorHint: string | undefined;
|
|
35
|
-
/** Inline tag values parsed from single-line form (e.g., `tag Priority as p High
|
|
36
|
+
/** Inline tag values parsed from single-line form (e.g., `tag Priority as p High red, Low blue`) */
|
|
36
37
|
inlineValues?: string[];
|
|
37
38
|
/**
|
|
38
39
|
* If the heading used the legacy `tag Name <alias>` (bare shorthand)
|
|
@@ -51,8 +52,8 @@ interface TagBlockMatch {
|
|
|
51
52
|
* Returns the cleaned text and whether the keyword was present.
|
|
52
53
|
*
|
|
53
54
|
* Examples:
|
|
54
|
-
* "NA
|
|
55
|
-
* "Done
|
|
55
|
+
* "NA gray default" → { text: "NA gray", isDefault: true }
|
|
56
|
+
* "Done green" → { text: "Done green", isDefault: false }
|
|
56
57
|
*/
|
|
57
58
|
export function stripDefaultModifier(text: string): {
|
|
58
59
|
text: string;
|
|
@@ -90,7 +91,7 @@ export function isTagBlockHeading(trimmed: string): boolean {
|
|
|
90
91
|
/**
|
|
91
92
|
* Parse a tag declaration line: `tag Name [as <alias>] [Values...]`
|
|
92
93
|
*
|
|
93
|
-
* Canonical form
|
|
94
|
+
* Canonical form: `tag Priority as p High red, Low blue` (universal §1.5).
|
|
94
95
|
*
|
|
95
96
|
* Legacy forms still parse for graceful degradation but set
|
|
96
97
|
* `legacyForm` on the result so the caller can emit
|
|
@@ -117,12 +118,26 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
117
118
|
let restStartIdx = 1;
|
|
118
119
|
|
|
119
120
|
// Locate any keyword separator (`as` or legacy `alias`) that appears
|
|
120
|
-
// BEFORE the first inline-value token. Inline values are
|
|
121
|
-
//
|
|
121
|
+
// BEFORE the first inline-value token. Inline values are recognized by
|
|
122
|
+
// a comma in the line: scan tokens for one. Under §1.5 trailing-token
|
|
123
|
+
// syntax there's no `(color)` marker anymore — a comma anywhere after
|
|
124
|
+
// the name span signals that inline values follow.
|
|
122
125
|
let valueStart = tokens.length;
|
|
123
126
|
for (let i = 1; i < tokens.length; i++) {
|
|
124
|
-
if (tokens[i].includes('
|
|
127
|
+
if (tokens[i].includes(',')) {
|
|
128
|
+
// valueStart is the FIRST token of the first inline value, which is
|
|
129
|
+
// the token immediately following the alias / keyword span. Walk
|
|
130
|
+
// back to the start of the value span by finding the most recent
|
|
131
|
+
// word boundary — but for the simple heuristic here, the inline
|
|
132
|
+
// value list starts at the previous non-keyword token.
|
|
125
133
|
valueStart = i;
|
|
134
|
+
// The token containing the comma might be `High` (in `High red,`)
|
|
135
|
+
// or `red,` (in `High red,` if tokenized differently). Treat the
|
|
136
|
+
// value span as starting at the token BEFORE the first comma
|
|
137
|
+
// unless that token is the alias / keyword.
|
|
138
|
+
// Simpler: use this index as a coarse upper bound. The keyword
|
|
139
|
+
// search below uses [1, valueStart) — anything past `as`/`alias`
|
|
140
|
+
// belongs to the value span.
|
|
126
141
|
break;
|
|
127
142
|
}
|
|
128
143
|
}
|
|
@@ -170,16 +185,77 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
170
185
|
} else {
|
|
171
186
|
// No `as`/`alias` keyword — try legacy bare-shorthand. The trailing
|
|
172
187
|
// token of the name span (just before inline values) is the alias
|
|
173
|
-
// candidate; if it passes the universal alias regex
|
|
174
|
-
//
|
|
175
|
-
|
|
188
|
+
// candidate; if it passes the universal alias regex AND is NOT a
|
|
189
|
+
// recognized palette color (§1.5 escape hatch), accept it.
|
|
190
|
+
//
|
|
191
|
+
// When inline values are present (valueStart < tokens.length), the
|
|
192
|
+
// tokens immediately before the first value-segment-with-color form
|
|
193
|
+
// the (name + alias) prefix. The first value contains at least the
|
|
194
|
+
// value name + trailing color, so we walk back to find where it
|
|
195
|
+
// starts: skip the trailing color token, then 1+ name tokens.
|
|
196
|
+
const isColorWord = (s: string): boolean =>
|
|
197
|
+
(RECOGNIZED_COLOR_NAMES as readonly string[]).includes(s);
|
|
198
|
+
|
|
199
|
+
if (valueStart < tokens.length) {
|
|
200
|
+
// Inline values are present (we found a comma at valueStart).
|
|
201
|
+
// The first value's last token is at index commaIdx; strip the
|
|
202
|
+
// comma to inspect. Walk back to determine the value name length.
|
|
203
|
+
const commaTokenIdx = valueStart;
|
|
204
|
+
// Find where the first value starts: the value contains at least
|
|
205
|
+
// 1 word + optional trailing color. Walk back from commaTokenIdx
|
|
206
|
+
// while the previous tokens look like value-name words (i.e. not
|
|
207
|
+
// a recognized alias-shaped lowercase short token that is followed
|
|
208
|
+
// by a value start).
|
|
209
|
+
// Simpler heuristic: pre-comma trailing color is the last token if
|
|
210
|
+
// it's a recognized color (after stripping comma). The value's
|
|
211
|
+
// name is the token immediately before that. So the value spans
|
|
212
|
+
// (firstValueStart..=commaTokenIdx). The "name + alias" prefix
|
|
213
|
+
// is [0, firstValueStart).
|
|
214
|
+
const lastBeforeComma = tokens[commaTokenIdx].replace(/,$/, '');
|
|
215
|
+
// value = `<name word(s)> <color>` if trailing token is a recognized
|
|
216
|
+
// palette word; otherwise value = `<name word(s)>` (no color).
|
|
217
|
+
const firstValueStart = isColorWord(lastBeforeComma)
|
|
218
|
+
? commaTokenIdx - 1
|
|
219
|
+
: commaTokenIdx;
|
|
220
|
+
// Now firstValueStart points at the first token of value #1.
|
|
221
|
+
// [0, firstValueStart) is the `<name + optional alias>` prefix.
|
|
222
|
+
const prefixEnd = firstValueStart;
|
|
223
|
+
const aliasCandidate = prefixEnd > 1 ? tokens[prefixEnd - 1] : undefined;
|
|
224
|
+
if (
|
|
225
|
+
aliasCandidate &&
|
|
226
|
+
isAliasToken(aliasCandidate) &&
|
|
227
|
+
!isColorWord(aliasCandidate)
|
|
228
|
+
) {
|
|
229
|
+
alias = aliasCandidate;
|
|
230
|
+
legacyForm = 'bare-shorthand';
|
|
231
|
+
name = tokens
|
|
232
|
+
.slice(0, prefixEnd - 1)
|
|
233
|
+
.map((t) => stripQuotes(t))
|
|
234
|
+
.join(' ');
|
|
235
|
+
restStartIdx = prefixEnd;
|
|
236
|
+
} else {
|
|
237
|
+
name = tokens
|
|
238
|
+
.slice(0, prefixEnd)
|
|
239
|
+
.map((t) => stripQuotes(t))
|
|
240
|
+
.join(' ');
|
|
241
|
+
restStartIdx = prefixEnd;
|
|
242
|
+
}
|
|
243
|
+
} else if (tokens[0][0] === '"' || tokens[0][0] === "'") {
|
|
176
244
|
// Quoted name. Check the next token for legacy bare alias.
|
|
177
|
-
if (
|
|
245
|
+
if (
|
|
246
|
+
tokens.length > 1 &&
|
|
247
|
+
isAliasToken(tokens[1]) &&
|
|
248
|
+
!isColorWord(tokens[1])
|
|
249
|
+
) {
|
|
178
250
|
alias = tokens[1];
|
|
179
251
|
legacyForm = 'bare-shorthand';
|
|
180
252
|
restStartIdx = 2;
|
|
181
253
|
}
|
|
182
|
-
} else if (
|
|
254
|
+
} else if (
|
|
255
|
+
valueStart > 1 &&
|
|
256
|
+
isAliasToken(tokens[valueStart - 1]) &&
|
|
257
|
+
!isColorWord(tokens[valueStart - 1])
|
|
258
|
+
) {
|
|
183
259
|
// Bare shorthand at the end of the name span.
|
|
184
260
|
alias = tokens[valueStart - 1];
|
|
185
261
|
legacyForm = 'bare-shorthand';
|
|
@@ -207,12 +283,16 @@ export function parseTagDeclaration(line: string): TagBlockMatch | null {
|
|
|
207
283
|
.filter(Boolean);
|
|
208
284
|
}
|
|
209
285
|
|
|
210
|
-
// Trailing
|
|
286
|
+
// Trailing recognized-color token on the name itself (no inline values).
|
|
287
|
+
// Per §1.5 universal trailing-token: case-sensitive lowercase match.
|
|
211
288
|
if (!inlineValues || inlineValues.length === 0) {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
289
|
+
const lastSpaceIdx = name.lastIndexOf(' ');
|
|
290
|
+
if (lastSpaceIdx > 0) {
|
|
291
|
+
const trailing = name.substring(lastSpaceIdx + 1);
|
|
292
|
+
if ((RECOGNIZED_COLOR_NAMES as readonly string[]).includes(trailing)) {
|
|
293
|
+
colorHint = trailing;
|
|
294
|
+
name = name.substring(0, lastSpaceIdx).trimEnd();
|
|
295
|
+
}
|
|
216
296
|
}
|
|
217
297
|
}
|
|
218
298
|
|