@diagrammo/dgmo 0.7.3 → 0.8.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/AGENTS.md +15 -20
- package/README.md +56 -58
- package/dist/cli.cjs +188 -181
- package/dist/index.cjs +3506 -1057
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +196 -43
- package/dist/index.d.ts +196 -43
- package/dist/index.js +3493 -1057
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +629 -289
- package/package.json +1 -1
- package/src/c4/layout.ts +6 -9
- package/src/c4/parser.ts +189 -83
- package/src/c4/renderer.ts +8 -9
- package/src/chart.ts +296 -83
- package/src/class/parser.ts +54 -37
- package/src/class/renderer.ts +8 -8
- package/src/cli.ts +8 -8
- package/src/colors.ts +4 -1
- package/src/completion.ts +757 -10
- package/src/d3.ts +310 -73
- package/src/dgmo-router.ts +63 -8
- package/src/echarts.ts +726 -231
- package/src/er/parser.ts +94 -76
- package/src/er/renderer.ts +6 -5
- package/src/gantt/parser.ts +144 -69
- package/src/gantt/renderer.ts +50 -14
- package/src/gantt/types.ts +3 -3
- package/src/graph/flowchart-parser.ts +97 -37
- package/src/graph/flowchart-renderer.ts +4 -3
- package/src/graph/state-parser.ts +50 -31
- package/src/graph/state-renderer.ts +4 -3
- package/src/index.ts +14 -5
- package/src/infra/compute.ts +1 -0
- package/src/infra/layout.ts +3 -0
- package/src/infra/parser.ts +291 -92
- package/src/infra/renderer.ts +172 -30
- package/src/infra/types.ts +5 -0
- package/src/initiative-status/layout.ts +1 -1
- package/src/initiative-status/parser.ts +121 -47
- package/src/initiative-status/renderer.ts +42 -23
- package/src/initiative-status/types.ts +10 -2
- package/src/kanban/parser.ts +60 -37
- package/src/kanban/renderer.ts +2 -2
- package/src/kanban/types.ts +1 -0
- package/src/org/layout.ts +9 -9
- package/src/org/parser.ts +39 -40
- package/src/org/renderer.ts +5 -6
- package/src/org/resolver.ts +26 -19
- package/src/render.ts +1 -1
- package/src/sequence/parser.ts +304 -95
- package/src/sequence/renderer.ts +9 -9
- package/src/sitemap/layout.ts +3 -4
- package/src/sitemap/parser.ts +57 -49
- package/src/sitemap/renderer.ts +6 -7
- package/src/utils/arrows.ts +25 -6
- package/src/utils/duration.ts +43 -7
- package/src/utils/legend-constants.ts +26 -0
- package/src/utils/legend-svg.ts +167 -0
- package/src/utils/parsing.ts +247 -7
- package/src/utils/tag-groups.ts +160 -15
- package/src/utils/title-constants.ts +9 -0
package/src/infra/parser.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// Infra Chart Parser
|
|
3
3
|
// ============================================================
|
|
4
4
|
//
|
|
5
|
-
// Parses `
|
|
5
|
+
// Parses `infra [Title]` syntax into a structured InfraModel.
|
|
6
6
|
// Handles: chart metadata, component blocks with indented properties
|
|
7
|
-
// and connections, [Group] containers, tag groups, pipe metadata.
|
|
7
|
+
// and connections, [Group] / # Group containers, tag groups, pipe metadata.
|
|
8
8
|
|
|
9
9
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
10
|
-
import { measureIndent } from '../utils/parsing';
|
|
10
|
+
import { measureIndent, normalizeDirection, parseFirstLine, GROUP_HASH_RE, OPTION_NOCOLON_RE } from '../utils/parsing';
|
|
11
|
+
import { matchTagBlockHeading } from '../utils/tag-groups';
|
|
11
12
|
import type {
|
|
12
13
|
ParsedInfra,
|
|
13
14
|
InfraNode,
|
|
@@ -20,22 +21,37 @@ import { INFRA_BEHAVIOR_KEYS, EDGE_ONLY_KEYS } from './types';
|
|
|
20
21
|
// Regex patterns
|
|
21
22
|
// ============================================================
|
|
22
23
|
|
|
23
|
-
// Connection: -label-> Target or -> Target (with optional | split: N%
|
|
24
|
+
// Connection: -label-> Target or -> Target (with optional | split: N% or pipe metadata)
|
|
24
25
|
const CONNECTION_RE =
|
|
25
|
-
/^-(?:([^-].*?))?->\s
|
|
26
|
+
/^-(?:([^-].*?))?->\s*(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
|
|
26
27
|
|
|
27
28
|
// Simple connection shorthand: -> Target (no label, no dash prefix needed for edge)
|
|
28
29
|
const SIMPLE_CONNECTION_RE =
|
|
29
|
-
/^->\s
|
|
30
|
+
/^->\s*(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
32
|
+
// Async connection: ~label~> Target or ~> Target (with optional | split: N% or pipe metadata)
|
|
33
|
+
const ASYNC_CONNECTION_RE =
|
|
34
|
+
/^~(?:([^~].*?))?~>\s*(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
|
|
33
35
|
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
+
// Async simple connection shorthand: ~> Target
|
|
37
|
+
const ASYNC_SIMPLE_CONNECTION_RE =
|
|
38
|
+
/^~>\s*(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
|
|
36
39
|
|
|
37
|
-
//
|
|
38
|
-
const
|
|
40
|
+
// Deprecated xN fanout suffix (e.g. "x5" at end of line)
|
|
41
|
+
const DEPRECATED_FANOUT_RE = /\bx(\d+)\s*$/;
|
|
42
|
+
|
|
43
|
+
// "is a" type declaration: NodeName is a <type>
|
|
44
|
+
const IS_A_RE = /^(.+?)\s+is\s+an?\s+(database|cache|queue|service|gateway|storage|function|network)\s*$/i;
|
|
45
|
+
|
|
46
|
+
// Valid node types for "is a" declarations
|
|
47
|
+
const VALID_NODE_TYPES = new Set(['database', 'cache', 'queue', 'service', 'gateway', 'storage', 'function', 'network']);
|
|
48
|
+
|
|
49
|
+
// Group declaration: [Group Name] with optional pipe metadata
|
|
50
|
+
const GROUP_RE = /^\[([^\]]+)\]\s*(?:\|\s*(.+))?$/;
|
|
51
|
+
|
|
52
|
+
// Tag value: Name or Name(color)
|
|
53
|
+
// Note: `default` keyword removed — first value is the default.
|
|
54
|
+
const TAG_VALUE_RE = /^(\w[\w\s]*?)(?:\(([^)]+)\))?\s*$/;
|
|
39
55
|
|
|
40
56
|
// Component line: ComponentName or ComponentName | t: Backend | env: Prod
|
|
41
57
|
// Allows hyphens in names (e.g. api-gateway, my-service-v2) — but not at the start.
|
|
@@ -44,8 +60,8 @@ const COMPONENT_RE = /^([a-zA-Z_][\w-]*)(.*)$/;
|
|
|
44
60
|
// Pipe metadata: | key: value or | k1: v1, k2: v2 (comma-separated)
|
|
45
61
|
const PIPE_META_RE = /[|,]\s*(\w+)\s*:\s*([^|,]+)/g;
|
|
46
62
|
|
|
47
|
-
// Property: key
|
|
48
|
-
const PROPERTY_RE = /^([\w-]+)\s
|
|
63
|
+
// Property: key value (space-separated, no colon)
|
|
64
|
+
const PROPERTY_RE = /^([\w-]+)\s+(.+)$/;
|
|
49
65
|
|
|
50
66
|
// Percentage value: 80% or 99.99%
|
|
51
67
|
const PERCENT_RE = /^([\d.]+)%$/;
|
|
@@ -56,6 +72,12 @@ const RANGE_RE = /^(\d+)-(\d+)$/;
|
|
|
56
72
|
// Node names that act as the traffic entry point (edge node)
|
|
57
73
|
const EDGE_NODE_NAMES = new Set(['edge', 'internet']);
|
|
58
74
|
|
|
75
|
+
// Known top-level option keys (space-separated, no colon)
|
|
76
|
+
const TOP_LEVEL_OPTIONS = new Set([
|
|
77
|
+
'slo-availability', 'slo-p90-latency-ms', 'slo-warning-margin',
|
|
78
|
+
'default-latency-ms', 'default-uptime', 'default-rps',
|
|
79
|
+
]);
|
|
80
|
+
|
|
59
81
|
// ============================================================
|
|
60
82
|
// Helpers
|
|
61
83
|
// ============================================================
|
|
@@ -182,72 +204,52 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
182
204
|
finishCurrentNode();
|
|
183
205
|
}
|
|
184
206
|
|
|
185
|
-
// chart: infra
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
setError(lineNumber, `Expected chart type 'infra', got '${
|
|
207
|
+
// First line: `infra [Title]` or legacy `chart: infra`
|
|
208
|
+
const firstLineResult = parseFirstLine(trimmed);
|
|
209
|
+
if (firstLineResult) {
|
|
210
|
+
if (firstLineResult.chartType !== 'infra') {
|
|
211
|
+
setError(lineNumber, `Expected chart type 'infra', got '${firstLineResult.chartType}'`);
|
|
212
|
+
}
|
|
213
|
+
if (firstLineResult.title) {
|
|
214
|
+
result.title = firstLineResult.title;
|
|
215
|
+
result.titleLineNumber = lineNumber;
|
|
190
216
|
}
|
|
191
217
|
continue;
|
|
192
218
|
}
|
|
193
219
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// direction: LR | TB
|
|
202
|
-
if (/^direction\s*:/i.test(trimmed)) {
|
|
203
|
-
const dir = trimmed.replace(/^direction\s*:\s*/i, '').trim().toUpperCase();
|
|
204
|
-
if (dir === 'LR' || dir === 'TB') {
|
|
220
|
+
// direction LR | TB (also accepts orientation as alias)
|
|
221
|
+
// Supports both `direction LR` (new) and `direction: LR` (legacy)
|
|
222
|
+
if (/^(?:direction|orientation)\s/i.test(trimmed)) {
|
|
223
|
+
const raw = trimmed.replace(/^(?:direction|orientation)\s+/i, '').trim();
|
|
224
|
+
const dir = normalizeDirection(raw);
|
|
225
|
+
if (dir) {
|
|
205
226
|
result.direction = dir;
|
|
206
227
|
} else {
|
|
207
|
-
warn(lineNumber, `Unknown direction '${
|
|
228
|
+
warn(lineNumber, `Unknown direction '${raw}'. Expected 'LR', 'TB', 'horizontal', or 'vertical'.`);
|
|
208
229
|
}
|
|
209
230
|
continue;
|
|
210
231
|
}
|
|
211
232
|
|
|
212
|
-
// animate
|
|
213
|
-
if (
|
|
214
|
-
result.options.animate =
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// default-latency-ms: <number>
|
|
219
|
-
if (/^default-latency-ms\s*:/i.test(trimmed)) {
|
|
220
|
-
result.options['default-latency-ms'] = trimmed.replace(/^default-latency-ms\s*:\s*/i, '').trim();
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// default-uptime: <number>
|
|
225
|
-
if (/^default-uptime\s*:/i.test(trimmed)) {
|
|
226
|
-
result.options['default-uptime'] = trimmed.replace(/^default-uptime\s*:\s*/i, '').trim();
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// slo-availability: <percentage e.g. 99.9%>
|
|
231
|
-
if (/^slo-availability\s*:/i.test(trimmed)) {
|
|
232
|
-
result.options['slo-availability'] = trimmed.replace(/^slo-availability\s*:\s*/i, '').trim();
|
|
233
|
+
// animate (default ON) / no-animate
|
|
234
|
+
if (trimmed === 'animate') {
|
|
235
|
+
result.options.animate = 'on';
|
|
233
236
|
continue;
|
|
234
237
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (/^slo-p90-latency-ms\s*:/i.test(trimmed)) {
|
|
238
|
-
result.options['slo-p90-latency-ms'] = trimmed.replace(/^slo-p90-latency-ms\s*:\s*/i, '').trim();
|
|
238
|
+
if (trimmed === 'no-animate') {
|
|
239
|
+
result.options.animate = 'off';
|
|
239
240
|
continue;
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
// Top-level options: `key value` (space-separated, no colon)
|
|
244
|
+
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
245
|
+
if (optMatch && TOP_LEVEL_OPTIONS.has(optMatch[1].toLowerCase())) {
|
|
246
|
+
result.options[optMatch[1].toLowerCase()] = optMatch[2].trim();
|
|
245
247
|
continue;
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
// scenario: Name —
|
|
250
|
+
// scenario: Name — no longer supported
|
|
249
251
|
if (/^scenario\s*:/i.test(trimmed)) {
|
|
250
|
-
|
|
252
|
+
setError(lineNumber, `'scenario:' syntax is no longer supported`);
|
|
251
253
|
// Skip indented block
|
|
252
254
|
let si = i + 1;
|
|
253
255
|
while (si < lines.length) {
|
|
@@ -262,32 +264,81 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
262
264
|
continue;
|
|
263
265
|
}
|
|
264
266
|
|
|
265
|
-
//
|
|
266
|
-
const tagMatch = trimmed
|
|
267
|
+
// Tag group: `tag Name [alias]` (via shared matchTagBlockHeading)
|
|
268
|
+
const tagMatch = matchTagBlockHeading(trimmed);
|
|
267
269
|
if (tagMatch) {
|
|
268
270
|
finishCurrentNode();
|
|
269
271
|
finishCurrentTagGroup();
|
|
270
272
|
currentTagGroup = {
|
|
271
|
-
name: tagMatch
|
|
272
|
-
alias: tagMatch
|
|
273
|
+
name: tagMatch.name,
|
|
274
|
+
alias: tagMatch.alias ?? null,
|
|
273
275
|
values: [],
|
|
274
276
|
lineNumber,
|
|
275
277
|
};
|
|
276
278
|
continue;
|
|
277
279
|
}
|
|
278
280
|
|
|
279
|
-
//
|
|
281
|
+
// # GroupName (alternate group notation)
|
|
282
|
+
const hashGroupMatch = trimmed.match(GROUP_HASH_RE);
|
|
283
|
+
if (hashGroupMatch) {
|
|
284
|
+
finishCurrentNode();
|
|
285
|
+
finishCurrentTagGroup();
|
|
286
|
+
const gLabel = hashGroupMatch[1].trim();
|
|
287
|
+
const gId = groupId(gLabel);
|
|
288
|
+
currentGroup = {
|
|
289
|
+
id: gId,
|
|
290
|
+
label: gLabel,
|
|
291
|
+
metadata: undefined,
|
|
292
|
+
lineNumber,
|
|
293
|
+
};
|
|
294
|
+
result.groups.push(currentGroup);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// [Group Name] or [Group Name] | t: Engineering
|
|
280
299
|
const groupMatch = trimmed.match(GROUP_RE);
|
|
281
300
|
if (groupMatch) {
|
|
282
301
|
finishCurrentNode();
|
|
283
302
|
finishCurrentTagGroup();
|
|
284
303
|
const gLabel = groupMatch[1].trim();
|
|
285
304
|
const gId = groupId(gLabel);
|
|
286
|
-
|
|
305
|
+
const groupMeta = groupMatch[2] ? extractPipeMetadata('|' + groupMatch[2]).tags : undefined;
|
|
306
|
+
currentGroup = {
|
|
307
|
+
id: gId,
|
|
308
|
+
label: gLabel,
|
|
309
|
+
metadata: groupMeta && Object.keys(groupMeta).length > 0 ? groupMeta : undefined,
|
|
310
|
+
lineNumber,
|
|
311
|
+
};
|
|
287
312
|
result.groups.push(currentGroup);
|
|
288
313
|
continue;
|
|
289
314
|
}
|
|
290
315
|
|
|
316
|
+
// "is a" type declaration: NodeName is a <type>
|
|
317
|
+
const isaMatch = trimmed.match(IS_A_RE);
|
|
318
|
+
if (isaMatch) {
|
|
319
|
+
finishCurrentNode();
|
|
320
|
+
finishCurrentTagGroup();
|
|
321
|
+
|
|
322
|
+
const name = isaMatch[1].trim();
|
|
323
|
+
const nType = isaMatch[2].toLowerCase();
|
|
324
|
+
const id = nodeId(name);
|
|
325
|
+
const isEdge = EDGE_NODE_NAMES.has(id.toLowerCase());
|
|
326
|
+
|
|
327
|
+
currentNode = {
|
|
328
|
+
id,
|
|
329
|
+
label: name,
|
|
330
|
+
properties: [],
|
|
331
|
+
groupId: null,
|
|
332
|
+
tags: {},
|
|
333
|
+
isEdge,
|
|
334
|
+
nodeType: nType,
|
|
335
|
+
lineNumber,
|
|
336
|
+
};
|
|
337
|
+
currentGroup = null;
|
|
338
|
+
baseIndent = 0;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
291
342
|
// Component at top level (no indent)
|
|
292
343
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
293
344
|
if (compMatch) {
|
|
@@ -317,7 +368,7 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
317
368
|
|
|
318
369
|
// ---- Indented lines ----
|
|
319
370
|
|
|
320
|
-
// Tag value inside tag group
|
|
371
|
+
// Tag value inside tag group — first value is the default
|
|
321
372
|
if (currentTagGroup && indent > 0) {
|
|
322
373
|
const tvMatch = trimmed.match(TAG_VALUE_RE);
|
|
323
374
|
if (tvMatch) {
|
|
@@ -326,7 +377,8 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
326
377
|
name: valueName,
|
|
327
378
|
color: tvMatch[2]?.trim(),
|
|
328
379
|
});
|
|
329
|
-
|
|
380
|
+
// First value is the default
|
|
381
|
+
if (currentTagGroup.values.length === 1) {
|
|
330
382
|
currentTagGroup.defaultValue = valueName;
|
|
331
383
|
}
|
|
332
384
|
continue;
|
|
@@ -356,13 +408,41 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
356
408
|
}
|
|
357
409
|
}
|
|
358
410
|
|
|
411
|
+
// "is a" type declaration inside group
|
|
412
|
+
const isaMatchG = trimmed.match(IS_A_RE);
|
|
413
|
+
if (isaMatchG) {
|
|
414
|
+
finishCurrentTagGroup();
|
|
415
|
+
const name = isaMatchG[1].trim();
|
|
416
|
+
const nType = isaMatchG[2].toLowerCase();
|
|
417
|
+
const id = nodeId(name);
|
|
418
|
+
// Cascade group metadata into node tags (node-level overrides later)
|
|
419
|
+
const tags: Record<string, string> = currentGroup.metadata ? { ...currentGroup.metadata } : {};
|
|
420
|
+
|
|
421
|
+
currentNode = {
|
|
422
|
+
id,
|
|
423
|
+
label: name,
|
|
424
|
+
properties: [],
|
|
425
|
+
groupId: currentGroup.id,
|
|
426
|
+
tags,
|
|
427
|
+
isEdge: false,
|
|
428
|
+
nodeType: nType,
|
|
429
|
+
lineNumber,
|
|
430
|
+
};
|
|
431
|
+
baseIndent = indent;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
359
435
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
360
436
|
if (compMatch) {
|
|
361
437
|
finishCurrentTagGroup();
|
|
362
438
|
const name = compMatch[1];
|
|
363
439
|
const rest = compMatch[2] || '';
|
|
364
|
-
const { tags } = extractPipeMetadata(rest);
|
|
440
|
+
const { tags: nodeTags } = extractPipeMetadata(rest);
|
|
365
441
|
const id = nodeId(name);
|
|
442
|
+
// Cascade group metadata into node tags; node-level metadata overrides
|
|
443
|
+
const tags: Record<string, string> = currentGroup.metadata
|
|
444
|
+
? { ...currentGroup.metadata, ...nodeTags }
|
|
445
|
+
: nodeTags;
|
|
366
446
|
|
|
367
447
|
currentNode = {
|
|
368
448
|
id,
|
|
@@ -380,22 +460,96 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
380
460
|
|
|
381
461
|
// Inside a component block — properties and connections
|
|
382
462
|
if (currentNode && indent > baseIndent) {
|
|
383
|
-
//
|
|
463
|
+
// Detect deprecated xN fanout syntax
|
|
464
|
+
const deprecatedFanout = trimmed.match(DEPRECATED_FANOUT_RE);
|
|
465
|
+
if (deprecatedFanout && (trimmed.startsWith('->') || trimmed.startsWith('-') || trimmed.startsWith('~'))) {
|
|
466
|
+
const n = deprecatedFanout[1];
|
|
467
|
+
setError(lineNumber, `'x${n}' fanout syntax is no longer supported — use '| fanout: ${n}' instead`);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Async simple connection: ~> Target
|
|
472
|
+
const asyncSimpleConn = trimmed.match(ASYNC_SIMPLE_CONNECTION_RE);
|
|
473
|
+
if (asyncSimpleConn) {
|
|
474
|
+
const targetRaw = asyncSimpleConn[1].trim();
|
|
475
|
+
const splitStr = asyncSimpleConn[2];
|
|
476
|
+
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
477
|
+
const targetName = pipeMeta.clean || targetRaw;
|
|
478
|
+
const split = splitStr ? parseFloat(splitStr)
|
|
479
|
+
: pipeMeta.tags.split ? parseFloat(pipeMeta.tags.split) : null;
|
|
480
|
+
const fanoutRaw = pipeMeta.tags.fanout ? parseInt(pipeMeta.tags.fanout, 10) : null;
|
|
481
|
+
if (fanoutRaw !== null && fanoutRaw < 1) {
|
|
482
|
+
warn(lineNumber, `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`);
|
|
483
|
+
}
|
|
484
|
+
const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
|
|
485
|
+
result.edges.push({
|
|
486
|
+
sourceId: currentNode.id,
|
|
487
|
+
targetId: nodeId(targetName),
|
|
488
|
+
label: '',
|
|
489
|
+
async: true,
|
|
490
|
+
split,
|
|
491
|
+
fanout,
|
|
492
|
+
lineNumber,
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Async labeled connection: ~label~> Target
|
|
498
|
+
const asyncConnMatch = trimmed.match(ASYNC_CONNECTION_RE);
|
|
499
|
+
if (asyncConnMatch) {
|
|
500
|
+
const label = asyncConnMatch[1]?.trim() || '';
|
|
501
|
+
const targetRaw = asyncConnMatch[2].trim();
|
|
502
|
+
const splitStr = asyncConnMatch[3];
|
|
503
|
+
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
504
|
+
const targetName = pipeMeta.clean || targetRaw;
|
|
505
|
+
const split = splitStr ? parseFloat(splitStr)
|
|
506
|
+
: pipeMeta.tags.split ? parseFloat(pipeMeta.tags.split) : null;
|
|
507
|
+
const fanoutRaw = pipeMeta.tags.fanout ? parseInt(pipeMeta.tags.fanout, 10) : null;
|
|
508
|
+
if (fanoutRaw !== null && fanoutRaw < 1) {
|
|
509
|
+
warn(lineNumber, `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`);
|
|
510
|
+
}
|
|
511
|
+
const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
|
|
512
|
+
|
|
513
|
+
let targetId: string;
|
|
514
|
+
const targetGroupMatch = targetName.match(/^\[([^\]]+)\]/);
|
|
515
|
+
if (targetGroupMatch) {
|
|
516
|
+
targetId = groupId(targetGroupMatch[1]);
|
|
517
|
+
} else {
|
|
518
|
+
targetId = nodeId(targetName);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
result.edges.push({
|
|
522
|
+
sourceId: currentNode.id,
|
|
523
|
+
targetId,
|
|
524
|
+
label,
|
|
525
|
+
async: true,
|
|
526
|
+
split,
|
|
527
|
+
fanout,
|
|
528
|
+
lineNumber,
|
|
529
|
+
});
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Simple connection: -> Target or -> Target | fanout: 5
|
|
384
534
|
const simpleConn = trimmed.match(SIMPLE_CONNECTION_RE);
|
|
385
535
|
if (simpleConn) {
|
|
386
|
-
const
|
|
536
|
+
const targetRaw = simpleConn[1].trim();
|
|
387
537
|
const splitStr = simpleConn[2];
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
const
|
|
538
|
+
// Parse pipe metadata for fanout/split (and clean target name)
|
|
539
|
+
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
540
|
+
const targetName = pipeMeta.clean || targetRaw;
|
|
541
|
+
const split = splitStr ? parseFloat(splitStr)
|
|
542
|
+
: pipeMeta.tags.split ? parseFloat(pipeMeta.tags.split) : null;
|
|
543
|
+
const fanoutRaw = pipeMeta.tags.fanout ? parseInt(pipeMeta.tags.fanout, 10) : null;
|
|
391
544
|
if (fanoutRaw !== null && fanoutRaw < 1) {
|
|
392
|
-
warn(lineNumber, `Fan-out multiplier must be at least 1 (got
|
|
545
|
+
warn(lineNumber, `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`);
|
|
393
546
|
}
|
|
394
547
|
const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
|
|
395
548
|
result.edges.push({
|
|
396
549
|
sourceId: currentNode.id,
|
|
397
550
|
targetId: nodeId(targetName),
|
|
398
551
|
label: '',
|
|
552
|
+
async: false,
|
|
399
553
|
split,
|
|
400
554
|
fanout,
|
|
401
555
|
lineNumber,
|
|
@@ -403,23 +557,26 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
403
557
|
continue;
|
|
404
558
|
}
|
|
405
559
|
|
|
406
|
-
// Labeled connection: -label-> Target | split: N
|
|
560
|
+
// Labeled connection: -label-> Target | split: N%, fanout: 3
|
|
407
561
|
const connMatch = trimmed.match(CONNECTION_RE);
|
|
408
562
|
if (connMatch) {
|
|
409
563
|
const label = connMatch[1]?.trim() || '';
|
|
410
|
-
const
|
|
564
|
+
const targetRaw = connMatch[2].trim();
|
|
411
565
|
const splitStr = connMatch[3];
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
const
|
|
566
|
+
// Parse pipe metadata for fanout/split (and clean target name)
|
|
567
|
+
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
568
|
+
const targetName = pipeMeta.clean || targetRaw;
|
|
569
|
+
const split = splitStr ? parseFloat(splitStr)
|
|
570
|
+
: pipeMeta.tags.split ? parseFloat(pipeMeta.tags.split) : null;
|
|
571
|
+
const fanoutRaw = pipeMeta.tags.fanout ? parseInt(pipeMeta.tags.fanout, 10) : null;
|
|
415
572
|
if (fanoutRaw !== null && fanoutRaw < 1) {
|
|
416
|
-
warn(lineNumber, `Fan-out multiplier must be at least 1 (got
|
|
573
|
+
warn(lineNumber, `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`);
|
|
417
574
|
}
|
|
418
575
|
const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
|
|
419
576
|
|
|
420
577
|
// Target might be a group ref like [API Pods]
|
|
421
578
|
let targetId: string;
|
|
422
|
-
const targetGroupMatch = targetName.match(
|
|
579
|
+
const targetGroupMatch = targetName.match(/^\[([^\]]+)\]/);
|
|
423
580
|
if (targetGroupMatch) {
|
|
424
581
|
targetId = groupId(targetGroupMatch[1]);
|
|
425
582
|
} else {
|
|
@@ -430,6 +587,7 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
430
587
|
sourceId: currentNode.id,
|
|
431
588
|
targetId,
|
|
432
589
|
label,
|
|
590
|
+
async: false,
|
|
433
591
|
split,
|
|
434
592
|
fanout,
|
|
435
593
|
lineNumber,
|
|
@@ -437,8 +595,8 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
437
595
|
continue;
|
|
438
596
|
}
|
|
439
597
|
|
|
440
|
-
// Empty description
|
|
441
|
-
if (/^description\s
|
|
598
|
+
// Empty description (no value) — silently skip rather than emitting "Unexpected line"
|
|
599
|
+
if (/^description\s*:?\s*$/i.test(trimmed)) continue;
|
|
442
600
|
|
|
443
601
|
// Property: key: value
|
|
444
602
|
const propMatch = trimmed.match(PROPERTY_RE);
|
|
@@ -480,12 +638,38 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
480
638
|
// Component inside group (same indent as group children)
|
|
481
639
|
if (currentGroup && indent > 0) {
|
|
482
640
|
finishCurrentNode();
|
|
641
|
+
|
|
642
|
+
// "is a" type declaration inside group
|
|
643
|
+
const isaMatchG2 = trimmed.match(IS_A_RE);
|
|
644
|
+
if (isaMatchG2) {
|
|
645
|
+
const name = isaMatchG2[1].trim();
|
|
646
|
+
const nType = isaMatchG2[2].toLowerCase();
|
|
647
|
+
const id = nodeId(name);
|
|
648
|
+
const tags: Record<string, string> = currentGroup.metadata ? { ...currentGroup.metadata } : {};
|
|
649
|
+
|
|
650
|
+
currentNode = {
|
|
651
|
+
id,
|
|
652
|
+
label: name,
|
|
653
|
+
properties: [],
|
|
654
|
+
groupId: currentGroup.id,
|
|
655
|
+
tags,
|
|
656
|
+
isEdge: false,
|
|
657
|
+
nodeType: nType,
|
|
658
|
+
lineNumber,
|
|
659
|
+
};
|
|
660
|
+
baseIndent = indent;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
|
|
483
664
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
484
665
|
if (compMatch) {
|
|
485
666
|
const name = compMatch[1];
|
|
486
667
|
const rest = compMatch[2] || '';
|
|
487
|
-
const { tags } = extractPipeMetadata(rest);
|
|
668
|
+
const { tags: nodeTags } = extractPipeMetadata(rest);
|
|
488
669
|
const id = nodeId(name);
|
|
670
|
+
const tags: Record<string, string> = currentGroup.metadata
|
|
671
|
+
? { ...currentGroup.metadata, ...nodeTags }
|
|
672
|
+
: nodeTags;
|
|
489
673
|
|
|
490
674
|
currentNode = {
|
|
491
675
|
id,
|
|
@@ -590,25 +774,40 @@ export function extractSymbols(docText: string): DiagramSymbols {
|
|
|
590
774
|
const indented = /^\s/.test(rawLine);
|
|
591
775
|
|
|
592
776
|
// Metadata phase: skip until first non-metadata root-level line.
|
|
593
|
-
//
|
|
777
|
+
// Metadata includes: `infra [Title]`, `chart: type`, `direction X`, `slo-*`, etc.
|
|
594
778
|
if (inMetadata) {
|
|
595
|
-
if (!indented
|
|
596
|
-
|
|
779
|
+
if (!indented) {
|
|
780
|
+
// Recognize new-style bare options (`key value`) and old-style (`key: value`)
|
|
781
|
+
const firstLine = parseFirstLine(line);
|
|
782
|
+
if (firstLine) continue; // chart type line
|
|
783
|
+
if (/^(?:direction|orientation|animate|no-animate|slo-|default-)/i.test(line)) continue;
|
|
784
|
+
if (/^[a-z-]+\s*:/i.test(line)) continue; // legacy colon options
|
|
785
|
+
inMetadata = false;
|
|
786
|
+
} else {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
597
789
|
}
|
|
598
790
|
|
|
599
791
|
if (!indented) {
|
|
600
792
|
// Root-level: tag group declaration, group header, or component
|
|
601
|
-
if (/^tag\s
|
|
793
|
+
if (/^tag\s/i.test(line)) { inTagGroup = true; continue; }
|
|
794
|
+
if (/^tag\s*:/i.test(line)) { inTagGroup = true; continue; } // legacy
|
|
602
795
|
inTagGroup = false;
|
|
603
|
-
if (/^\[/.test(line)) continue; //
|
|
796
|
+
if (/^\[/.test(line)) continue; // [Group] header
|
|
797
|
+
if (/^#\s/.test(line)) continue; // # Group header
|
|
604
798
|
const m = COMPONENT_RE.exec(line);
|
|
605
799
|
if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
|
|
606
800
|
} else {
|
|
607
801
|
// Indented: skip tag values, connections, and properties; extract grouped components
|
|
608
802
|
if (inTagGroup) continue;
|
|
609
803
|
if (/^->/.test(line)) continue; // simple connection
|
|
804
|
+
if (/^~>/.test(line)) continue; // async simple connection
|
|
610
805
|
if (/^-[^>]+-?>/.test(line)) continue; // labeled connection
|
|
611
|
-
if (
|
|
806
|
+
if (/^~[^~]+~>/.test(line)) continue; // async labeled connection
|
|
807
|
+
if (/^\w[\w-]*\s*:/.test(line)) continue; // property (key: value) legacy
|
|
808
|
+
// New-style property: first token is a known behavior/property key
|
|
809
|
+
const firstToken = line.split(/\s/)[0].toLowerCase();
|
|
810
|
+
if ((INFRA_BEHAVIOR_KEYS.has(firstToken) || EDGE_ONLY_KEYS.has(firstToken) || firstToken === 'description' || firstToken === 'instances' || firstToken === 'collapsed') && /\s/.test(line)) continue;
|
|
612
811
|
const m = COMPONENT_RE.exec(line);
|
|
613
812
|
if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
|
|
614
813
|
}
|