@diagrammo/dgmo 0.32.0 → 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 +55 -24
- 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
package/src/map/completion.ts
CHANGED
|
@@ -151,6 +151,122 @@ export function completeMapPlaces(
|
|
|
151
151
|
return [...cityItems, ...airportItems].slice(0, limit);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/** A POI search hit — a city or airport the resolver can resolve, with the
|
|
155
|
+
* exact `token` to paste into DGMO (a `poi`/route endpoint). Powers the CLI
|
|
156
|
+
* `dgmo map search` command and the MCP `lookup_map_location` tool, so authors
|
|
157
|
+
* (and AI agents) can DISCOVER valid place tokens instead of guessing — e.g.
|
|
158
|
+
* learn that "New York" must be `New York City` and that `JFK` is bundled. */
|
|
159
|
+
export interface MapLocationMatch {
|
|
160
|
+
/** Canonical display name (cities) or full airport name. */
|
|
161
|
+
readonly name: string;
|
|
162
|
+
/** Exact text to use in DGMO. Cities: the name, ISO-qualified when ambiguous
|
|
163
|
+
* (`Portland US-OR`). Airports: the upper-case IATA code (`JFK`). */
|
|
164
|
+
readonly token: string;
|
|
165
|
+
readonly kind: 'city' | 'airport';
|
|
166
|
+
readonly iso: string;
|
|
167
|
+
readonly sub?: string;
|
|
168
|
+
/** City population (0 for airports). */
|
|
169
|
+
readonly pop: number;
|
|
170
|
+
/** Human detail, e.g. `US-OR · 652,503` or `Airport · John F Kennedy Intl`. */
|
|
171
|
+
readonly detail: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Substring-search cities + airports for a discovery surface (CLI / MCP), so an
|
|
176
|
+
* author can find the exact token the resolver expects. Unlike
|
|
177
|
+
* {@link completeMapPlaces} (prefix-only, editor type-ahead), this matches
|
|
178
|
+
* anywhere in a city name OR an airport code/name, so "york" finds
|
|
179
|
+
* `New York City` and "kennedy" finds `JFK`.
|
|
180
|
+
*
|
|
181
|
+
* Ranking: exact-name (or exact-IATA) → prefix → substring; within a tier,
|
|
182
|
+
* cities by population desc (deterministic index tie-break), cities before
|
|
183
|
+
* airports. Pure + deterministic. Empty query → `[]`.
|
|
184
|
+
*/
|
|
185
|
+
export function searchMapLocations(
|
|
186
|
+
query: string,
|
|
187
|
+
gazetteer: Gazetteer,
|
|
188
|
+
opts?: { readonly limit?: number; readonly airports?: AirportData }
|
|
189
|
+
): MapLocationMatch[] {
|
|
190
|
+
const q = fold(query);
|
|
191
|
+
if (!q) return [];
|
|
192
|
+
const limit = opts?.limit ?? 20;
|
|
193
|
+
|
|
194
|
+
// rank: 0 exact, 1 prefix, 2 substring; -1 = no match.
|
|
195
|
+
const rankOf = (hay: string): number =>
|
|
196
|
+
hay === q ? 0 : hay.startsWith(q) ? 1 : hay.includes(q) ? 2 : -1;
|
|
197
|
+
|
|
198
|
+
// Cities — match on folded display name (via byName keys) + aliases.
|
|
199
|
+
const cityRank = new Map<number, number>(); // city index → best rank
|
|
200
|
+
const consider = (idx: number, rank: number): void => {
|
|
201
|
+
if (rank < 0) return;
|
|
202
|
+
const prev = cityRank.get(idx);
|
|
203
|
+
if (prev === undefined || rank < prev) cityRank.set(idx, rank);
|
|
204
|
+
};
|
|
205
|
+
for (const [key, list] of Object.entries(gazetteer.byName)) {
|
|
206
|
+
const r = rankOf(key);
|
|
207
|
+
if (r >= 0) for (const i of list) consider(i, r);
|
|
208
|
+
}
|
|
209
|
+
for (const [key, idx] of Object.entries(gazetteer.alt)) {
|
|
210
|
+
consider(idx, rankOf(key));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const cityItems: Array<MapLocationMatch & { _rank: number }> = [...cityRank]
|
|
214
|
+
.filter(([i]) => gazetteer.cities[i] !== undefined)
|
|
215
|
+
.map(([i, rank]) => {
|
|
216
|
+
const c = gazetteer.cities[i]!;
|
|
217
|
+
const [, , iso, pop, name, sub] = c;
|
|
218
|
+
const ambiguous = (gazetteer.byName[fold(name)]?.length ?? 0) > 1;
|
|
219
|
+
const qualifier = sub ?? iso;
|
|
220
|
+
return {
|
|
221
|
+
name,
|
|
222
|
+
token: ambiguous ? `${name} ${qualifier}` : name,
|
|
223
|
+
kind: 'city' as const,
|
|
224
|
+
iso,
|
|
225
|
+
...(sub !== undefined && { sub }),
|
|
226
|
+
pop,
|
|
227
|
+
detail: `${qualifier} · ${groupThousands(pop)}`,
|
|
228
|
+
_rank: rank,
|
|
229
|
+
};
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Airports — match on folded IATA code OR folded airport name.
|
|
233
|
+
const airports = opts?.airports;
|
|
234
|
+
const airportItems: Array<MapLocationMatch & { _rank: number }> = [];
|
|
235
|
+
if (airports?.airportIata) {
|
|
236
|
+
for (const [code, idx] of Object.entries(airports.airportIata)) {
|
|
237
|
+
const a = airports.airports[idx];
|
|
238
|
+
if (!a) continue;
|
|
239
|
+
const codeRank = rankOf(code);
|
|
240
|
+
const nameRank = rankOf(fold(a[4]));
|
|
241
|
+
const rank = Math.min(
|
|
242
|
+
codeRank < 0 ? 99 : codeRank,
|
|
243
|
+
nameRank < 0 ? 99 : nameRank
|
|
244
|
+
);
|
|
245
|
+
if (rank > 2) continue;
|
|
246
|
+
airportItems.push({
|
|
247
|
+
name: a[4],
|
|
248
|
+
token: code.toUpperCase(),
|
|
249
|
+
kind: 'airport',
|
|
250
|
+
iso: a[2],
|
|
251
|
+
pop: 0,
|
|
252
|
+
detail: `Airport · ${a[4]}`,
|
|
253
|
+
_rank: rank,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return [...cityItems, ...airportItems]
|
|
259
|
+
.sort(
|
|
260
|
+
(x, y) =>
|
|
261
|
+
x._rank - y._rank || // best match tier first
|
|
262
|
+
(x.kind === y.kind ? 0 : x.kind === 'city' ? -1 : 1) || // city before airport
|
|
263
|
+
y.pop - x.pop || // populous city first
|
|
264
|
+
(x.token < y.token ? -1 : x.token > y.token ? 1 : 0)
|
|
265
|
+
)
|
|
266
|
+
.slice(0, limit)
|
|
267
|
+
.map(({ _rank, ...rest }) => rest);
|
|
268
|
+
}
|
|
269
|
+
|
|
154
270
|
export interface MapRegionCompletion {
|
|
155
271
|
/** Display name = insert text (the resolver disambiguates cross-layer
|
|
156
272
|
* collisions like Georgia by map scope, §24B.8). */
|
package/src/map/layout.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// POI radii + edge widths, and places labels with per-cluster collision
|
|
6
6
|
// escalation. The SVG emission is renderer.ts (it only draws what we compute).
|
|
7
7
|
// See spec section 24B.3-.7/.11 + the tech-spec Adversarial Review Resolutions AR1-AR9.
|
|
8
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
8
9
|
import {
|
|
9
10
|
geoPath,
|
|
10
11
|
geoNaturalEarth1,
|
|
@@ -1582,7 +1583,7 @@ export function layoutMap(
|
|
|
1582
1583
|
(g) => g.name.toLowerCase() === groupName.toLowerCase()
|
|
1583
1584
|
);
|
|
1584
1585
|
if (!group) return null;
|
|
1585
|
-
const val = tags[group.name
|
|
1586
|
+
const val = tags[tagAttrKey(group.name)] ?? group.defaultValue;
|
|
1586
1587
|
if (!val) return null;
|
|
1587
1588
|
const entry = group.entries.find(
|
|
1588
1589
|
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
@@ -2656,7 +2657,7 @@ export function layoutMap(
|
|
|
2656
2657
|
if (directHex)
|
|
2657
2658
|
return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
|
|
2658
2659
|
for (const group of resolved.tagGroups) {
|
|
2659
|
-
const val = p.tags[group.name
|
|
2660
|
+
const val = p.tags[tagAttrKey(group.name)];
|
|
2660
2661
|
if (!val) continue;
|
|
2661
2662
|
const entry = group.entries.find(
|
|
2662
2663
|
(e) => e.value.toLowerCase() === val.toLowerCase()
|
|
@@ -2678,7 +2679,7 @@ export function layoutMap(
|
|
|
2678
2679
|
// or null → caller falls back to the neutral connector mix.
|
|
2679
2680
|
const lineColor = (tags: Readonly<Record<string, string>>): string | null => {
|
|
2680
2681
|
for (const group of resolved.tagGroups) {
|
|
2681
|
-
const val = tags[group.name
|
|
2682
|
+
const val = tags[tagAttrKey(group.name)];
|
|
2682
2683
|
if (!val) continue;
|
|
2683
2684
|
const entry = group.entries.find(
|
|
2684
2685
|
(e) => e.value.toLowerCase() === val.toLowerCase()
|
package/src/map/parser.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
validateTagGroupNames,
|
|
26
26
|
finalizeAutoTagColors,
|
|
27
27
|
AUTO_TAG_COLOR_SENTINEL,
|
|
28
|
+
tagAttrKey,
|
|
28
29
|
} from '../utils/tag-groups';
|
|
29
30
|
import { parseInArrowLabel } from '../utils/arrows';
|
|
30
31
|
import type {
|
|
@@ -157,7 +158,7 @@ export function parseMap(content: string, palette?: PaletteColors): ParsedMap {
|
|
|
157
158
|
// Declared tag-group names (lowercased) — the keys `partitionMeta` treats as
|
|
158
159
|
// tag values (resolved from aliases). Recomputed as groups are declared.
|
|
159
160
|
const tagGroupNames = (): ReadonlySet<string> =>
|
|
160
|
-
new Set(tagGroups.map((g) => g.name
|
|
161
|
+
new Set(tagGroups.map((g) => tagAttrKey(g.name)));
|
|
161
162
|
|
|
162
163
|
const closeBlocks = (): void => {
|
|
163
164
|
open.tag = null;
|
|
@@ -378,7 +379,7 @@ export function parseMap(content: string, palette?: PaletteColors): ParsedMap {
|
|
|
378
379
|
entries: [],
|
|
379
380
|
lineNumber: line,
|
|
380
381
|
};
|
|
381
|
-
if (m.alias) aliasMap.set(m.alias.toLowerCase(), m.name
|
|
382
|
+
if (m.alias) aliasMap.set(m.alias.toLowerCase(), tagAttrKey(m.name));
|
|
382
383
|
tagGroups.push(group);
|
|
383
384
|
open.tag = group;
|
|
384
385
|
// Inline form: `tag Market as m HQ indigo, Region teal` (R4).
|
package/src/mindmap/parser.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
finalizeAutoTagColors,
|
|
20
20
|
cascadeTagMetadata,
|
|
21
21
|
AUTO_TAG_COLOR_SENTINEL,
|
|
22
|
+
tagAttrKey,
|
|
22
23
|
} from '../utils/tag-groups';
|
|
23
24
|
import {
|
|
24
25
|
measureIndent,
|
|
@@ -167,12 +168,12 @@ export function parseMindmap(
|
|
|
167
168
|
if (tagBlockMatch.alias) {
|
|
168
169
|
aliasMap.set(
|
|
169
170
|
tagBlockMatch.alias.toLowerCase(),
|
|
170
|
-
tagBlockMatch.name
|
|
171
|
+
tagAttrKey(tagBlockMatch.name)
|
|
171
172
|
);
|
|
172
173
|
}
|
|
173
174
|
aliasMap.set(
|
|
174
|
-
tagBlockMatch.name
|
|
175
|
-
tagBlockMatch.name
|
|
175
|
+
tagAttrKey(tagBlockMatch.name),
|
|
176
|
+
tagAttrKey(tagBlockMatch.name)
|
|
176
177
|
);
|
|
177
178
|
result.tagGroups.push(currentTagGroup);
|
|
178
179
|
continue;
|
package/src/mindmap/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Mindmap 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 {
|
|
@@ -389,7 +390,7 @@ export function renderMindmap(
|
|
|
389
390
|
|
|
390
391
|
// Expose active tag group value for legend-entry hover dimming
|
|
391
392
|
if (activeTagGroup) {
|
|
392
|
-
const tagKey = activeTagGroup
|
|
393
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
393
394
|
const metaValue = node.metadata[tagKey];
|
|
394
395
|
if (metaValue) {
|
|
395
396
|
nodeG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
|
package/src/org/layout.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Org Chart Tree Layout Engine (d3-hierarchy)
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import { hierarchy, tree } from 'd3-hierarchy';
|
|
6
7
|
import type { ParsedOrg, OrgNode } from './parser';
|
|
7
8
|
import type { Writable } from '../utils/brand';
|
|
@@ -303,7 +304,7 @@ function computeLegendGroups(
|
|
|
303
304
|
if (group.entries.length === 0) continue;
|
|
304
305
|
|
|
305
306
|
// Filter entries to only values actually used by nodes (if provided)
|
|
306
|
-
const usedValues = usedValuesByGroup?.get(group.name
|
|
307
|
+
const usedValues = usedValuesByGroup?.get(tagAttrKey(group.name));
|
|
307
308
|
const visibleEntries = usedValues
|
|
308
309
|
? group.entries.filter((e) => usedValues.has(e.value.toLowerCase()))
|
|
309
310
|
: group.entries;
|
|
@@ -1185,7 +1186,7 @@ export function layoutOrg(
|
|
|
1185
1186
|
// Collect which tag group values are actually used by nodes
|
|
1186
1187
|
const usedValuesByGroup = new Map<string, Set<string>>();
|
|
1187
1188
|
for (const group of parsed.tagGroups) {
|
|
1188
|
-
const key = group.name
|
|
1189
|
+
const key = tagAttrKey(group.name);
|
|
1189
1190
|
const used = new Set<string>();
|
|
1190
1191
|
const walk = (node: OrgNode) => {
|
|
1191
1192
|
if (!node.isContainer && node.metadata[key]) {
|
package/src/org/parser.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
stripDefaultModifier,
|
|
21
21
|
finalizeAutoTagColors,
|
|
22
22
|
AUTO_TAG_COLOR_SENTINEL,
|
|
23
|
+
tagAttrKey,
|
|
23
24
|
} from '../utils/tag-groups';
|
|
24
25
|
import {
|
|
25
26
|
measureIndent,
|
|
@@ -219,14 +220,14 @@ export function parseOrg(content: string, palette?: PaletteColors): ParsedOrg {
|
|
|
219
220
|
if (tagBlockMatch.alias) {
|
|
220
221
|
metaAliasMap.set(
|
|
221
222
|
normalizeName(tagBlockMatch.alias),
|
|
222
|
-
tagBlockMatch.name
|
|
223
|
+
tagAttrKey(tagBlockMatch.name)
|
|
223
224
|
);
|
|
224
225
|
}
|
|
225
226
|
// §1.4 dispatch: register canonical name so `<name>:` triggers
|
|
226
227
|
// the metadata cut even without an explicit `as <alias>`.
|
|
227
228
|
metaAliasMap.set(
|
|
228
229
|
normalizeName(tagBlockMatch.name),
|
|
229
|
-
tagBlockMatch.name
|
|
230
|
+
tagAttrKey(tagBlockMatch.name)
|
|
230
231
|
);
|
|
231
232
|
result.tagGroups.push(currentTagGroup);
|
|
232
233
|
continue;
|
package/src/org/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Org 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 { FONT_FAMILY } from '../fonts';
|
|
7
8
|
import {
|
|
@@ -269,7 +270,7 @@ export function renderOrg(
|
|
|
269
270
|
// Build display name map from tag groups (lowercase key → original casing)
|
|
270
271
|
const displayNames = new Map<string, string>();
|
|
271
272
|
for (const group of parsed.tagGroups) {
|
|
272
|
-
displayNames.set(group.name
|
|
273
|
+
displayNames.set(tagAttrKey(group.name), group.name);
|
|
273
274
|
}
|
|
274
275
|
|
|
275
276
|
// Root node IDs — focus icon is suppressed on these (already the tree root)
|
|
@@ -288,7 +289,7 @@ export function renderOrg(
|
|
|
288
289
|
// Use tagMetadata (unfiltered) so hover-highlight works even when the
|
|
289
290
|
// active tag group is hidden from the visible card body via the eye toggle.
|
|
290
291
|
if (activeTagGroup) {
|
|
291
|
-
const tagKey = activeTagGroup
|
|
292
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
292
293
|
const metaValue = c.tagMetadata[tagKey];
|
|
293
294
|
if (metaValue) {
|
|
294
295
|
cG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
|
|
@@ -466,7 +467,7 @@ export function renderOrg(
|
|
|
466
467
|
// Use tagMetadata (unfiltered) so hover-highlight works even when the
|
|
467
468
|
// active tag group is hidden from the visible card body via the eye toggle.
|
|
468
469
|
if (activeTagGroup) {
|
|
469
|
-
const tagKey = activeTagGroup
|
|
470
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
470
471
|
const metaValue = node.tagMetadata[tagKey];
|
|
471
472
|
if (metaValue) {
|
|
472
473
|
nodeG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
|
|
@@ -781,7 +782,7 @@ export function renderOrg(
|
|
|
781
782
|
const computedLayout = legendHandle.getLayout();
|
|
782
783
|
if (computedLayout.activeCapsule?.addonX != null) {
|
|
783
784
|
const capsule = computedLayout.activeCapsule;
|
|
784
|
-
const groupKey = capsule.groupName
|
|
785
|
+
const groupKey = tagAttrKey(capsule.groupName);
|
|
785
786
|
const isHidden = hiddenAttributes?.has(groupKey) ?? false;
|
|
786
787
|
|
|
787
788
|
// Find the rendered active group <g> and append eye icon
|
package/src/pert/parser.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
finalizeAutoTagColors,
|
|
37
37
|
AUTO_TAG_COLOR_SENTINEL,
|
|
38
38
|
type TagGroup,
|
|
39
|
+
tagAttrKey,
|
|
39
40
|
} from '../utils/tag-groups';
|
|
40
41
|
import type { Writable } from '../utils/brand';
|
|
41
42
|
import type { Duration, DurationUnit } from '../gantt/types';
|
|
@@ -695,12 +696,12 @@ export function parsePert(
|
|
|
695
696
|
if (tagBlockMatch.alias) {
|
|
696
697
|
metaAliasMap.set(
|
|
697
698
|
tagBlockMatch.alias.toLowerCase(),
|
|
698
|
-
tagBlockMatch.name
|
|
699
|
+
tagAttrKey(tagBlockMatch.name)
|
|
699
700
|
);
|
|
700
701
|
}
|
|
701
702
|
metaAliasMap.set(
|
|
702
703
|
normalizeName(tagBlockMatch.name),
|
|
703
|
-
tagBlockMatch.name
|
|
704
|
+
tagAttrKey(tagBlockMatch.name)
|
|
704
705
|
);
|
|
705
706
|
tagGroups.push(currentTagGroup);
|
|
706
707
|
// Inline values (e.g. `tag Priority as p Low green, High red`).
|
package/src/sequence/renderer.ts
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
// Sequence 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 type { PaletteColors } from '../palettes';
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
contrastText,
|
|
10
|
+
mix,
|
|
11
|
+
shapeFill,
|
|
12
|
+
themeBaseBg,
|
|
13
|
+
} from '../palettes/color-utils';
|
|
8
14
|
import {
|
|
9
15
|
parseInlineMarkdown,
|
|
10
16
|
truncateBareUrl,
|
|
@@ -1062,7 +1068,7 @@ export function renderSequenceDiagram(
|
|
|
1062
1068
|
}
|
|
1063
1069
|
const getTagColor = (value: string | undefined): string | undefined =>
|
|
1064
1070
|
value ? tagValueToColor.get(value.toLowerCase()) : undefined;
|
|
1065
|
-
const tagKey = activeTagGroup
|
|
1071
|
+
const tagKey = activeTagGroup ? tagAttrKey(activeTagGroup) : undefined;
|
|
1066
1072
|
|
|
1067
1073
|
// Build hidden message set for collapse support
|
|
1068
1074
|
const hiddenMsgIndices = new Set<number>();
|
|
@@ -2008,11 +2014,7 @@ export function renderSequenceDiagram(
|
|
|
2008
2014
|
const groupTagValue = tagKey && group.metadata?.[tagKey];
|
|
2009
2015
|
const groupTagColor = getTagColor(groupTagValue || undefined);
|
|
2010
2016
|
const fillColor = groupTagColor
|
|
2011
|
-
? mix(
|
|
2012
|
-
groupTagColor,
|
|
2013
|
-
themeBaseBg(palette, isDark),
|
|
2014
|
-
isDark ? 15 : 20
|
|
2015
|
-
)
|
|
2017
|
+
? mix(groupTagColor, themeBaseBg(palette, isDark), isDark ? 15 : 20)
|
|
2016
2018
|
: isDark
|
|
2017
2019
|
? palette.surface
|
|
2018
2020
|
: palette.bg;
|
|
@@ -2137,11 +2139,7 @@ export function renderSequenceDiagram(
|
|
|
2137
2139
|
|
|
2138
2140
|
// Overlay a taller rect to replace the standard participant box
|
|
2139
2141
|
const pFill = effectiveTagColor
|
|
2140
|
-
? mix(
|
|
2141
|
-
effectiveTagColor,
|
|
2142
|
-
themeBaseBg(palette, isDark),
|
|
2143
|
-
isDark ? 30 : 40
|
|
2144
|
-
)
|
|
2142
|
+
? mix(effectiveTagColor, themeBaseBg(palette, isDark), isDark ? 30 : 40)
|
|
2145
2143
|
: isDark
|
|
2146
2144
|
? mix(palette.overlay, palette.surface, 50)
|
|
2147
2145
|
: mix(palette.bg, palette.surface, 50);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Resolves effective tag values for participants and messages
|
|
6
6
|
// using the priority chain: explicit > group > receiver-inherit > default > neutral
|
|
7
7
|
|
|
8
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
8
9
|
import type {
|
|
9
10
|
ParsedSequenceDgmo,
|
|
10
11
|
SequenceParticipant,
|
|
@@ -116,7 +117,7 @@ export function resolveSequenceTags(
|
|
|
116
117
|
return result;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
const groupKey = group.name
|
|
120
|
+
const groupKey = tagAttrKey(group.name);
|
|
120
121
|
|
|
121
122
|
// Clone participant metadata (don't mutate parsed data)
|
|
122
123
|
const participantMeta = new Map<string, Record<string, string>>();
|
package/src/sitemap/layout.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Sitemap Diagram Layout Engine (Dagre flat graph)
|
|
3
3
|
// ============================================================
|
|
4
4
|
|
|
5
|
+
import { tagAttrKey } from '../utils/tag-groups';
|
|
5
6
|
import dagre from '@dagrejs/dagre';
|
|
6
7
|
import type { Writable } from '../utils/brand';
|
|
7
8
|
import type { ParsedSitemap, SitemapNode } from './types';
|
|
@@ -232,7 +233,7 @@ function computeLegendGroups(
|
|
|
232
233
|
for (const group of tagGroups) {
|
|
233
234
|
if (group.entries.length === 0) continue;
|
|
234
235
|
|
|
235
|
-
const usedValues = usedValuesByGroup?.get(group.name
|
|
236
|
+
const usedValues = usedValuesByGroup?.get(tagAttrKey(group.name));
|
|
236
237
|
const visibleEntries = usedValues
|
|
237
238
|
? group.entries.filter((e) => usedValues.has(e.value.toLowerCase()))
|
|
238
239
|
: group.entries;
|
|
@@ -866,7 +867,7 @@ export function layoutSitemap(
|
|
|
866
867
|
// Collect used tag values
|
|
867
868
|
const usedValuesByGroup = new Map<string, Set<string>>();
|
|
868
869
|
for (const group of parsed.tagGroups) {
|
|
869
|
-
const key = group.name
|
|
870
|
+
const key = tagAttrKey(group.name);
|
|
870
871
|
const used = new Set<string>();
|
|
871
872
|
const walk = (node: SitemapNode) => {
|
|
872
873
|
if (!node.isContainer && node.metadata[key]) {
|
package/src/sitemap/parser.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
stripDefaultModifier,
|
|
30
30
|
finalizeAutoTagColors,
|
|
31
31
|
AUTO_TAG_COLOR_SENTINEL,
|
|
32
|
+
tagAttrKey,
|
|
32
33
|
} from '../utils/tag-groups';
|
|
33
34
|
import {
|
|
34
35
|
measureIndent,
|
|
@@ -273,12 +274,12 @@ export function parseSitemap(
|
|
|
273
274
|
if (tagBlockMatch.alias) {
|
|
274
275
|
metaAliasMap.set(
|
|
275
276
|
normalizeName(tagBlockMatch.alias),
|
|
276
|
-
tagBlockMatch.name
|
|
277
|
+
tagAttrKey(tagBlockMatch.name)
|
|
277
278
|
);
|
|
278
279
|
}
|
|
279
280
|
metaAliasMap.set(
|
|
280
281
|
normalizeName(tagBlockMatch.name),
|
|
281
|
-
tagBlockMatch.name
|
|
282
|
+
tagAttrKey(tagBlockMatch.name)
|
|
282
283
|
);
|
|
283
284
|
result.tagGroups.push(newTagGroup);
|
|
284
285
|
continue;
|
|
@@ -501,6 +502,16 @@ export function parseSitemap(
|
|
|
501
502
|
}
|
|
502
503
|
}
|
|
503
504
|
|
|
505
|
+
// §12: reject sub-pages nested under a page inside a container.
|
|
506
|
+
const enclosingContainer = nestedPageInsideContainer(indent, indentStack);
|
|
507
|
+
if (enclosingContainer) {
|
|
508
|
+
pushError(
|
|
509
|
+
lineNumber,
|
|
510
|
+
`Pages inside container '[${enclosingContainer}]' cannot have indented sub-pages. Containers hold a flat list of pages.`
|
|
511
|
+
);
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
504
515
|
// Node label — possibly with pipe-delimited metadata
|
|
505
516
|
const node = parseNodeLabel(
|
|
506
517
|
trimmed,
|
|
@@ -702,6 +713,29 @@ function attachNode(
|
|
|
702
713
|
indentStack.push({ node, indent });
|
|
703
714
|
}
|
|
704
715
|
|
|
716
|
+
/**
|
|
717
|
+
* §12: containers hold a flat list of pages — a page inside a `[Container]`
|
|
718
|
+
* may not have indented sub-pages. Returns the enclosing container's label
|
|
719
|
+
* when attaching a page at `indent` would nest it under a page that itself
|
|
720
|
+
* lives inside a container; otherwise null. Non-destructive (mirrors
|
|
721
|
+
* `attachNode`'s pop logic without mutating the stack).
|
|
722
|
+
*/
|
|
723
|
+
function nestedPageInsideContainer(
|
|
724
|
+
indent: number,
|
|
725
|
+
indentStack: { node: Writable<SitemapNode>; indent: number }[]
|
|
726
|
+
): string | null {
|
|
727
|
+
let idx = indentStack.length - 1;
|
|
728
|
+
while (idx >= 0 && indentStack[idx]!.indent >= indent) idx--;
|
|
729
|
+
if (idx < 0) return null;
|
|
730
|
+
// Immediate parent. A container parent → flat member, allowed.
|
|
731
|
+
if (indentStack[idx]!.node.isContainer) return null;
|
|
732
|
+
// Parent is a page; flag only when an ancestor is a container.
|
|
733
|
+
for (let k = idx - 1; k >= 0; k--) {
|
|
734
|
+
if (indentStack[k]!.node.isContainer) return indentStack[k]!.node.label;
|
|
735
|
+
}
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
|
|
705
739
|
function findParentNode(
|
|
706
740
|
indent: number,
|
|
707
741
|
indentStack: { node: Writable<SitemapNode>; indent: number }[]
|
package/src/sitemap/renderer.ts
CHANGED
|
@@ -2,11 +2,17 @@
|
|
|
2
2
|
// Sitemap 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';
|
|
8
9
|
import type { PaletteColors } from '../palettes';
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
contrastText,
|
|
12
|
+
mix,
|
|
13
|
+
shapeFill,
|
|
14
|
+
themeBaseBg,
|
|
15
|
+
} from '../palettes/color-utils';
|
|
10
16
|
import type { ParsedSitemap } from './types';
|
|
11
17
|
import type { SitemapLayoutResult, SitemapLegendGroup } from './layout';
|
|
12
18
|
import { renderInlineText } from '../utils/inline-markdown';
|
|
@@ -284,10 +290,10 @@ export function renderSitemap(
|
|
|
284
290
|
// tagColors: "groupkey:valueLower" → hex color
|
|
285
291
|
const tagColors = new Map<string, string>();
|
|
286
292
|
for (const group of parsed.tagGroups) {
|
|
287
|
-
displayNames.set(group.name
|
|
293
|
+
displayNames.set(tagAttrKey(group.name), group.name);
|
|
288
294
|
for (const entry of group.entries) {
|
|
289
295
|
tagColors.set(
|
|
290
|
-
`${group.name
|
|
296
|
+
`${tagAttrKey(group.name)}:${entry.value.toLowerCase()}`,
|
|
291
297
|
entry.color
|
|
292
298
|
);
|
|
293
299
|
}
|
|
@@ -317,7 +323,7 @@ export function renderSitemap(
|
|
|
317
323
|
|
|
318
324
|
// Tag metadata for legend hover dimming
|
|
319
325
|
if (activeTagGroup) {
|
|
320
|
-
const tagKey = activeTagGroup
|
|
326
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
321
327
|
const tagVal = c.tagMetadata[tagKey];
|
|
322
328
|
if (tagVal) cG.attr(`data-tag-${tagKey}`, tagVal.toLowerCase());
|
|
323
329
|
}
|
|
@@ -486,7 +492,7 @@ export function renderSitemap(
|
|
|
486
492
|
|
|
487
493
|
// Tag metadata for legend hover dimming
|
|
488
494
|
if (activeTagGroup) {
|
|
489
|
-
const tagKey = activeTagGroup
|
|
495
|
+
const tagKey = tagAttrKey(activeTagGroup);
|
|
490
496
|
const tagVal = node.tagMetadata[tagKey];
|
|
491
497
|
if (tagVal) nodeG.attr(`data-tag-${tagKey}`, tagVal.toLowerCase());
|
|
492
498
|
}
|
|
@@ -721,7 +727,7 @@ function renderLegend(
|
|
|
721
727
|
const computedLayout = legendHandle.getLayout();
|
|
722
728
|
if (computedLayout.activeCapsule?.addonX != null) {
|
|
723
729
|
const capsule = computedLayout.activeCapsule;
|
|
724
|
-
const groupKey = capsule.groupName
|
|
730
|
+
const groupKey = tagAttrKey(capsule.groupName);
|
|
725
731
|
const isHidden = hiddenAttributes?.has(groupKey) ?? false;
|
|
726
732
|
|
|
727
733
|
const activeGroupEl = parent.select(`[data-legend-group="${groupKey}"]`);
|
package/src/timeline/renderer.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Timeline renderer — Story 109.2 (arch-review). Extracted from d3.ts.
|
|
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';
|
|
@@ -820,7 +821,7 @@ function setupTimeline(
|
|
|
820
821
|
let tagLanes: Lane[] | null = null;
|
|
821
822
|
|
|
822
823
|
if (resolvedSwimlaneTG) {
|
|
823
|
-
const tagKey = resolvedSwimlaneTG
|
|
824
|
+
const tagKey = tagAttrKey(resolvedSwimlaneTG);
|
|
824
825
|
const tagGroup = parsed.timelineTagGroups.find(
|
|
825
826
|
(g) => g.name.toLowerCase() === tagKey
|
|
826
827
|
);
|
|
@@ -1322,7 +1323,7 @@ function renderTimelineTagLegendOverlay(
|
|
|
1322
1323
|
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
1323
1324
|
},
|
|
1324
1325
|
onEntryHover: (groupName, entryValue) => {
|
|
1325
|
-
const tagKey = groupName
|
|
1326
|
+
const tagKey = tagAttrKey(groupName);
|
|
1326
1327
|
if (entryValue) {
|
|
1327
1328
|
const tagVal = entryValue.toLowerCase();
|
|
1328
1329
|
fadeToTagValue(mainG, tagKey, tagVal);
|
|
@@ -1349,7 +1350,7 @@ function renderTimelineTagLegendOverlay(
|
|
|
1349
1350
|
}
|
|
1350
1351
|
},
|
|
1351
1352
|
onGroupRendered: (groupName, groupEl, isActive) => {
|
|
1352
|
-
const groupKey = groupName
|
|
1353
|
+
const groupKey = tagAttrKey(groupName);
|
|
1353
1354
|
groupEl.attr('data-tag-group', groupKey);
|
|
1354
1355
|
if (isActive && !viewMode) {
|
|
1355
1356
|
const isSwimActive =
|
package/src/utils/legend-d3.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Thin adapter: layout engine → D3 DOM
|
|
4
4
|
// ============================================================
|
|
5
5
|
|
|
6
|
+
import { tagAttrKey } from './tag-groups';
|
|
6
7
|
import {
|
|
7
8
|
LEGEND_HEIGHT,
|
|
8
9
|
LEGEND_PILL_FONT_SIZE,
|
|
@@ -164,7 +165,7 @@ function renderCapsule(
|
|
|
164
165
|
const g = parent
|
|
165
166
|
.append('g')
|
|
166
167
|
.attr('transform', `translate(${capsule.x},${capsule.y})`)
|
|
167
|
-
.attr('data-legend-group', capsule.groupName
|
|
168
|
+
.attr('data-legend-group', tagAttrKey(capsule.groupName))
|
|
168
169
|
.style('cursor', 'pointer');
|
|
169
170
|
|
|
170
171
|
// Outer capsule background
|
|
@@ -211,7 +212,7 @@ function renderCapsule(
|
|
|
211
212
|
// Continuous ramp (choropleth group): min | gradient | max, in place of dots.
|
|
212
213
|
if (capsule.gradient) {
|
|
213
214
|
const gr = capsule.gradient;
|
|
214
|
-
const gradId = `dgmo-legend-ramp-${capsule.groupName
|
|
215
|
+
const gradId = `dgmo-legend-ramp-${tagAttrKey(capsule.groupName).replace(/[^a-z0-9]+/g, '-')}`;
|
|
215
216
|
const def = g.append('defs').append('linearGradient').attr('id', gradId);
|
|
216
217
|
// Sample the SAME ramp the fills use (direct = 2 endpoints; diverging =
|
|
217
218
|
// stops through the neutral midpoint), so the legend matches the basemap.
|
|
@@ -318,7 +319,7 @@ function renderPill(
|
|
|
318
319
|
const g = parent
|
|
319
320
|
.append('g')
|
|
320
321
|
.attr('transform', `translate(${pill.x},${pill.y})`)
|
|
321
|
-
.attr('data-legend-group', pill.groupName
|
|
322
|
+
.attr('data-legend-group', tagAttrKey(pill.groupName))
|
|
322
323
|
.style('cursor', 'pointer');
|
|
323
324
|
|
|
324
325
|
g.append('rect')
|
package/src/utils/legend-svg.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// (not dominant-baseline) so resvg renders it correctly in PNG export.
|
|
13
13
|
// ============================================================
|
|
14
14
|
|
|
15
|
+
import { tagAttrKey } from './tag-groups';
|
|
15
16
|
import type {
|
|
16
17
|
LegendConfig,
|
|
17
18
|
LegendState,
|
|
@@ -99,7 +100,7 @@ function emitCapsule(
|
|
|
99
100
|
);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
return `<g transform="translate(${capsule.x},${capsule.y})" data-legend-group="${esc(capsule.groupName
|
|
103
|
+
return `<g transform="translate(${capsule.x},${capsule.y})" data-legend-group="${esc(tagAttrKey(capsule.groupName))}" style="cursor:pointer">${inner.join('')}</g>`;
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/** Collapsed group pill (no active group) → SVG string. */
|
|
@@ -109,7 +110,7 @@ function emitPill(
|
|
|
109
110
|
groupBg: string
|
|
110
111
|
): string {
|
|
111
112
|
return (
|
|
112
|
-
`<g transform="translate(${pill.x},${pill.y})" data-legend-group="${esc(pill.groupName
|
|
113
|
+
`<g transform="translate(${pill.x},${pill.y})" data-legend-group="${esc(tagAttrKey(pill.groupName))}" style="cursor:pointer">` +
|
|
113
114
|
`<rect width="${pill.width}" height="${pill.height}" rx="${pill.height / 2}" fill="${esc(groupBg)}"/>` +
|
|
114
115
|
`<text x="${pill.width / 2}" y="${pill.height / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2}" font-size="${LEGEND_PILL_FONT_SIZE}" font-weight="500" fill="${esc(palette.textMuted)}" text-anchor="middle" font-family="${esc(FONT_FAMILY)}">${esc(pill.groupName)}</text>` +
|
|
115
116
|
`</g>`
|