@diagrammo/dgmo 0.32.1 → 0.33.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/.cursorrules +7 -2
- package/.github/copilot-instructions.md +7 -2
- package/.windsurfrules +7 -2
- package/SKILL.md +7 -2
- package/dist/advanced.cjs +981 -170
- package/dist/advanced.d.cts +35 -1
- package/dist/advanced.d.ts +35 -1
- package/dist/advanced.js +980 -170
- package/dist/auto.cjs +915 -171
- package/dist/auto.js +93 -93
- package/dist/auto.mjs +915 -171
- package/dist/cli.cjs +136 -134
- package/dist/index.cjs +981 -170
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +980 -170
- package/docs/ai-integration.md +7 -2
- package/docs/language-reference.md +39 -8
- package/package.json +2 -2
- package/src/advanced.ts +6 -1
- package/src/boxes-and-lines/layout-grouped.ts +909 -0
- package/src/boxes-and-lines/layout-search.ts +18 -1
- package/src/boxes-and-lines/parser.ts +3 -2
- package/src/c4/layout.ts +2 -1
- package/src/c4/parser.ts +3 -2
- package/src/c4/renderer.ts +3 -2
- package/src/cli.ts +68 -0
- package/src/er/parser.ts +2 -1
- package/src/er/renderer.ts +2 -1
- package/src/gantt/parser.ts +3 -5
- package/src/gantt/renderer.ts +7 -6
- package/src/index.ts +6 -1
- package/src/infra/parser.ts +2 -1
- package/src/infra/renderer.ts +4 -3
- package/src/journey-map/parser.ts +27 -8
- package/src/journey-map/renderer.ts +26 -9
- package/src/kanban/parser.ts +4 -3
- package/src/kanban/renderer.ts +6 -5
- package/src/map/completion.ts +116 -0
- package/src/map/layout.ts +4 -3
- package/src/map/parser.ts +3 -2
- package/src/mindmap/parser.ts +4 -3
- package/src/mindmap/renderer.ts +2 -1
- package/src/org/layout.ts +3 -2
- package/src/org/parser.ts +3 -2
- package/src/org/renderer.ts +5 -4
- package/src/pert/parser.ts +3 -2
- package/src/sequence/renderer.ts +10 -12
- package/src/sequence/tag-resolution.ts +2 -1
- package/src/sitemap/layout.ts +3 -2
- package/src/sitemap/parser.ts +36 -2
- package/src/sitemap/renderer.ts +12 -6
- package/src/timeline/renderer.ts +4 -3
- package/src/utils/legend-d3.ts +4 -3
- package/src/utils/legend-svg.ts +3 -2
- package/src/utils/tag-groups.ts +28 -7
- package/src/visualizations/parse.ts +2 -1
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type BLLayoutEdge,
|
|
23
23
|
} from './layout';
|
|
24
24
|
import { layeredCandidates } from './layout-layered';
|
|
25
|
+
import { groupedTierCandidates } from './layout-grouped';
|
|
25
26
|
|
|
26
27
|
type Pt = { x: number; y: number };
|
|
27
28
|
|
|
@@ -1528,6 +1529,21 @@ export async function layoutBoxesAndLinesSearch(
|
|
|
1528
1529
|
/* ignore */
|
|
1529
1530
|
}
|
|
1530
1531
|
|
|
1532
|
+
// Tier-banded grouped candidates. The dagre path treats `[Group]` containers
|
|
1533
|
+
// as compound clusters with no tier ORDER and no cycle-aware routing, so
|
|
1534
|
+
// tiered architecture diagrams collide bands side-by-side and balloon on
|
|
1535
|
+
// back-edges. This generator orders the groups into disjoint rank bands and
|
|
1536
|
+
// routes against-tier edges around the periphery. Additive — kept only on a
|
|
1537
|
+
// strict badness win. Skipped when any group is collapsed (the generator
|
|
1538
|
+
// models fully-expanded groups only).
|
|
1539
|
+
if (parsed.groups.length > 0 && collapsedGroupLabels.size === 0) {
|
|
1540
|
+
try {
|
|
1541
|
+
layered = layered.concat(groupedTierCandidates(parsed, sizes));
|
|
1542
|
+
} catch {
|
|
1543
|
+
/* ignore */
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1531
1547
|
// Stage 1: rank the dagre pool with the cheap straight-segment counter — a
|
|
1532
1548
|
// cheap proxy to pick which candidates are worth the expensive exact scoring.
|
|
1533
1549
|
// Widen REFINE_K a little since the proxy only sees crossings, not O/P.
|
|
@@ -1536,7 +1552,8 @@ export async function layoutBoxesAndLinesSearch(
|
|
|
1536
1552
|
// for both operands on every comparison (O(C log C) calls vs C). The score is a
|
|
1537
1553
|
// pure function of the layout, so the resulting order is identical.
|
|
1538
1554
|
const fastKey = new Map<BLLayoutResult, number>();
|
|
1539
|
-
for (const lay of pool)
|
|
1555
|
+
for (const lay of pool)
|
|
1556
|
+
fastKey.set(lay, objective(lay, countCrossingsFast(lay)));
|
|
1540
1557
|
pool.sort((a, b) => fastKey.get(a)! - fastKey.get(b)!);
|
|
1541
1558
|
const refineK = Math.min(REFINE_K, pool.length);
|
|
1542
1559
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
stripDefaultModifier,
|
|
22
22
|
finalizeAutoTagColors,
|
|
23
23
|
AUTO_TAG_COLOR_SENTINEL,
|
|
24
|
+
tagAttrKey,
|
|
24
25
|
} from '../utils/tag-groups';
|
|
25
26
|
import type { TagGroup } from '../utils/tag-groups';
|
|
26
27
|
import type { Writable } from '../utils/brand';
|
|
@@ -399,12 +400,12 @@ export function parseBoxesAndLines(
|
|
|
399
400
|
if (tagBlockMatch.alias) {
|
|
400
401
|
metaAliasMap.set(
|
|
401
402
|
normalizeName(tagBlockMatch.alias),
|
|
402
|
-
tagBlockMatch.name
|
|
403
|
+
tagAttrKey(tagBlockMatch.name)
|
|
403
404
|
);
|
|
404
405
|
}
|
|
405
406
|
metaAliasMap.set(
|
|
406
407
|
normalizeName(tagBlockMatch.name),
|
|
407
|
-
tagBlockMatch.name
|
|
408
|
+
tagAttrKey(tagBlockMatch.name)
|
|
408
409
|
);
|
|
409
410
|
if (tagBlockMatch.inlineValues) {
|
|
410
411
|
for (const rawVal of tagBlockMatch.inlineValues) {
|
package/src/c4/layout.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// C4 Context Diagram Layout Engine (dagre)
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import dagre from '@dagrejs/dagre';
|
|
6
7
|
import type {
|
|
7
8
|
ParsedC4,
|
|
@@ -619,7 +620,7 @@ function resolveNodeColor(
|
|
|
619
620
|
(g) => g.name.toLowerCase() === activeGroupName.toLowerCase()
|
|
620
621
|
);
|
|
621
622
|
if (!group) return undefined;
|
|
622
|
-
const key = group.name
|
|
623
|
+
const key = tagAttrKey(group.name);
|
|
623
624
|
// Walk inheritance chain: element → ancestors (container → system)
|
|
624
625
|
let metaValue: string | undefined = el.metadata[key];
|
|
625
626
|
if (!metaValue && ancestors) {
|
package/src/c4/parser.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
validateTagGroupNames,
|
|
25
25
|
finalizeAutoTagColors,
|
|
26
26
|
AUTO_TAG_COLOR_SENTINEL,
|
|
27
|
+
tagAttrKey,
|
|
27
28
|
} from '../utils/tag-groups';
|
|
28
29
|
import { inferParticipantType } from '../sequence/participant-inference';
|
|
29
30
|
import {
|
|
@@ -416,12 +417,12 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
|
416
417
|
if (tagBlockMatch.alias) {
|
|
417
418
|
metaAliasMap.set(
|
|
418
419
|
normalizeName(tagBlockMatch.alias),
|
|
419
|
-
tagBlockMatch.name
|
|
420
|
+
tagAttrKey(tagBlockMatch.name)
|
|
420
421
|
);
|
|
421
422
|
}
|
|
422
423
|
metaAliasMap.set(
|
|
423
424
|
normalizeName(tagBlockMatch.name),
|
|
424
|
-
tagBlockMatch.name
|
|
425
|
+
tagAttrKey(tagBlockMatch.name)
|
|
425
426
|
);
|
|
426
427
|
result.tagGroups.push(currentTagGroup);
|
|
427
428
|
continue;
|
package/src/c4/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// C4 Context Diagram SVG Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Selection from 'd3-selection';
|
|
6
7
|
import * as d3Shape from 'd3-shape';
|
|
7
8
|
import { FONT_FAMILY } from '../fonts';
|
|
@@ -432,7 +433,7 @@ export function renderC4Context(
|
|
|
432
433
|
.attr('data-node-id', node.id);
|
|
433
434
|
|
|
434
435
|
if (activeTagGroup) {
|
|
435
|
-
const tagKey = activeTagGroup
|
|
436
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
436
437
|
const tagValue = node.metadata[tagKey];
|
|
437
438
|
if (tagValue) {
|
|
438
439
|
nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
|
|
@@ -1517,7 +1518,7 @@ export function renderC4Containers(
|
|
|
1517
1518
|
.attr('data-node-id', node.id);
|
|
1518
1519
|
|
|
1519
1520
|
if (activeTagGroup) {
|
|
1520
|
-
const tagKey = activeTagGroup
|
|
1521
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
1521
1522
|
const tagValue = node.metadata[tagKey];
|
|
1522
1523
|
if (tagValue) {
|
|
1523
1524
|
nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
|
package/src/cli.ts
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
} from './migrate';
|
|
27
27
|
import { migrateEmbedded } from './migrate/embedded';
|
|
28
28
|
import { renderBanner } from './cli-banner';
|
|
29
|
+
import { searchMapLocations } from './map/completion';
|
|
30
|
+
import { loadMapData } from './map/load-data';
|
|
29
31
|
|
|
30
32
|
// Derived from the palette registry so new palettes are auto-included.
|
|
31
33
|
const PALETTES = getAvailablePalettes().map((p) => p.id);
|
|
@@ -470,6 +472,7 @@ function printHelp(): void {
|
|
|
470
472
|
cat input.dgmo | dgmo [options]
|
|
471
473
|
dgmo cat <file> Display file with syntax highlighting
|
|
472
474
|
dgmo migrate <path> Convert legacy "|" metadata to §1.4 grammar
|
|
475
|
+
dgmo map search <query> Find the map place token (city or IATA code)
|
|
473
476
|
|
|
474
477
|
Render a .dgmo file to PNG (default) or SVG.
|
|
475
478
|
|
|
@@ -859,6 +862,66 @@ async function runMigrateCommand(args: string[]): Promise<void> {
|
|
|
859
862
|
}
|
|
860
863
|
}
|
|
861
864
|
|
|
865
|
+
// `dgmo map search <query>` — discover the exact place token the map resolver
|
|
866
|
+
// expects (city name or bundled IATA airport code), so authors never guess
|
|
867
|
+
// (e.g. "New York" → `New York City`; "kennedy" → `JFK`). Searches cities +
|
|
868
|
+
// the bundled airport set; `--json` for machine output, `--limit N` to widen.
|
|
869
|
+
async function runMapCommand(args: string[]): Promise<void> {
|
|
870
|
+
const sub = args[0];
|
|
871
|
+
const json = args.includes('--json');
|
|
872
|
+
const limFlag = args.indexOf('--limit');
|
|
873
|
+
const limit =
|
|
874
|
+
limFlag >= 0 ? Math.max(1, Number(args[limFlag + 1]) || 20) : 20;
|
|
875
|
+
// The query is the positional args (allow unquoted multi-word: `... New York`).
|
|
876
|
+
const query = args
|
|
877
|
+
.slice(1)
|
|
878
|
+
.filter(
|
|
879
|
+
(a, i, arr) =>
|
|
880
|
+
a !== '--json' && a !== '--limit' && arr[i - 1] !== '--limit'
|
|
881
|
+
)
|
|
882
|
+
.join(' ')
|
|
883
|
+
.trim();
|
|
884
|
+
|
|
885
|
+
if (sub !== 'search' || !query) {
|
|
886
|
+
console.log('Usage: dgmo map search <query> [--json] [--limit N]');
|
|
887
|
+
console.log(
|
|
888
|
+
' Find the place token the map resolver expects (city or IATA airport code).'
|
|
889
|
+
);
|
|
890
|
+
console.log(' Examples:');
|
|
891
|
+
console.log(
|
|
892
|
+
' dgmo map search "new york" # → New York City (+ JFK/LGA/EWR airports)'
|
|
893
|
+
);
|
|
894
|
+
console.log(' dgmo map search kennedy # → JFK by airport name');
|
|
895
|
+
console.log(' dgmo map search heathrow --json');
|
|
896
|
+
if (sub === 'search' && !query) process.exitCode = 1;
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const data = await loadMapData();
|
|
901
|
+
const results = searchMapLocations(query, data.gazetteer, {
|
|
902
|
+
limit,
|
|
903
|
+
...(data.airports && { airports: data.airports }),
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
if (json) {
|
|
907
|
+
process.stdout.write(JSON.stringify({ query, results }, null, 2) + '\n');
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (!results.length) {
|
|
911
|
+
console.log(
|
|
912
|
+
`No cities or airports match "${query}". Use coordinates: poi Name as <lat,lng>`
|
|
913
|
+
);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
console.log(
|
|
917
|
+
`Places matching "${query}" — use the token column in your DGMO:`
|
|
918
|
+
);
|
|
919
|
+
for (const r of results) {
|
|
920
|
+
const tag = r.kind === 'airport' ? '✈' : '•';
|
|
921
|
+
console.log(` ${tag} ${r.token.padEnd(22)} ${r.detail}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
862
925
|
async function main(): Promise<void> {
|
|
863
926
|
// Subcommand dispatch — `dgmo migrate` is parsed independently from
|
|
864
927
|
// the rendering flags so its surface doesn't pollute `parseArgs`.
|
|
@@ -867,6 +930,11 @@ async function main(): Promise<void> {
|
|
|
867
930
|
return;
|
|
868
931
|
}
|
|
869
932
|
|
|
933
|
+
if (process.argv[2] === 'map') {
|
|
934
|
+
await runMapCommand(process.argv.slice(3));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
870
938
|
const opts = parseArgs(process.argv);
|
|
871
939
|
|
|
872
940
|
if (opts.help) {
|
package/src/er/parser.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
stripDefaultModifier,
|
|
32
32
|
finalizeAutoTagColors,
|
|
33
33
|
AUTO_TAG_COLOR_SENTINEL,
|
|
34
|
+
tagAttrKey,
|
|
34
35
|
} from '../utils/tag-groups';
|
|
35
36
|
import type { TagGroup } from '../utils/tag-groups';
|
|
36
37
|
import { tryCollectNote, resolveNotes, type DiagramNote } from '../utils/notes';
|
|
@@ -608,7 +609,7 @@ export function parseERDiagram(
|
|
|
608
609
|
// Inject defaults for tables without explicit tags
|
|
609
610
|
for (const group of result.tagGroups) {
|
|
610
611
|
if (!group.defaultValue) continue;
|
|
611
|
-
const key = group.name
|
|
612
|
+
const key = tagAttrKey(group.name);
|
|
612
613
|
for (const table of result.tables) {
|
|
613
614
|
if (!table.metadata[key]) {
|
|
614
615
|
const mutableMeta = tableMetadataMap.get(table.id);
|
package/src/er/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// ER Diagram SVG Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Selection from 'd3-selection';
|
|
6
7
|
import * as d3Shape from 'd3-shape';
|
|
7
8
|
import { FONT_FAMILY } from '../fonts';
|
|
@@ -455,7 +456,7 @@ export function renderERDiagram(
|
|
|
455
456
|
|
|
456
457
|
// Set data-tag-* attributes for legend hover
|
|
457
458
|
if (activeTagGroup) {
|
|
458
|
-
const tagKey = activeTagGroup
|
|
459
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
459
460
|
const tagValue = node.metadata[tagKey];
|
|
460
461
|
if (tagValue) {
|
|
461
462
|
nodeG.attr(`data-tag-${tagKey}`, tagValue.toLowerCase());
|
package/src/gantt/parser.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
emitTagLegacyDiagnostic,
|
|
26
26
|
stripDefaultModifier,
|
|
27
27
|
validateTagGroupNames,
|
|
28
|
+
tagAttrKey,
|
|
28
29
|
} from '../utils/tag-groups';
|
|
29
30
|
import {
|
|
30
31
|
measureIndent,
|
|
@@ -814,13 +815,10 @@ export function parseGantt(
|
|
|
814
815
|
if (tagMatch.alias) {
|
|
815
816
|
metaAliasMap.set(
|
|
816
817
|
normalizeName(tagMatch.alias),
|
|
817
|
-
tagMatch.name
|
|
818
|
+
tagAttrKey(tagMatch.name)
|
|
818
819
|
);
|
|
819
820
|
}
|
|
820
|
-
metaAliasMap.set(
|
|
821
|
-
normalizeName(tagMatch.name),
|
|
822
|
-
tagMatch.name.toLowerCase()
|
|
823
|
-
);
|
|
821
|
+
metaAliasMap.set(normalizeName(tagMatch.name), tagAttrKey(tagMatch.name));
|
|
824
822
|
continue;
|
|
825
823
|
}
|
|
826
824
|
|
package/src/gantt/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Gantt Chart Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Scale from 'd3-scale';
|
|
6
7
|
import * as d3Selection from 'd3-selection';
|
|
7
8
|
import { FONT_FAMILY } from '../fonts';
|
|
@@ -660,7 +661,7 @@ export function renderGantt(
|
|
|
660
661
|
(tg) => tg.name.toLowerCase() === currentSwimlaneGroup.toLowerCase()
|
|
661
662
|
);
|
|
662
663
|
if (tagGroup) {
|
|
663
|
-
const tagKey = tagGroup.name
|
|
664
|
+
const tagKey = tagAttrKey(tagGroup.name);
|
|
664
665
|
for (const rt of resolved.tasks) {
|
|
665
666
|
let value = rt.effectiveMetadata[tagKey];
|
|
666
667
|
if (!value && tagGroup.defaultValue) value = tagGroup.defaultValue;
|
|
@@ -2091,7 +2092,7 @@ function renderTagLegend(
|
|
|
2091
2092
|
const usedValues = new Map<string, Set<string>>();
|
|
2092
2093
|
if (resolvedTasks) {
|
|
2093
2094
|
for (const group of visibleGroups) {
|
|
2094
|
-
const key = group.name
|
|
2095
|
+
const key = tagAttrKey(group.name);
|
|
2095
2096
|
const used = new Set<string>();
|
|
2096
2097
|
for (const rt of resolvedTasks) {
|
|
2097
2098
|
const val = rt.effectiveMetadata[key];
|
|
@@ -2104,7 +2105,7 @@ function renderTagLegend(
|
|
|
2104
2105
|
// Filter entries to only those used in the current view
|
|
2105
2106
|
const filteredEntries = new Map<string, readonly TagEntry[]>();
|
|
2106
2107
|
for (const group of visibleGroups) {
|
|
2107
|
-
const key = group.name
|
|
2108
|
+
const key = tagAttrKey(group.name);
|
|
2108
2109
|
const used = usedValues.get(key);
|
|
2109
2110
|
if (used && used.size > 0) {
|
|
2110
2111
|
filteredEntries.set(
|
|
@@ -2133,7 +2134,7 @@ function renderTagLegend(
|
|
|
2133
2134
|
let groupW = pillW;
|
|
2134
2135
|
if (isActive) {
|
|
2135
2136
|
const entries =
|
|
2136
|
-
filteredEntries.get(group.name
|
|
2137
|
+
filteredEntries.get(tagAttrKey(group.name)) ?? group.entries;
|
|
2137
2138
|
let entriesW = 0;
|
|
2138
2139
|
for (const entry of entries) {
|
|
2139
2140
|
entriesW +=
|
|
@@ -2215,7 +2216,7 @@ function renderTagLegend(
|
|
|
2215
2216
|
}),
|
|
2216
2217
|
...(onControlsToggle !== undefined && { onControlsToggle }),
|
|
2217
2218
|
onEntryHover: (groupName, entryValue) => {
|
|
2218
|
-
const tagKey = groupName
|
|
2219
|
+
const tagKey = tagAttrKey(groupName);
|
|
2219
2220
|
if (entryValue) {
|
|
2220
2221
|
const ev = entryValue.toLowerCase();
|
|
2221
2222
|
chartG
|
|
@@ -3388,7 +3389,7 @@ export function buildTagLaneRowList(
|
|
|
3388
3389
|
);
|
|
3389
3390
|
if (!tagGroup) return null;
|
|
3390
3391
|
|
|
3391
|
-
const tagKey = tagGroup.name
|
|
3392
|
+
const tagKey = tagAttrKey(tagGroup.name);
|
|
3392
3393
|
const rows: Row[] = [];
|
|
3393
3394
|
|
|
3394
3395
|
// Bucket tasks by tag value
|
package/src/index.ts
CHANGED
|
@@ -226,11 +226,16 @@ export { normalizeSvgForEmbed, getEmbedSvgViewBox } from './utils/svg-embed';
|
|
|
226
226
|
// ============================================================
|
|
227
227
|
// Pure + dependency-injected: the caller supplies the `Gazetteer` asset. Safe
|
|
228
228
|
// to export from the main index — no d3-geo/topojson runtime imports.
|
|
229
|
-
export {
|
|
229
|
+
export {
|
|
230
|
+
completeMapPlaces,
|
|
231
|
+
completeMapRegions,
|
|
232
|
+
searchMapLocations,
|
|
233
|
+
} from './map/completion';
|
|
230
234
|
export type {
|
|
231
235
|
MapPlaceCompletion,
|
|
232
236
|
MapRegionCompletion,
|
|
233
237
|
MapCompletionOptions,
|
|
238
|
+
MapLocationMatch,
|
|
234
239
|
} from './map/completion';
|
|
235
240
|
export type {
|
|
236
241
|
Gazetteer,
|
package/src/infra/parser.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Handles: chart metadata, component blocks with indented properties
|
|
7
7
|
// and connections, [Group] containers, tag groups, pipe metadata.
|
|
8
8
|
|
|
9
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
9
10
|
import {
|
|
10
11
|
descriptionBareRemovedMessage,
|
|
11
12
|
formatDgmoError,
|
|
@@ -1054,7 +1055,7 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
1054
1055
|
// Inject default tag values into nodes that don't have one
|
|
1055
1056
|
for (const tg of result.tagGroups) {
|
|
1056
1057
|
if (!tg.defaultValue) continue;
|
|
1057
|
-
const key = (tg.alias ?? tg.name)
|
|
1058
|
+
const key = tagAttrKey(tg.alias ?? tg.name);
|
|
1058
1059
|
for (const node of result.nodes) {
|
|
1059
1060
|
if (node.isEdge) continue;
|
|
1060
1061
|
if (!(key in node.tags)) {
|
package/src/infra/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Infra Chart SVG Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Selection from 'd3-selection';
|
|
6
7
|
import * as d3Shape from 'd3-shape';
|
|
7
8
|
import { FONT_FAMILY } from '../fonts';
|
|
@@ -1388,7 +1389,7 @@ function resolveActiveTagStroke(
|
|
|
1388
1389
|
(t) => t.name.toLowerCase() === activeGroup.toLowerCase()
|
|
1389
1390
|
);
|
|
1390
1391
|
if (!tg) return null;
|
|
1391
|
-
const tagKey = (tg.alias ?? tg.name)
|
|
1392
|
+
const tagKey = tagAttrKey(tg.alias ?? tg.name);
|
|
1392
1393
|
const tagVal = node.tags[tagKey];
|
|
1393
1394
|
if (!tagVal) return null;
|
|
1394
1395
|
const tv = tg.values.find(
|
|
@@ -2073,7 +2074,7 @@ export function computeInfraLegendGroups(
|
|
|
2073
2074
|
groups.push({
|
|
2074
2075
|
name: tg.name,
|
|
2075
2076
|
type: 'tag',
|
|
2076
|
-
tagKey: (tg.alias ?? tg.name)
|
|
2077
|
+
tagKey: tagAttrKey(tg.alias ?? tg.name),
|
|
2077
2078
|
entries,
|
|
2078
2079
|
width: LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth,
|
|
2079
2080
|
minifiedWidth: pillWidth,
|
|
@@ -2146,7 +2147,7 @@ function renderLegend(
|
|
|
2146
2147
|
// Add infra-specific classes and data attributes for app interactivity
|
|
2147
2148
|
legendG.selectAll('[data-legend-group]').classed('infra-legend-group', true);
|
|
2148
2149
|
for (const group of legendGroups) {
|
|
2149
|
-
const groupKey = group.name
|
|
2150
|
+
const groupKey = tagAttrKey(group.name);
|
|
2150
2151
|
for (const entry of group.entries) {
|
|
2151
2152
|
const entryEl = legendG.select(
|
|
2152
2153
|
`[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
JOURNEY_MAP_REGISTRY,
|
|
15
15
|
withTagAliases,
|
|
16
16
|
} from '../utils/reserved-key-registry';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
splitNameAndMeta,
|
|
19
|
+
warnUnknownMetaKeys,
|
|
20
|
+
peelTrailingColorName,
|
|
21
|
+
} from '../utils/parsing';
|
|
18
22
|
import {
|
|
19
23
|
matchTagBlockHeading,
|
|
20
24
|
emitTagLegacyDiagnostic,
|
|
@@ -22,6 +26,7 @@ import {
|
|
|
22
26
|
validateTagGroupNames,
|
|
23
27
|
finalizeAutoTagColors,
|
|
24
28
|
AUTO_TAG_COLOR_SENTINEL,
|
|
29
|
+
tagAttrKey,
|
|
25
30
|
} from '../utils/tag-groups';
|
|
26
31
|
import {
|
|
27
32
|
measureIndent,
|
|
@@ -180,8 +185,10 @@ export function parseJourneyMap(
|
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
} else {
|
|
183
|
-
// Same-line form (pipes removed in 0.18.0)
|
|
184
|
-
// `color: <token>`
|
|
188
|
+
// Same-line form (pipes removed in 0.18.0). Two ways to set color:
|
|
189
|
+
// 1. explicit `persona <name> color: <token>` (long form)
|
|
190
|
+
// 2. universal §1.5 trailing-token color (`persona <name> green`);
|
|
191
|
+
// capitalize the word (`Green`) to keep it as literal name text.
|
|
185
192
|
const colorMatch = afterKeyword.match(/^(.+?)\s+color:\s*(\S+)$/i);
|
|
186
193
|
if (colorMatch) {
|
|
187
194
|
personaName = colorMatch[1]!.trim();
|
|
@@ -193,7 +200,19 @@ export function parseJourneyMap(
|
|
|
193
200
|
palette
|
|
194
201
|
) ?? undefined;
|
|
195
202
|
} else {
|
|
196
|
-
|
|
203
|
+
const { label, colorName } = peelTrailingColorName(afterKeyword);
|
|
204
|
+
if (colorName) {
|
|
205
|
+
personaName = label;
|
|
206
|
+
personaColor =
|
|
207
|
+
resolveColorWithDiagnostic(
|
|
208
|
+
colorName,
|
|
209
|
+
lineNumber,
|
|
210
|
+
result.diagnostics,
|
|
211
|
+
palette
|
|
212
|
+
) ?? undefined;
|
|
213
|
+
} else {
|
|
214
|
+
personaName = afterKeyword;
|
|
215
|
+
}
|
|
197
216
|
}
|
|
198
217
|
}
|
|
199
218
|
|
|
@@ -246,12 +265,12 @@ export function parseJourneyMap(
|
|
|
246
265
|
if (tagBlockMatch.alias) {
|
|
247
266
|
aliasMap.set(
|
|
248
267
|
tagBlockMatch.alias.toLowerCase(),
|
|
249
|
-
tagBlockMatch.name
|
|
268
|
+
tagAttrKey(tagBlockMatch.name)
|
|
250
269
|
);
|
|
251
270
|
}
|
|
252
271
|
aliasMap.set(
|
|
253
|
-
tagBlockMatch.name
|
|
254
|
-
tagBlockMatch.name
|
|
272
|
+
tagAttrKey(tagBlockMatch.name),
|
|
273
|
+
tagAttrKey(tagBlockMatch.name)
|
|
255
274
|
);
|
|
256
275
|
result.tagGroups.push(currentTagGroup);
|
|
257
276
|
continue;
|
|
@@ -445,7 +464,7 @@ export function parseJourneyMap(
|
|
|
445
464
|
// Build tag value sets
|
|
446
465
|
for (const group of result.tagGroups) {
|
|
447
466
|
const values = new Set(group.entries.map((e) => e.value.toLowerCase()));
|
|
448
|
-
tagValueSets.set(group.name
|
|
467
|
+
tagValueSets.set(tagAttrKey(group.name), values);
|
|
449
468
|
}
|
|
450
469
|
|
|
451
470
|
// Validate tag values on all steps
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
1
2
|
import * as d3 from 'd3-selection';
|
|
2
3
|
import * as d3Shape from 'd3-shape';
|
|
3
4
|
import type { PaletteColors } from '../palettes';
|
|
@@ -18,6 +19,7 @@ import type {
|
|
|
18
19
|
JourneyMapStep,
|
|
19
20
|
} from './types';
|
|
20
21
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
22
|
+
import { wrapNoteBody, NOTE_BULLET_INDENT } from '../utils/note-box';
|
|
21
23
|
import { renderIntegratedLegend } from '../utils/legend-integration';
|
|
22
24
|
import { resolveActiveTagGroup } from '../utils/tag-groups';
|
|
23
25
|
import { ScaleContext } from '../utils/scaling';
|
|
@@ -194,9 +196,10 @@ export function renderJourneyMap(
|
|
|
194
196
|
const textAreaWidth = panelWidth - silhouetteZone - CARD_PADDING_X;
|
|
195
197
|
const descLineH = 14;
|
|
196
198
|
|
|
197
|
-
// Wrap description text into lines
|
|
199
|
+
// Wrap description text into lines — bullet-aware (`- `/`* ` → hanging
|
|
200
|
+
// "•") + inline markdown, matching how note boxes render rich bodies.
|
|
198
201
|
const descLines = parsed.persona.description
|
|
199
|
-
?
|
|
202
|
+
? wrapNoteBody(parsed.persona.description, textAreaWidth, FONT_SIZE_META)
|
|
200
203
|
: [];
|
|
201
204
|
const descRowH =
|
|
202
205
|
descLines.length > 0 ? descLines.length * descLineH + 8 : 0;
|
|
@@ -276,16 +279,30 @@ export function renderJourneyMap(
|
|
|
276
279
|
.attr('fill', onPersonaText)
|
|
277
280
|
.text(parsed.persona.name);
|
|
278
281
|
|
|
279
|
-
// Description — wrapped lines below divider, with inline markdown
|
|
282
|
+
// Description — wrapped lines below divider, with inline markdown.
|
|
283
|
+
// Bullet first-lines get a "•" glyph at the left edge with the body
|
|
284
|
+
// hanging-indented; continuation lines align under the body.
|
|
280
285
|
for (let li = 0; li < descLines.length; li++) {
|
|
286
|
+
// In-bounds by loop guard.
|
|
287
|
+
const line = descLines[li]!;
|
|
288
|
+
const indent = line.kind === 'plain' ? 0 : NOTE_BULLET_INDENT;
|
|
289
|
+
const lineY = panelY + titleRowH + descLineH * (li + 1);
|
|
290
|
+
if (line.kind === 'bullet-first') {
|
|
291
|
+
personaG
|
|
292
|
+
.append('text')
|
|
293
|
+
.attr('x', textX)
|
|
294
|
+
.attr('y', lineY)
|
|
295
|
+
.attr('font-size', FONT_SIZE_META)
|
|
296
|
+
.attr('fill', onPersonaText)
|
|
297
|
+
.text('•');
|
|
298
|
+
}
|
|
281
299
|
const lineEl = personaG
|
|
282
300
|
.append('text')
|
|
283
|
-
.attr('x', textX)
|
|
284
|
-
.attr('y',
|
|
301
|
+
.attr('x', textX + indent)
|
|
302
|
+
.attr('y', lineY)
|
|
285
303
|
.attr('font-size', FONT_SIZE_META)
|
|
286
304
|
.attr('fill', onPersonaText);
|
|
287
|
-
|
|
288
|
-
renderInlineText(lineEl, descLines[li]!, palette, FONT_SIZE_META);
|
|
305
|
+
renderInlineText(lineEl, line.text, palette, FONT_SIZE_META);
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
if (onNavigateToLine) {
|
|
@@ -346,7 +363,7 @@ export function renderJourneyMap(
|
|
|
346
363
|
const isScore = groupName === 'Score';
|
|
347
364
|
const attrName = isScore
|
|
348
365
|
? 'data-score'
|
|
349
|
-
: `data-tag-${groupName
|
|
366
|
+
: `data-tag-${tagAttrKey(groupName)}`;
|
|
350
367
|
|
|
351
368
|
const matches = (el: Element) =>
|
|
352
369
|
el.getAttribute(attrName) === entryValue;
|
|
@@ -1094,7 +1111,7 @@ function resolveStepColor(
|
|
|
1094
1111
|
);
|
|
1095
1112
|
if (!group) return undefined;
|
|
1096
1113
|
|
|
1097
|
-
const tagValue = step.tags[group.name
|
|
1114
|
+
const tagValue = step.tags[tagAttrKey(group.name)];
|
|
1098
1115
|
if (!tagValue) return undefined;
|
|
1099
1116
|
|
|
1100
1117
|
const entry = group.entries.find(
|
package/src/kanban/parser.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
validateTagGroupNames,
|
|
20
20
|
finalizeAutoTagColors,
|
|
21
21
|
AUTO_TAG_COLOR_SENTINEL,
|
|
22
|
+
tagAttrKey,
|
|
22
23
|
} from '../utils/tag-groups';
|
|
23
24
|
import {
|
|
24
25
|
measureIndent,
|
|
@@ -168,14 +169,14 @@ export function parseKanban(
|
|
|
168
169
|
if (tagBlockMatch.alias) {
|
|
169
170
|
metaAliasMap.set(
|
|
170
171
|
normalizeName(tagBlockMatch.alias),
|
|
171
|
-
tagBlockMatch.name
|
|
172
|
+
tagAttrKey(tagBlockMatch.name)
|
|
172
173
|
);
|
|
173
174
|
}
|
|
174
175
|
// §1.4 dispatch: register the canonical tag name so `priority: X`
|
|
175
176
|
// also dispatches as metadata (not just the optional `p: X` alias).
|
|
176
177
|
metaAliasMap.set(
|
|
177
178
|
normalizeName(tagBlockMatch.name),
|
|
178
|
-
tagBlockMatch.name
|
|
179
|
+
tagAttrKey(tagBlockMatch.name)
|
|
179
180
|
);
|
|
180
181
|
result.tagGroups.push(currentTagGroup);
|
|
181
182
|
continue;
|
|
@@ -419,7 +420,7 @@ export function parseKanban(
|
|
|
419
420
|
// Build tag value sets for validation
|
|
420
421
|
for (const group of result.tagGroups) {
|
|
421
422
|
const values = new Set(group.entries.map((e) => e.value.toLowerCase()));
|
|
422
|
-
tagValueSets.set(group.name
|
|
423
|
+
tagValueSets.set(tagAttrKey(group.name), values);
|
|
423
424
|
}
|
|
424
425
|
|
|
425
426
|
// Validate WIP limits
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Kanban Board SVG Renderer
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import * as d3Selection from 'd3-selection';
|
|
6
7
|
import { FONT_FAMILY } from '../fonts';
|
|
7
8
|
import type { PaletteColors } from '../palettes';
|
|
@@ -82,7 +83,7 @@ function resolveCardTagMeta(
|
|
|
82
83
|
const meta: { label: string; value: string; color?: string }[] = [];
|
|
83
84
|
for (const group of tagGroups) {
|
|
84
85
|
if (hiddenMetaGroups?.includes(group.name.toLowerCase())) continue;
|
|
85
|
-
const tagValue = card.tags[group.name
|
|
86
|
+
const tagValue = card.tags[tagAttrKey(group.name)];
|
|
86
87
|
const value = tagValue ?? group.defaultValue;
|
|
87
88
|
if (!value) continue;
|
|
88
89
|
const entry = group.entries.find(
|
|
@@ -107,7 +108,7 @@ function resolveCardTagColor(
|
|
|
107
108
|
(g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
|
|
108
109
|
);
|
|
109
110
|
if (!group) return card.color;
|
|
110
|
-
const tagValue = card.tags[group.name
|
|
111
|
+
const tagValue = card.tags[tagAttrKey(group.name)];
|
|
111
112
|
const value = tagValue ?? group.defaultValue;
|
|
112
113
|
if (!value) return undefined;
|
|
113
114
|
const entry = group.entries.find(
|
|
@@ -639,7 +640,7 @@ export function renderKanban(
|
|
|
639
640
|
.attr('data-line-number', card.lineNumber);
|
|
640
641
|
|
|
641
642
|
if (activeTagGroup) {
|
|
642
|
-
const tagKey = activeTagGroup
|
|
643
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
643
644
|
const tagValue = card.tags[tagKey];
|
|
644
645
|
const group = parsed.tagGroups.find(
|
|
645
646
|
(tg) => tg.name.toLowerCase() === tagKey
|
|
@@ -806,7 +807,7 @@ function bucketCardsBySwimlane(
|
|
|
806
807
|
columns: KanbanColumn[],
|
|
807
808
|
swimlaneGroup: KanbanTagGroup
|
|
808
809
|
): SwimlaneBucket[] {
|
|
809
|
-
const tagKey = swimlaneGroup.name
|
|
810
|
+
const tagKey = tagAttrKey(swimlaneGroup.name);
|
|
810
811
|
const buckets = new Map<string, SwimlaneBucket>();
|
|
811
812
|
|
|
812
813
|
// Seed lanes from declaration order
|
|
@@ -1369,7 +1370,7 @@ function renderSwimlaneCard(
|
|
|
1369
1370
|
.attr('data-line-number', card.lineNumber);
|
|
1370
1371
|
|
|
1371
1372
|
if (activeTagGroup) {
|
|
1372
|
-
const tagKey = activeTagGroup
|
|
1373
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
1373
1374
|
const group = tagGroups.find((tg) => tg.name.toLowerCase() === tagKey);
|
|
1374
1375
|
const value = card.tags[tagKey] ?? group?.defaultValue;
|
|
1375
1376
|
if (value) {
|