@diagrammo/dgmo 0.4.1 → 0.4.3
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/skills/dgmo-chart/SKILL.md +28 -0
- package/.claude/skills/dgmo-generate/SKILL.md +1 -0
- package/.claude/skills/dgmo-sequence/SKILL.md +24 -1
- package/.cursorrules +27 -2
- package/.github/copilot-instructions.md +36 -3
- package/.windsurfrules +27 -2
- package/README.md +12 -3
- package/dist/cli.cjs +611 -153
- package/dist/index.cjs +8371 -3200
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +502 -58
- package/dist/index.d.ts +502 -58
- package/dist/index.js +8594 -3444
- package/dist/index.js.map +1 -1
- package/docs/ai-integration.md +1 -1
- package/docs/language-reference.md +336 -17
- package/docs/migration-sequence-color-to-tags.md +98 -0
- package/package.json +1 -1
- package/src/c4/renderer.ts +1 -20
- package/src/class/renderer.ts +1 -11
- package/src/cli.ts +40 -0
- package/src/d3.ts +92 -2
- package/src/dgmo-router.ts +11 -0
- package/src/echarts.ts +74 -8
- package/src/er/parser.ts +29 -3
- package/src/er/renderer.ts +1 -15
- package/src/graph/flowchart-parser.ts +7 -30
- package/src/graph/flowchart-renderer.ts +62 -69
- package/src/graph/layout.ts +5 -0
- package/src/graph/state-parser.ts +388 -0
- package/src/graph/state-renderer.ts +496 -0
- package/src/graph/types.ts +4 -2
- package/src/index.ts +42 -1
- package/src/infra/compute.ts +1113 -0
- package/src/infra/layout.ts +575 -0
- package/src/infra/parser.ts +559 -0
- package/src/infra/renderer.ts +1509 -0
- package/src/infra/roles.ts +60 -0
- package/src/infra/serialize.ts +67 -0
- package/src/infra/types.ts +221 -0
- package/src/infra/validation.ts +192 -0
- package/src/initiative-status/layout.ts +56 -61
- package/src/initiative-status/renderer.ts +13 -13
- package/src/kanban/renderer.ts +1 -24
- package/src/org/layout.ts +28 -37
- package/src/org/parser.ts +16 -1
- package/src/org/renderer.ts +159 -121
- package/src/org/resolver.ts +90 -23
- package/src/palettes/color-utils.ts +30 -0
- package/src/render.ts +2 -0
- package/src/sequence/parser.ts +202 -42
- package/src/sequence/renderer.ts +576 -113
- package/src/sequence/tag-resolution.ts +163 -0
- package/src/sitemap/collapse.ts +187 -0
- package/src/sitemap/layout.ts +738 -0
- package/src/sitemap/parser.ts +489 -0
- package/src/sitemap/renderer.ts +774 -0
- package/src/sitemap/types.ts +42 -0
- package/src/utils/tag-groups.ts +119 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Sitemap Diagram Parser
|
|
3
|
+
// ============================================================
|
|
4
|
+
|
|
5
|
+
import type { PaletteColors } from '../palettes';
|
|
6
|
+
import { resolveColor } from '../colors';
|
|
7
|
+
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
8
|
+
import type { TagGroup, TagEntry } from '../utils/tag-groups';
|
|
9
|
+
import { isTagBlockHeading, matchTagBlockHeading, validateTagValues } from '../utils/tag-groups';
|
|
10
|
+
import {
|
|
11
|
+
measureIndent,
|
|
12
|
+
extractColor,
|
|
13
|
+
parsePipeMetadata,
|
|
14
|
+
CHART_TYPE_RE,
|
|
15
|
+
TITLE_RE,
|
|
16
|
+
OPTION_RE,
|
|
17
|
+
} from '../utils/parsing';
|
|
18
|
+
import type {
|
|
19
|
+
SitemapNode,
|
|
20
|
+
SitemapEdge,
|
|
21
|
+
SitemapDirection,
|
|
22
|
+
ParsedSitemap,
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
// ============================================================
|
|
26
|
+
// Regexes
|
|
27
|
+
// ============================================================
|
|
28
|
+
|
|
29
|
+
const CONTAINER_RE = /^\[([^\]]+)\]$/;
|
|
30
|
+
const METADATA_RE = /^([^:]+):\s*(.+)$/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Arrow line: `-label->`, `-(color)->`, `-label(color)->`, `->` followed by target label.
|
|
34
|
+
* Captures: [1] label, [2] color, [3] target
|
|
35
|
+
*/
|
|
36
|
+
const ARROW_RE = /^-([^(>][^(>]*?)?\s*(?:\(([^)]+)\))?\s*->\s*(.+)$/;
|
|
37
|
+
const BARE_ARROW_RE = /^->\s*(.+)$/;
|
|
38
|
+
|
|
39
|
+
// ============================================================
|
|
40
|
+
// Helpers
|
|
41
|
+
// ============================================================
|
|
42
|
+
|
|
43
|
+
function parseArrowLine(
|
|
44
|
+
trimmed: string,
|
|
45
|
+
palette?: PaletteColors,
|
|
46
|
+
): { label?: string; color?: string; target: string } | null {
|
|
47
|
+
// Bare arrow: -> Target
|
|
48
|
+
const bareMatch = trimmed.match(BARE_ARROW_RE);
|
|
49
|
+
if (bareMatch) {
|
|
50
|
+
return { target: bareMatch[1].trim() };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Labeled/colored arrow: -label(color)-> Target
|
|
54
|
+
const arrowMatch = trimmed.match(ARROW_RE);
|
|
55
|
+
if (arrowMatch) {
|
|
56
|
+
const label = arrowMatch[1]?.trim() || undefined;
|
|
57
|
+
const color = arrowMatch[2]
|
|
58
|
+
? resolveColor(arrowMatch[2].trim(), palette)
|
|
59
|
+
: undefined;
|
|
60
|
+
const target = arrowMatch[3].trim();
|
|
61
|
+
return { label, color, target };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// Inference
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns true if content looks like a sitemap diagram.
|
|
73
|
+
* Heuristic: has `->` arrows AND `[Group]` containers but does NOT have
|
|
74
|
+
* flowchart shape delimiters ((...), <...>, /.../) adjacent to arrows.
|
|
75
|
+
*/
|
|
76
|
+
export function looksLikeSitemap(content: string): boolean {
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
let hasArrow = false;
|
|
79
|
+
let hasContainer = false;
|
|
80
|
+
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed || trimmed.startsWith('//')) continue;
|
|
84
|
+
|
|
85
|
+
// Skip header lines
|
|
86
|
+
if (CHART_TYPE_RE.test(trimmed) || TITLE_RE.test(trimmed)) continue;
|
|
87
|
+
if (isTagBlockHeading(trimmed)) continue;
|
|
88
|
+
|
|
89
|
+
if (/^-.*->\s*.+/.test(trimmed) || /^->\s*.+/.test(trimmed)) {
|
|
90
|
+
hasArrow = true;
|
|
91
|
+
}
|
|
92
|
+
if (CONTAINER_RE.test(trimmed)) {
|
|
93
|
+
hasContainer = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!hasArrow || !hasContainer) return false;
|
|
98
|
+
|
|
99
|
+
// Exclude flowchart: flowchart arrows connect shaped nodes like (X) -> [Y]
|
|
100
|
+
// Sitemap arrows are indented under a parent node, target is plain text
|
|
101
|
+
const hasFlowchartShapes =
|
|
102
|
+
/[\])][ \t]*-.*->/.test(content) || /->[ \t]*[\[(<\/]/.test(content);
|
|
103
|
+
|
|
104
|
+
return !hasFlowchartShapes;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================
|
|
108
|
+
// Parser
|
|
109
|
+
// ============================================================
|
|
110
|
+
|
|
111
|
+
export function parseSitemap(
|
|
112
|
+
content: string,
|
|
113
|
+
palette?: PaletteColors,
|
|
114
|
+
): ParsedSitemap {
|
|
115
|
+
const result: ParsedSitemap = {
|
|
116
|
+
title: null,
|
|
117
|
+
titleLineNumber: null,
|
|
118
|
+
direction: 'TB',
|
|
119
|
+
roots: [],
|
|
120
|
+
edges: [],
|
|
121
|
+
tagGroups: [],
|
|
122
|
+
options: {},
|
|
123
|
+
diagnostics: [],
|
|
124
|
+
error: null,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const fail = (line: number, message: string): ParsedSitemap => {
|
|
128
|
+
const diag = makeDgmoError(line, message);
|
|
129
|
+
result.diagnostics.push(diag);
|
|
130
|
+
result.error = formatDgmoError(diag);
|
|
131
|
+
return result;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const pushError = (line: number, message: string): void => {
|
|
135
|
+
const diag = makeDgmoError(line, message);
|
|
136
|
+
result.diagnostics.push(diag);
|
|
137
|
+
if (!result.error) result.error = formatDgmoError(diag);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const pushWarning = (line: number, message: string): void => {
|
|
141
|
+
result.diagnostics.push(makeDgmoError(line, message, 'warning'));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (!content || !content.trim()) {
|
|
145
|
+
return fail(0, 'No content provided');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const lines = content.split('\n');
|
|
149
|
+
let contentStarted = false;
|
|
150
|
+
let nodeCounter = 0;
|
|
151
|
+
let containerCounter = 0;
|
|
152
|
+
|
|
153
|
+
// Tag group parsing state
|
|
154
|
+
let currentTagGroup: TagGroup | null = null;
|
|
155
|
+
|
|
156
|
+
// Alias map: alias (lowercased) -> group name (lowercased)
|
|
157
|
+
const aliasMap = new Map<string, string>();
|
|
158
|
+
|
|
159
|
+
// Indent stack for hierarchy tracking
|
|
160
|
+
const indentStack: { node: SitemapNode; indent: number }[] = [];
|
|
161
|
+
|
|
162
|
+
// Map label (lowercased) -> node for arrow target resolution
|
|
163
|
+
const labelToNode = new Map<string, SitemapNode>();
|
|
164
|
+
|
|
165
|
+
// Deferred arrows: { sourceNode, arrow info, lineNumber }
|
|
166
|
+
const deferredArrows: {
|
|
167
|
+
sourceNode: SitemapNode;
|
|
168
|
+
targetLabel: string;
|
|
169
|
+
label?: string;
|
|
170
|
+
color?: string;
|
|
171
|
+
lineNumber: number;
|
|
172
|
+
}[] = [];
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < lines.length; i++) {
|
|
175
|
+
const line = lines[i];
|
|
176
|
+
const lineNumber = i + 1;
|
|
177
|
+
const trimmed = line.trim();
|
|
178
|
+
|
|
179
|
+
// Skip empty lines
|
|
180
|
+
if (!trimmed) {
|
|
181
|
+
if (currentTagGroup) {
|
|
182
|
+
currentTagGroup = null;
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Skip comments
|
|
188
|
+
if (trimmed.startsWith('//')) continue;
|
|
189
|
+
|
|
190
|
+
// --- Header phase ---
|
|
191
|
+
|
|
192
|
+
// chart: type
|
|
193
|
+
if (!contentStarted) {
|
|
194
|
+
const chartMatch = trimmed.match(CHART_TYPE_RE);
|
|
195
|
+
if (chartMatch) {
|
|
196
|
+
const chartType = chartMatch[1].trim().toLowerCase();
|
|
197
|
+
if (chartType !== 'sitemap') {
|
|
198
|
+
const allTypes = [
|
|
199
|
+
'sitemap', 'org', 'class', 'flowchart', 'sequence', 'er',
|
|
200
|
+
'bar', 'line', 'pie', 'scatter', 'sankey', 'venn', 'timeline',
|
|
201
|
+
'arc', 'slope', 'kanban', 'c4', 'initiative-status', 'state',
|
|
202
|
+
];
|
|
203
|
+
let msg = `Expected chart type "sitemap", got "${chartType}"`;
|
|
204
|
+
const hint = suggest(chartType, allTypes);
|
|
205
|
+
if (hint) msg += `. ${hint}`;
|
|
206
|
+
return fail(lineNumber, msg);
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// title: value
|
|
213
|
+
if (!contentStarted) {
|
|
214
|
+
const titleMatch = trimmed.match(TITLE_RE);
|
|
215
|
+
if (titleMatch) {
|
|
216
|
+
result.title = titleMatch[1].trim();
|
|
217
|
+
result.titleLineNumber = lineNumber;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Tag group heading
|
|
223
|
+
const tagBlockMatch = matchTagBlockHeading(trimmed);
|
|
224
|
+
if (tagBlockMatch) {
|
|
225
|
+
if (contentStarted) {
|
|
226
|
+
pushError(lineNumber, 'Tag groups must appear before sitemap content');
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (tagBlockMatch.deprecated) {
|
|
230
|
+
pushWarning(
|
|
231
|
+
lineNumber,
|
|
232
|
+
`'## ${tagBlockMatch.name}' is deprecated for tag groups — use 'tag: ${tagBlockMatch.name}' instead`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
currentTagGroup = {
|
|
236
|
+
name: tagBlockMatch.name,
|
|
237
|
+
alias: tagBlockMatch.alias,
|
|
238
|
+
entries: [],
|
|
239
|
+
lineNumber,
|
|
240
|
+
};
|
|
241
|
+
if (tagBlockMatch.alias) {
|
|
242
|
+
aliasMap.set(tagBlockMatch.alias.toLowerCase(), tagBlockMatch.name.toLowerCase());
|
|
243
|
+
}
|
|
244
|
+
result.tagGroups.push(currentTagGroup);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Generic header options (before content/tag groups)
|
|
249
|
+
if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
|
|
250
|
+
const optMatch = trimmed.match(OPTION_RE);
|
|
251
|
+
if (optMatch) {
|
|
252
|
+
const key = optMatch[1].trim().toLowerCase();
|
|
253
|
+
if (key === 'direction') {
|
|
254
|
+
const dir = optMatch[2].trim().toUpperCase();
|
|
255
|
+
if (dir === 'TB' || dir === 'LR') {
|
|
256
|
+
result.direction = dir as SitemapDirection;
|
|
257
|
+
}
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (key !== 'chart' && key !== 'title') {
|
|
261
|
+
result.options[key] = optMatch[2].trim();
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Tag group entries (indented Value(color) under tag: heading)
|
|
268
|
+
if (currentTagGroup && !contentStarted) {
|
|
269
|
+
const indent = measureIndent(line);
|
|
270
|
+
if (indent > 0) {
|
|
271
|
+
const isDefault = /\bdefault\s*$/.test(trimmed);
|
|
272
|
+
const entryText = isDefault
|
|
273
|
+
? trimmed.replace(/\s+default\s*$/, '').trim()
|
|
274
|
+
: trimmed;
|
|
275
|
+
const { label, color } = extractColor(entryText, palette);
|
|
276
|
+
if (!color) {
|
|
277
|
+
pushError(
|
|
278
|
+
lineNumber,
|
|
279
|
+
`Expected 'Value(color)' in tag group '${currentTagGroup.name}'`,
|
|
280
|
+
);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (isDefault) {
|
|
284
|
+
currentTagGroup.defaultValue = label;
|
|
285
|
+
}
|
|
286
|
+
currentTagGroup.entries.push({
|
|
287
|
+
value: label,
|
|
288
|
+
color,
|
|
289
|
+
lineNumber,
|
|
290
|
+
});
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
// Non-indented line after tag group — fall through to content
|
|
294
|
+
currentTagGroup = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- Content phase ---
|
|
298
|
+
contentStarted = true;
|
|
299
|
+
currentTagGroup = null;
|
|
300
|
+
|
|
301
|
+
const indent = measureIndent(line);
|
|
302
|
+
|
|
303
|
+
// Check for arrow syntax (must check before metadata — arrows contain `:` in labels
|
|
304
|
+
// but also start with `-`)
|
|
305
|
+
const arrowInfo = parseArrowLine(trimmed, palette);
|
|
306
|
+
if (arrowInfo) {
|
|
307
|
+
// Find the source node: the most recent node on the indent stack
|
|
308
|
+
// at a shallower indent (same pattern as metadata attachment)
|
|
309
|
+
const source = findParentNode(indent, indentStack);
|
|
310
|
+
if (!source) {
|
|
311
|
+
pushError(lineNumber, 'Arrow has no source node');
|
|
312
|
+
} else {
|
|
313
|
+
deferredArrows.push({
|
|
314
|
+
sourceNode: source,
|
|
315
|
+
targetLabel: arrowInfo.target,
|
|
316
|
+
label: arrowInfo.label,
|
|
317
|
+
color: arrowInfo.color,
|
|
318
|
+
lineNumber,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check for container syntax: [Group Name]
|
|
325
|
+
const containerMatch = trimmed.match(CONTAINER_RE);
|
|
326
|
+
|
|
327
|
+
// Check for metadata syntax: key: value
|
|
328
|
+
const metadataMatch =
|
|
329
|
+
trimmed.includes('|') ? null : trimmed.match(METADATA_RE);
|
|
330
|
+
|
|
331
|
+
if (containerMatch) {
|
|
332
|
+
const rawLabel = containerMatch[1].trim();
|
|
333
|
+
const { label, color } = extractColor(rawLabel, palette);
|
|
334
|
+
|
|
335
|
+
containerCounter++;
|
|
336
|
+
const node: SitemapNode = {
|
|
337
|
+
id: `container-${containerCounter}`,
|
|
338
|
+
label,
|
|
339
|
+
metadata: {},
|
|
340
|
+
children: [],
|
|
341
|
+
parentId: null,
|
|
342
|
+
isContainer: true,
|
|
343
|
+
lineNumber,
|
|
344
|
+
color,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
attachNode(node, indent, indentStack, result);
|
|
348
|
+
// Don't register containers in labelToNode — arrows target pages, not containers
|
|
349
|
+
} else if (metadataMatch && indentStack.length > 0) {
|
|
350
|
+
// Metadata line — attach to parent
|
|
351
|
+
const rawKey = metadataMatch[1].trim().toLowerCase();
|
|
352
|
+
const key = aliasMap.get(rawKey) ?? rawKey;
|
|
353
|
+
const value = metadataMatch[2].trim();
|
|
354
|
+
|
|
355
|
+
const parent = findParentNode(indent, indentStack);
|
|
356
|
+
if (!parent) {
|
|
357
|
+
pushError(lineNumber, 'Metadata has no parent node');
|
|
358
|
+
} else {
|
|
359
|
+
parent.metadata[key] = value;
|
|
360
|
+
}
|
|
361
|
+
} else if (metadataMatch && indentStack.length === 0) {
|
|
362
|
+
// Could be a node label containing ':'
|
|
363
|
+
if (indent === 0) {
|
|
364
|
+
const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap);
|
|
365
|
+
attachNode(node, indent, indentStack, result);
|
|
366
|
+
labelToNode.set(node.label.toLowerCase(), node);
|
|
367
|
+
} else {
|
|
368
|
+
pushError(lineNumber, 'Metadata has no parent node');
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
// Node label — possibly with pipe-delimited metadata
|
|
372
|
+
const node = parseNodeLabel(trimmed, lineNumber, palette, ++nodeCounter, aliasMap);
|
|
373
|
+
attachNode(node, indent, indentStack, result);
|
|
374
|
+
labelToNode.set(node.label.toLowerCase(), node);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// --- Post-parse: resolve arrow targets ---
|
|
379
|
+
for (const arrow of deferredArrows) {
|
|
380
|
+
const targetKey = arrow.targetLabel.toLowerCase();
|
|
381
|
+
const targetNode = labelToNode.get(targetKey);
|
|
382
|
+
|
|
383
|
+
if (!targetNode) {
|
|
384
|
+
// Try suggestion
|
|
385
|
+
const allLabels = Array.from(labelToNode.keys());
|
|
386
|
+
let msg = `Arrow target "${arrow.targetLabel}" not found`;
|
|
387
|
+
const hint = suggest(targetKey, allLabels);
|
|
388
|
+
if (hint) msg += `. ${hint}`;
|
|
389
|
+
pushError(arrow.lineNumber, msg);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
result.edges.push({
|
|
394
|
+
sourceId: arrow.sourceNode.id,
|
|
395
|
+
targetId: targetNode.id,
|
|
396
|
+
label: arrow.label,
|
|
397
|
+
color: arrow.color,
|
|
398
|
+
lineNumber: arrow.lineNumber,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Validate tag group values on all nodes
|
|
403
|
+
if (result.tagGroups.length > 0) {
|
|
404
|
+
const allNodes: SitemapNode[] = [];
|
|
405
|
+
const collectAll = (nodes: SitemapNode[]) => {
|
|
406
|
+
for (const node of nodes) {
|
|
407
|
+
allNodes.push(node);
|
|
408
|
+
collectAll(node.children);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
collectAll(result.roots);
|
|
412
|
+
validateTagValues(allNodes, result.tagGroups, pushWarning, suggest);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (result.roots.length === 0 && result.tagGroups.length === 0 && !result.error) {
|
|
416
|
+
const diag = makeDgmoError(1, 'No pages found in sitemap');
|
|
417
|
+
result.diagnostics.push(diag);
|
|
418
|
+
result.error = formatDgmoError(diag);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ============================================================
|
|
425
|
+
// Internal helpers
|
|
426
|
+
// ============================================================
|
|
427
|
+
|
|
428
|
+
function parseNodeLabel(
|
|
429
|
+
trimmed: string,
|
|
430
|
+
lineNumber: number,
|
|
431
|
+
palette: PaletteColors | undefined,
|
|
432
|
+
counter: number,
|
|
433
|
+
aliasMap: Map<string, string> = new Map(),
|
|
434
|
+
): SitemapNode {
|
|
435
|
+
const segments = trimmed.split('|').map((s) => s.trim());
|
|
436
|
+
const rawLabel = segments[0];
|
|
437
|
+
const { label, color } = extractColor(rawLabel, palette);
|
|
438
|
+
const metadata = parsePipeMetadata(segments, aliasMap);
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
id: `node-${counter}`,
|
|
442
|
+
label,
|
|
443
|
+
metadata,
|
|
444
|
+
children: [],
|
|
445
|
+
parentId: null,
|
|
446
|
+
isContainer: false,
|
|
447
|
+
lineNumber,
|
|
448
|
+
color,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function attachNode(
|
|
453
|
+
node: SitemapNode,
|
|
454
|
+
indent: number,
|
|
455
|
+
indentStack: { node: SitemapNode; indent: number }[],
|
|
456
|
+
result: ParsedSitemap,
|
|
457
|
+
): void {
|
|
458
|
+
// Pop stack entries with indent >= current indent
|
|
459
|
+
while (indentStack.length > 0) {
|
|
460
|
+
const top = indentStack[indentStack.length - 1];
|
|
461
|
+
if (top.indent < indent) break;
|
|
462
|
+
indentStack.pop();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (indentStack.length > 0) {
|
|
466
|
+
const parent = indentStack[indentStack.length - 1].node;
|
|
467
|
+
node.parentId = parent.id;
|
|
468
|
+
parent.children.push(node);
|
|
469
|
+
} else {
|
|
470
|
+
result.roots.push(node);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
indentStack.push({ node, indent });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function findParentNode(
|
|
477
|
+
indent: number,
|
|
478
|
+
indentStack: { node: SitemapNode; indent: number }[],
|
|
479
|
+
): SitemapNode | null {
|
|
480
|
+
for (let i = indentStack.length - 1; i >= 0; i--) {
|
|
481
|
+
if (indentStack[i].indent < indent) {
|
|
482
|
+
return indentStack[i].node;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (indentStack.length > 0) {
|
|
486
|
+
return indentStack[indentStack.length - 1].node;
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|