@diagrammo/dgmo 0.34.0 → 0.36.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/.claude/commands/dgmo.md +41 -43
- package/.github/copilot-instructions.md +1 -1
- package/README.md +7 -1
- package/SKILL.md +12 -7
- package/dist/advanced.cjs +460 -463
- package/dist/advanced.d.cts +2 -51
- package/dist/advanced.d.ts +2 -51
- package/dist/advanced.js +460 -464
- package/dist/auto.cjs +461 -140
- package/dist/auto.js +91 -91
- package/dist/auto.mjs +461 -140
- package/dist/cli.cjs +143 -326
- package/dist/index.cjs +460 -139
- package/dist/index.js +460 -139
- package/docs/ai-integration.md +47 -25
- package/docs/language-reference.md +2 -2
- package/package.json +2 -2
- package/src/advanced.ts +0 -12
- package/src/c4/parser.ts +15 -6
- package/src/chart.ts +1 -1
- package/src/class/parser.ts +6 -2
- package/src/cli-banner.ts +2 -3
- package/src/cli.ts +539 -1223
- package/src/cycle/parser.ts +4 -1
- package/src/dgmo-router.ts +16 -2
- package/src/diagnostics.ts +0 -0
- package/src/echarts.ts +5 -1
- package/src/er/parser.ts +5 -1
- package/src/gantt/parser.ts +13 -4
- package/src/graph/flowchart-parser.ts +5 -1
- package/src/graph/state-parser.ts +5 -1
- package/src/infra/parser.ts +10 -6
- package/src/journey-map/parser.ts +5 -2
- package/src/kanban/parser.ts +6 -2
- package/src/map/context-labels.ts +29 -19
- package/src/map/layout.ts +403 -108
- package/src/map/legend-band.ts +28 -0
- package/src/map/parser.ts +13 -4
- package/src/map/renderer.ts +74 -23
- package/src/map/resolver.ts +2 -2
- package/src/org/parser.ts +8 -2
- package/src/pyramid/parser.ts +4 -1
- package/src/raci/parser.ts +4 -1
- package/src/ring/parser.ts +4 -1
- package/src/sequence/parser.ts +6 -3
- package/src/sitemap/parser.ts +12 -3
- package/src/tech-radar/parser.ts +6 -3
- package/src/utils/parsing.ts +2 -2
- package/src/utils/tag-groups.ts +3 -3
- package/src/visualizations/parse.ts +9 -3
- package/src/migrate/data-comma.ts +0 -117
- package/src/migrate/embedded.ts +0 -238
- package/src/migrate/index.ts +0 -295
- package/src/migrate/line-classifier.ts +0 -128
- package/src/migrate/line-transformer.ts +0 -218
package/src/map/legend-band.ts
CHANGED
|
@@ -97,3 +97,31 @@ export function mapLegendBand(
|
|
|
97
97
|
mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/** The legend's actual centred bounding box (top-centre overlay), or null when no
|
|
102
|
+
* legend is drawn. Used to feed the legend into the on-map label collision set so
|
|
103
|
+
* a region / context name never lands under it. Mirrors `mapLegendBand` but keeps
|
|
104
|
+
* the width + x rather than collapsing to a band height. */
|
|
105
|
+
export function mapLegendBox(
|
|
106
|
+
legend: MapLayoutLegend | null,
|
|
107
|
+
opts: {
|
|
108
|
+
width: number;
|
|
109
|
+
mode: LegendMode;
|
|
110
|
+
hasTitle: boolean;
|
|
111
|
+
hasSubtitle: boolean;
|
|
112
|
+
}
|
|
113
|
+
): { x: number; y: number; width: number; height: number } | null {
|
|
114
|
+
if (!legend) return null;
|
|
115
|
+
const groups = mapLegendGroups(legend);
|
|
116
|
+
if (groups.length === 0) return null;
|
|
117
|
+
const config = mapLegendConfig(groups, opts.mode);
|
|
118
|
+
const state: LegendState = { activeGroup: legend.activeGroup };
|
|
119
|
+
const { width, height } = computeLegendLayout(config, state, opts.width);
|
|
120
|
+
if (height <= 0 || width <= 0) return null;
|
|
121
|
+
return {
|
|
122
|
+
x: (opts.width - width) / 2,
|
|
123
|
+
y: mapLegendTop(opts.hasTitle, opts.hasSubtitle),
|
|
124
|
+
width,
|
|
125
|
+
height,
|
|
126
|
+
};
|
|
127
|
+
}
|
package/src/map/parser.ts
CHANGED
|
@@ -369,7 +369,10 @@ export function parseMap(content: string, palette?: PaletteColors): ParsedMap {
|
|
|
369
369
|
function handleTag(trimmed: string, line: number): void {
|
|
370
370
|
const m = matchTagBlockHeading(trimmed);
|
|
371
371
|
if (!m) {
|
|
372
|
-
pushError(
|
|
372
|
+
pushError(
|
|
373
|
+
line,
|
|
374
|
+
`Malformed tag declaration: "${trimmed}" — write 'tag <Group>' (optionally 'tag <Group> as <alias>'), e.g. 'tag Market as m', with indented 'Value color' entries below.`
|
|
375
|
+
);
|
|
373
376
|
return;
|
|
374
377
|
}
|
|
375
378
|
emitTagLegacyDiagnostic(m, line, diagnostics);
|
|
@@ -500,7 +503,7 @@ export function parseMap(content: string, palette?: PaletteColors): ParsedMap {
|
|
|
500
503
|
if (!pos || (pos.kind === 'name' && !pos.name)) {
|
|
501
504
|
pushError(
|
|
502
505
|
line,
|
|
503
|
-
'route
|
|
506
|
+
'route needs an origin place — `route <origin>` then indented legs (`-> dest`); origin may be a city name, IATA code, or `<lat> <lon>`. (§24B)'
|
|
504
507
|
);
|
|
505
508
|
return;
|
|
506
509
|
}
|
|
@@ -579,7 +582,10 @@ export function parseMap(content: string, palette?: PaletteColors): ParsedMap {
|
|
|
579
582
|
const parts = trimmed.split(ARROW_SPLIT);
|
|
580
583
|
// parts = [ep0, tok0, ep1, tok1, ep2, ...]
|
|
581
584
|
if (parts.length < 3) {
|
|
582
|
-
pushError(
|
|
585
|
+
pushError(
|
|
586
|
+
line,
|
|
587
|
+
`Malformed edge: "${trimmed}" — an edge is 'A -> B' (or 'A -label-> B', '~>' for arc, '--' undirected), both endpoints non-empty. (§24B)`
|
|
588
|
+
);
|
|
583
589
|
return;
|
|
584
590
|
}
|
|
585
591
|
const endpoints: string[] = [];
|
|
@@ -609,7 +615,10 @@ export function parseMap(content: string, palette?: PaletteColors): ParsedMap {
|
|
|
609
615
|
const from = endpoints[k]!;
|
|
610
616
|
const to = endpoints[k + 1]!;
|
|
611
617
|
if (!from || !to) {
|
|
612
|
-
pushError(
|
|
618
|
+
pushError(
|
|
619
|
+
line,
|
|
620
|
+
`Edge has an empty endpoint: "${trimmed}" — both sides of the arrow need a place, e.g. 'Paris -> Berlin'. (§24B)`
|
|
621
|
+
);
|
|
613
622
|
continue;
|
|
614
623
|
}
|
|
615
624
|
const isLast = k === links.length - 1;
|
package/src/map/renderer.ts
CHANGED
|
@@ -136,7 +136,8 @@ function coastlineOuterRings(
|
|
|
136
136
|
const paths: string[] = [];
|
|
137
137
|
for (const r of regions) {
|
|
138
138
|
// Reuse the rings parsed once in layoutMap; fall back for older layouts.
|
|
139
|
-
const rings =
|
|
139
|
+
const rings =
|
|
140
|
+
(r.rings as Array<Array<[number, number]>>) ?? parsePathRings(r.d);
|
|
140
141
|
for (let i = 0; i < rings.length; i++) {
|
|
141
142
|
const ring = rings[i]!;
|
|
142
143
|
if (ring.length < 3) continue;
|
|
@@ -684,10 +685,7 @@ export function renderMap(
|
|
|
684
685
|
for (const r of layout.regions) {
|
|
685
686
|
// bbox is precomputed once in layoutMap (roadmap #2); fall back to
|
|
686
687
|
// parsing only for layouts predating that field.
|
|
687
|
-
let minX: number,
|
|
688
|
-
minY: number,
|
|
689
|
-
maxX: number,
|
|
690
|
-
maxY: number;
|
|
688
|
+
let minX: number, minY: number, maxX: number, maxY: number;
|
|
691
689
|
if (r.bbox) {
|
|
692
690
|
[minX, minY, maxX, maxY] = r.bbox;
|
|
693
691
|
} else {
|
|
@@ -898,10 +896,67 @@ export function renderMap(
|
|
|
898
896
|
.append('g')
|
|
899
897
|
.attr('class', 'dgmo-map-legs')
|
|
900
898
|
.attr('fill', 'none');
|
|
901
|
-
|
|
899
|
+
// Parse a single-segment leg path (`M…L…` or `M…Q…`) into its end point and the
|
|
900
|
+
// unit tangent arriving there. Returns null for anything unexpected (then the
|
|
901
|
+
// leg renders without the trimmed-arrow treatment).
|
|
902
|
+
const legEnd = (
|
|
903
|
+
d: string
|
|
904
|
+
): { ex: number; ey: number; tx: number; ty: number } | null => {
|
|
905
|
+
const m =
|
|
906
|
+
/^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
|
|
907
|
+
d
|
|
908
|
+
);
|
|
909
|
+
if (!m) return null;
|
|
910
|
+
const x0 = +m[1]!;
|
|
911
|
+
const y0 = +m[2]!;
|
|
912
|
+
let ex: number;
|
|
913
|
+
let ey: number;
|
|
914
|
+
let fromX: number;
|
|
915
|
+
let fromY: number;
|
|
916
|
+
if (m[3] !== undefined) {
|
|
917
|
+
ex = +m[3]!;
|
|
918
|
+
ey = +m[4]!;
|
|
919
|
+
fromX = x0;
|
|
920
|
+
fromY = y0;
|
|
921
|
+
} else {
|
|
922
|
+
ex = +m[7]!;
|
|
923
|
+
ey = +m[8]!;
|
|
924
|
+
fromX = +m[5]!; // quadratic control → tangent at the end
|
|
925
|
+
fromY = +m[6]!;
|
|
926
|
+
}
|
|
927
|
+
const dx = ex - fromX;
|
|
928
|
+
const dy = ey - fromY;
|
|
929
|
+
const len = Math.hypot(dx, dy);
|
|
930
|
+
if (!(len > 0)) return null;
|
|
931
|
+
return { ex, ey, tx: dx / len, ty: dy / len };
|
|
932
|
+
};
|
|
933
|
+
layout.legs.forEach((leg) => {
|
|
934
|
+
// A directed leg's stroke is TRIMMED back to the arrowhead base so a heavy
|
|
935
|
+
// weighted line doesn't run through (and bulge past) the narrowing arrowhead.
|
|
936
|
+
// The arrowhead itself is drawn as a filled triangle at the true endpoint,
|
|
937
|
+
// sized to always be a touch wider than the stroke so the stroke end is fully
|
|
938
|
+
// covered. Undirected (or unparseable) legs keep the full path.
|
|
939
|
+
const end = leg.arrow ? legEnd(leg.d) : null;
|
|
940
|
+
let drawD = leg.d;
|
|
941
|
+
let arrowTri: string | null = null;
|
|
942
|
+
if (end) {
|
|
943
|
+
const aLen = arrowSize(leg.width); // arrow depth along the line
|
|
944
|
+
const aHalf = Math.max(aLen * 0.5, leg.width * 0.6 + 1); // half base ≥ stroke
|
|
945
|
+
const bx = end.ex - end.tx * aLen; // base centre
|
|
946
|
+
const by = end.ey - end.ty * aLen;
|
|
947
|
+
const px = -end.ty; // perpendicular
|
|
948
|
+
const py = end.tx;
|
|
949
|
+
arrowTri = `M${bx + px * aHalf},${by + py * aHalf}L${end.ex},${end.ey}L${bx - px * aHalf},${by - py * aHalf}z`;
|
|
950
|
+
// Re-point the stroke's end at the arrow base (small along-tangent shift; for
|
|
951
|
+
// an arc leg the negligible end-curvature change is hidden by the arrowhead).
|
|
952
|
+
drawD = leg.d.replace(
|
|
953
|
+
/(-?[\d.]+),(-?[\d.]+)$/,
|
|
954
|
+
`${bx.toFixed(2)},${by.toFixed(2)}`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
902
957
|
const p = gLegs
|
|
903
958
|
.append('path')
|
|
904
|
-
.attr('d',
|
|
959
|
+
.attr('d', drawD)
|
|
905
960
|
.attr('stroke', leg.color)
|
|
906
961
|
.attr('stroke-width', leg.width)
|
|
907
962
|
.attr('stroke-linecap', 'round')
|
|
@@ -918,23 +973,19 @@ export function renderMap(
|
|
|
918
973
|
// A 0-width invisible leg path is hard to hit; pointer-events on the visible
|
|
919
974
|
// stroke is enough for the line widths legs use.
|
|
920
975
|
wireSync(p, leg.lineNumber);
|
|
921
|
-
if (
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
.attr('id', id)
|
|
927
|
-
.attr('viewBox', '0 0 10 10')
|
|
928
|
-
.attr('refX', 10)
|
|
929
|
-
.attr('refY', 5)
|
|
930
|
-
.attr('markerUnits', 'userSpaceOnUse')
|
|
931
|
-
.attr('markerWidth', s)
|
|
932
|
-
.attr('markerHeight', s)
|
|
933
|
-
.attr('orient', 'auto-start-reverse')
|
|
976
|
+
if (arrowTri) {
|
|
977
|
+
// Filled arrowhead at the true endpoint — a flat triangle whose base sits
|
|
978
|
+
// where the trimmed stroke ends, so the line meets it cleanly with no
|
|
979
|
+
// show-through. Round join keeps the tip from looking spiky on thin legs.
|
|
980
|
+
gLegs
|
|
934
981
|
.append('path')
|
|
935
|
-
.attr('d',
|
|
936
|
-
.attr('fill', leg.color)
|
|
937
|
-
|
|
982
|
+
.attr('d', arrowTri)
|
|
983
|
+
.attr('fill', leg.color)
|
|
984
|
+
.attr('stroke', leg.color)
|
|
985
|
+
.attr('stroke-width', 1)
|
|
986
|
+
.attr('stroke-linejoin', 'round')
|
|
987
|
+
.attr('data-from-id', leg.fromId)
|
|
988
|
+
.attr('data-to-id', leg.toId);
|
|
938
989
|
}
|
|
939
990
|
if (leg.label !== undefined && leg.labelX !== undefined) {
|
|
940
991
|
// Text shade is contrast-picked in layout against the fill under the label
|
package/src/map/resolver.ts
CHANGED
|
@@ -420,7 +420,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
420
420
|
const hint = suggest(r.name, allNames);
|
|
421
421
|
err(
|
|
422
422
|
r.lineNumber,
|
|
423
|
-
`Unknown
|
|
423
|
+
`Unknown region "${r.name}" — not a known country or US state.${hint ? ' ' + hint : ''} Search the exact token with \`dgmo map-search "${r.name}"\` (or the lookup_map_location tool), use an ISO code (e.g. FR, US-CA), or coordinates.`,
|
|
424
424
|
'E_MAP_UNKNOWN_SUBDIVISION'
|
|
425
425
|
);
|
|
426
426
|
continue;
|
|
@@ -527,7 +527,7 @@ export function resolveMap(parsed: ParsedMap, data: MapData): ResolvedMap {
|
|
|
527
527
|
const hint = suggest(name, cityNames);
|
|
528
528
|
err(
|
|
529
529
|
line,
|
|
530
|
-
`Unknown place "${name}"
|
|
530
|
+
`Unknown place "${name}" — not in the gazetteer.${hint ? ' ' + hint : ''} Search the exact token with \`dgmo map-search "${name}"\` (or the lookup_map_location tool), or use coordinates \`poi <lat> <lon>\`.`,
|
|
531
531
|
'E_MAP_UNKNOWN_PLACE'
|
|
532
532
|
);
|
|
533
533
|
return { kind: 'miss' };
|
package/src/org/parser.ts
CHANGED
|
@@ -342,7 +342,10 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
|
|
|
342
342
|
// Find the parent node: top of stack (the most recent node)
|
|
343
343
|
const parent = findMetadataParent(indent, indentStack);
|
|
344
344
|
if (!parent) {
|
|
345
|
-
pushError(
|
|
345
|
+
pushError(
|
|
346
|
+
lineNumber,
|
|
347
|
+
"Metadata 'key: value' must be indented under a node — no node precedes it."
|
|
348
|
+
);
|
|
346
349
|
} else {
|
|
347
350
|
parent.metadata = { ...parent.metadata, [key]: value };
|
|
348
351
|
}
|
|
@@ -364,7 +367,10 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
|
|
|
364
367
|
);
|
|
365
368
|
attachNode(node, indent, indentStack, result);
|
|
366
369
|
} else {
|
|
367
|
-
pushError(
|
|
370
|
+
pushError(
|
|
371
|
+
lineNumber,
|
|
372
|
+
"Metadata 'key: value' must be indented under a node — no node precedes it."
|
|
373
|
+
);
|
|
368
374
|
}
|
|
369
375
|
} else {
|
|
370
376
|
// It's a node label — possibly with single-line metadata
|
package/src/pyramid/parser.ts
CHANGED
|
@@ -193,7 +193,10 @@ export function parsePyramid(content: string): ParsedPyramid {
|
|
|
193
193
|
|
|
194
194
|
// ── Indented: description line under current layer ──
|
|
195
195
|
if (!currentLayer) {
|
|
196
|
-
warn(
|
|
196
|
+
warn(
|
|
197
|
+
lineNum,
|
|
198
|
+
`Indented description "${trimmed}" has no layer above it — add a layer name on a non-indented line first. (§23)`
|
|
199
|
+
);
|
|
197
200
|
continue;
|
|
198
201
|
}
|
|
199
202
|
const descLine = trimmed.startsWith('- ')
|
package/src/raci/parser.ts
CHANGED
|
@@ -706,7 +706,10 @@ export function parseRaci(
|
|
|
706
706
|
|
|
707
707
|
// ── Catch-all: stray content line in body ────────────────────
|
|
708
708
|
if (bodyStarted) {
|
|
709
|
-
errorAt(
|
|
709
|
+
errorAt(
|
|
710
|
+
lineNumber,
|
|
711
|
+
`Unexpected line: '${trimmed}'. In the body expect a '[Phase]' header, an indented task name, or an assignment 'Role: A R C I' under a task. (§24A)`
|
|
712
|
+
);
|
|
710
713
|
} else {
|
|
711
714
|
// Pre-body unrecognized line — likely a typo'd directive. Tolerate
|
|
712
715
|
// by emitting a warning rather than failing the whole parse.
|
package/src/ring/parser.ts
CHANGED
|
@@ -205,7 +205,10 @@ export function parseRing(content: string): ParsedRing {
|
|
|
205
205
|
|
|
206
206
|
// ── Indented: description line under current layer ──
|
|
207
207
|
if (!currentLayer) {
|
|
208
|
-
warn(
|
|
208
|
+
warn(
|
|
209
|
+
lineNum,
|
|
210
|
+
`Indented description "${trimmed}" has no layer above it — add a layer name on a non-indented line first. (§24)`
|
|
211
|
+
);
|
|
209
212
|
continue;
|
|
210
213
|
}
|
|
211
214
|
const descLine = trimmed.startsWith('- ')
|
package/src/sequence/parser.ts
CHANGED
|
@@ -878,10 +878,13 @@ export function parseSequenceDgmo(
|
|
|
878
878
|
.trim()
|
|
879
879
|
.replace(/\([^)]*\)$/, '')
|
|
880
880
|
.trim();
|
|
881
|
-
const metaPart = rawInside
|
|
881
|
+
const metaPart = rawInside
|
|
882
|
+
.substring(pipeIdx)
|
|
883
|
+
.replace(/^\|\s*/, '')
|
|
884
|
+
.trim();
|
|
882
885
|
pushError(
|
|
883
886
|
lineNumber,
|
|
884
|
-
`
|
|
887
|
+
`Metadata goes after the bracket with no pipe — use '[${cleanName}] ${metaPart}' (e.g. '[Backend] t: Product'), not '[${rawInside.trim()}]'`
|
|
885
888
|
);
|
|
886
889
|
continue;
|
|
887
890
|
}
|
|
@@ -1676,7 +1679,7 @@ export function parseSequenceDgmo(
|
|
|
1676
1679
|
// Catch-all: nothing matched this line
|
|
1677
1680
|
pushWarning(
|
|
1678
1681
|
lineNumber,
|
|
1679
|
-
`Unexpected line: '${trimmed}'. Expected a message (A -> B), participant, section (== Name ==), or block
|
|
1682
|
+
`Unexpected line: '${trimmed}'. Expected a message ('A -> B' or 'A -label-> B'), participant ('Name' or 'Name is a actor'), note ('note right of A Text'), section ('== Name =='), or block ('if/loop/parallel <label>').`
|
|
1680
1683
|
);
|
|
1681
1684
|
}
|
|
1682
1685
|
|
package/src/sitemap/parser.ts
CHANGED
|
@@ -363,7 +363,10 @@ export function parseSitemap(
|
|
|
363
363
|
// at a shallower indent (same pattern as metadata attachment)
|
|
364
364
|
const source = findParentNode(indent, indentStack);
|
|
365
365
|
if (!source) {
|
|
366
|
-
pushError(
|
|
366
|
+
pushError(
|
|
367
|
+
lineNumber,
|
|
368
|
+
"Arrow '-> target' must be indented under its source page — no page precedes it."
|
|
369
|
+
);
|
|
367
370
|
} else {
|
|
368
371
|
deferredArrows.push({
|
|
369
372
|
sourceNode: source,
|
|
@@ -455,7 +458,10 @@ export function parseSitemap(
|
|
|
455
458
|
|
|
456
459
|
const parent = findParentNode(indent, indentStack);
|
|
457
460
|
if (!parent) {
|
|
458
|
-
pushError(
|
|
461
|
+
pushError(
|
|
462
|
+
lineNumber,
|
|
463
|
+
"Metadata 'key: value' must be indented under a page — no page precedes it."
|
|
464
|
+
);
|
|
459
465
|
} else {
|
|
460
466
|
parent.metadata = { ...parent.metadata, [key]: value };
|
|
461
467
|
}
|
|
@@ -476,7 +482,10 @@ export function parseSitemap(
|
|
|
476
482
|
const key = normalizeName(node.label);
|
|
477
483
|
labelToNode.set(key, node);
|
|
478
484
|
} else {
|
|
479
|
-
pushError(
|
|
485
|
+
pushError(
|
|
486
|
+
lineNumber,
|
|
487
|
+
"Metadata 'key: value' must be indented under a page — no page precedes it."
|
|
488
|
+
);
|
|
480
489
|
}
|
|
481
490
|
} else {
|
|
482
491
|
// Check if this is a description line for a parent node
|
package/src/tech-radar/parser.ts
CHANGED
|
@@ -144,7 +144,10 @@ export function parseTechRadar(content: string): ParsedTechRadar {
|
|
|
144
144
|
// --- Rings block ---
|
|
145
145
|
if (indent === 0 && trimmed.toLowerCase() === 'rings') {
|
|
146
146
|
if (result.rings.length > 0) {
|
|
147
|
-
warn(
|
|
147
|
+
warn(
|
|
148
|
+
lineNumber,
|
|
149
|
+
'Duplicate "rings" block — only the last is kept; merge all ring names into one "rings" block.'
|
|
150
|
+
);
|
|
148
151
|
result.rings = [];
|
|
149
152
|
}
|
|
150
153
|
inRingsBlock = true;
|
|
@@ -410,7 +413,7 @@ export function parseTechRadar(content: string): ParsedTechRadar {
|
|
|
410
413
|
result.diagnostics.push(
|
|
411
414
|
makeDgmoError(
|
|
412
415
|
lineNumber,
|
|
413
|
-
`Blip "${trimmed}" has no ring
|
|
416
|
+
`Blip "${trimmed}" has no ring. Put it under a ring section header, or add a colon-keyed ring — '${trimmed} ring: RingName' (per §20).`
|
|
414
417
|
)
|
|
415
418
|
);
|
|
416
419
|
continue;
|
|
@@ -421,7 +424,7 @@ export function parseTechRadar(content: string): ParsedTechRadar {
|
|
|
421
424
|
if (!trimmed.includes('|') && headerParsed && result.rings.length > 0) {
|
|
422
425
|
warn(
|
|
423
426
|
lineNumber,
|
|
424
|
-
`Unrecognized line "${trimmed}".
|
|
427
|
+
`Unrecognized line "${trimmed}". A quadrant header is 'Name quadrant: <top-left|top-right|bottom-left|bottom-right>' (per §20).`
|
|
425
428
|
);
|
|
426
429
|
}
|
|
427
430
|
}
|
package/src/utils/parsing.ts
CHANGED
|
@@ -564,9 +564,9 @@ export function peelRampColors(label: string): {
|
|
|
564
564
|
return { label: second.rest, low: second.color, high: first.color };
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
-
/** Error message for
|
|
567
|
+
/** Error message for stray pipe(s) where metadata used to go (0.18.0 removed `|`). */
|
|
568
568
|
export const MULTIPLE_PIPE_ERROR =
|
|
569
|
-
'
|
|
569
|
+
"'|' is no longer metadata syntax — put metadata on the same line as 'key: value, key2: value2' (no pipes), per §1.4.";
|
|
570
570
|
|
|
571
571
|
/**
|
|
572
572
|
* Parse metadata from segments after the first (name) segment.
|
package/src/utils/tag-groups.ts
CHANGED
|
@@ -524,12 +524,12 @@ export function validateTagValues(
|
|
|
524
524
|
);
|
|
525
525
|
if (!isPrefix) {
|
|
526
526
|
const defined = group.entries.map((e) => e.value);
|
|
527
|
-
let msg =
|
|
527
|
+
let msg =
|
|
528
|
+
`Unknown value '${value}' for tag group '${group.name}' — ` +
|
|
529
|
+
`use one of: ${defined.join(', ')}`;
|
|
528
530
|
const hint = suggestFn?.(value, defined);
|
|
529
531
|
if (hint) {
|
|
530
532
|
msg += `. ${hint}`;
|
|
531
|
-
} else {
|
|
532
|
-
msg += ` — defined values: ${defined.join(', ')}`;
|
|
533
533
|
}
|
|
534
534
|
pushWarning(entity.lineNumber, msg);
|
|
535
535
|
}
|
|
@@ -452,7 +452,10 @@ function parseVisualizationFull(
|
|
|
452
452
|
lineNumber,
|
|
453
453
|
});
|
|
454
454
|
} else {
|
|
455
|
-
warn(
|
|
455
|
+
warn(
|
|
456
|
+
lineNumber,
|
|
457
|
+
`Unrecognized era entry: "${line}" — use '<start> -> <end> Label [color]' (e.g. '1716 -> 1717 Reign').`
|
|
458
|
+
);
|
|
456
459
|
}
|
|
457
460
|
continue;
|
|
458
461
|
}
|
|
@@ -486,7 +489,10 @@ function parseVisualizationFull(
|
|
|
486
489
|
lineNumber,
|
|
487
490
|
});
|
|
488
491
|
} else {
|
|
489
|
-
warn(
|
|
492
|
+
warn(
|
|
493
|
+
lineNumber,
|
|
494
|
+
`Unrecognized marker entry: "${line}" — use '<date> Label [color]' (e.g. '1776 Independence').`
|
|
495
|
+
);
|
|
490
496
|
}
|
|
491
497
|
continue;
|
|
492
498
|
}
|
|
@@ -1411,7 +1417,7 @@ function parseVisualizationFull(
|
|
|
1411
1417
|
result.diagnostics.push(
|
|
1412
1418
|
makeDgmoError(
|
|
1413
1419
|
ov.lineNumber,
|
|
1414
|
-
`Intersection references unknown set or alias "${ref}"`
|
|
1420
|
+
`Intersection references unknown set or alias "${ref}" — declare the set first on its own line ('Name [as <alias>] [color]', e.g. 'Navigation as nav blue'), then reference it by that name or alias. (§16.4)`
|
|
1415
1421
|
)
|
|
1416
1422
|
);
|
|
1417
1423
|
if (!result.error)
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// Data-row comma migration for the `dgmo migrate` tool.
|
|
2
|
-
//
|
|
3
|
-
// 1.0 freeze: data-chart value separators are space-only and thousands
|
|
4
|
-
// commas are dropped (see `dgmo-language-spec-decisions.md` §"Silently-accepted
|
|
5
|
-
// dual forms" — Comma-row reconciliation). This module rewrites the VALUE
|
|
6
|
-
// region of a data-chart row, stripping value-separator and thousands commas
|
|
7
|
-
// while leaving everything else (label, trailing color word, arrow source/
|
|
8
|
-
// target, alias) byte-identical.
|
|
9
|
-
//
|
|
10
|
-
// Surgery, not re-parse: we never reconstruct the line from parsed values, so
|
|
11
|
-
// colors, aliases, and arrow endpoints survive verbatim. We only touch the
|
|
12
|
-
// trailing run of numeric value tokens.
|
|
13
|
-
|
|
14
|
-
import { RECOGNIZED_COLOR_NAMES } from '../colors';
|
|
15
|
-
|
|
16
|
-
/** Data-chart types whose rows carry comma-separable numeric values. */
|
|
17
|
-
export const DATA_COMMA_CHART_TYPES = new Set<string>([
|
|
18
|
-
// Simple charts (chart.ts)
|
|
19
|
-
'bar',
|
|
20
|
-
'line',
|
|
21
|
-
'multi-line',
|
|
22
|
-
'pie',
|
|
23
|
-
'doughnut',
|
|
24
|
-
'area',
|
|
25
|
-
'polar-area',
|
|
26
|
-
'radar',
|
|
27
|
-
'bar-stacked',
|
|
28
|
-
// Extended charts (echarts.ts)
|
|
29
|
-
'scatter',
|
|
30
|
-
'heatmap',
|
|
31
|
-
'sankey',
|
|
32
|
-
'chord',
|
|
33
|
-
'funnel',
|
|
34
|
-
// Visualizations (d3 / visualizations/parse.ts)
|
|
35
|
-
'quadrant',
|
|
36
|
-
'slope',
|
|
37
|
-
'arc',
|
|
38
|
-
'wordcloud',
|
|
39
|
-
]);
|
|
40
|
-
|
|
41
|
-
const COLOR_SET = new Set<string>(RECOGNIZED_COLOR_NAMES);
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* A token that is a numeric value, optionally carrying comma/underscore digit
|
|
45
|
-
* grouping. Accepts an optional leading `-`, optional trailing comma (the
|
|
46
|
-
* separator glued to the value, e.g. `400,`), and decimals.
|
|
47
|
-
*/
|
|
48
|
-
const NUMERIC_VALUE_RE = /^-?\d[\d,_]*(?:\.\d+)?,?$/;
|
|
49
|
-
|
|
50
|
-
/** True iff `tok` is a bare numeric value token (modulo grouping/separators). */
|
|
51
|
-
function isNumericValueToken(tok: string): boolean {
|
|
52
|
-
if (!NUMERIC_VALUE_RE.test(tok)) return false;
|
|
53
|
-
// Must contain at least one digit (guards against `,` alone).
|
|
54
|
-
return /\d/.test(tok);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Rewrite the value region of a single data-chart row, removing value-separator
|
|
59
|
-
* and thousands commas. Returns the comma-free line plus whether it changed.
|
|
60
|
-
* Lines without a comma in the value region are returned unchanged.
|
|
61
|
-
*
|
|
62
|
-
* The value region is the maximal trailing run of numeric value tokens
|
|
63
|
-
* (whitespace-separated, each optionally comma-grouped or comma-suffixed),
|
|
64
|
-
* optionally preceded — for the trailing-color-after-value forms — by nothing
|
|
65
|
-
* (a trailing color word sits AFTER values and is preserved verbatim because it
|
|
66
|
-
* is not numeric and stops the scan from the right).
|
|
67
|
-
*/
|
|
68
|
-
export function migrateDataRowCommaLine(line: string): {
|
|
69
|
-
line: string;
|
|
70
|
-
changed: boolean;
|
|
71
|
-
} {
|
|
72
|
-
// Preserve exact leading indentation.
|
|
73
|
-
const indentMatch = line.match(/^[ \t]*/);
|
|
74
|
-
const indent = indentMatch ? indentMatch[0] : '';
|
|
75
|
-
const body = line.substring(indent.length);
|
|
76
|
-
if (!body || body.startsWith('//')) return { line, changed: false };
|
|
77
|
-
|
|
78
|
-
// Split into whitespace tokens, keeping a simple structure. Commas may be
|
|
79
|
-
// glued to tokens (`400,`) or stand between number tokens after a comma-split
|
|
80
|
-
// — we normalize by splitting on whitespace then handling glued commas.
|
|
81
|
-
const tokens = body.split(/\s+/);
|
|
82
|
-
if (tokens.length < 2) return { line, changed: false };
|
|
83
|
-
|
|
84
|
-
// Walk from the right collecting the value region. A trailing color word is
|
|
85
|
-
// allowed as the very last token (sankey link color / data-point color) and
|
|
86
|
-
// is kept verbatim — we resume value collection before it.
|
|
87
|
-
let i = tokens.length - 1;
|
|
88
|
-
|
|
89
|
-
// Peel an optional trailing color word (kept as-is, not a value).
|
|
90
|
-
if (i >= 1 && COLOR_SET.has(tokens[i]!)) {
|
|
91
|
-
i--;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const valueStart = ((): number => {
|
|
95
|
-
let j = i;
|
|
96
|
-
while (j >= 1 && isNumericValueToken(tokens[j]!)) j--;
|
|
97
|
-
return j + 1; // first value-token index
|
|
98
|
-
})();
|
|
99
|
-
|
|
100
|
-
// No value region (need at least one numeric token, and a label before it).
|
|
101
|
-
if (valueStart > i || valueStart < 1) return { line, changed: false };
|
|
102
|
-
|
|
103
|
-
// The value tokens are tokens[valueStart..i]. Detect any comma among them.
|
|
104
|
-
const valueTokens = tokens.slice(valueStart, i + 1);
|
|
105
|
-
const hasComma = valueTokens.some((t) => t.includes(','));
|
|
106
|
-
if (!hasComma) return { line, changed: false };
|
|
107
|
-
|
|
108
|
-
// Strip commas: each numeric token loses its thousands/separator commas.
|
|
109
|
-
// A token like `400,` → `400`; `1,000` → `1000`; `1,234.56` → `1234.56`.
|
|
110
|
-
const cleaned = valueTokens.map((t) => t.replace(/,/g, ''));
|
|
111
|
-
|
|
112
|
-
// Rebuild: label (tokens before valueStart) + cleaned values + verbatim tail.
|
|
113
|
-
const labelPart = tokens.slice(0, valueStart);
|
|
114
|
-
const tail = tokens.slice(i + 1); // trailing color word(s), verbatim
|
|
115
|
-
const rebuiltBody = [...labelPart, ...cleaned, ...tail].join(' ');
|
|
116
|
-
return { line: indent + rebuiltBody, changed: rebuiltBody !== body };
|
|
117
|
-
}
|