@diagrammo/dgmo 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +185 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +189 -194
- package/dist/editor.cjs +336 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3699 -1564
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +3699 -1564
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +822 -1060
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +8 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +113 -62
- package/src/chart.ts +149 -64
- package/src/class/parser.ts +84 -28
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +179 -77
- package/src/completion.ts +381 -182
- package/src/d3.ts +1026 -428
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +70 -24
- package/src/echarts.ts +682 -169
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +55 -29
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +291 -97
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +48 -75
- package/src/graph/state-parser.ts +54 -27
- package/src/infra/parser.ts +161 -177
- package/src/infra/renderer.ts +723 -271
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +144 -56
- package/src/kanban/parser.ts +27 -19
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +71 -27
- package/src/org/resolver.ts +3 -3
- package/src/palettes/index.ts +3 -2
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +209 -100
- package/src/sitemap/parser.ts +73 -44
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +82 -72
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
package/src/infra/parser.ts
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Parses `infra [Title]` syntax into a structured InfraModel.
|
|
6
6
|
// Handles: chart metadata, component blocks with indented properties
|
|
7
|
-
// and connections, [Group]
|
|
7
|
+
// and connections, [Group] containers, tag groups, pipe metadata.
|
|
8
8
|
|
|
9
9
|
import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
measureIndent,
|
|
12
|
+
parseFirstLine,
|
|
13
|
+
OPTION_NOCOLON_RE,
|
|
14
|
+
} from '../utils/parsing';
|
|
11
15
|
import { matchTagBlockHeading } from '../utils/tag-groups';
|
|
12
16
|
import type {
|
|
13
17
|
ParsedInfra,
|
|
@@ -21,31 +25,21 @@ import { INFRA_BEHAVIOR_KEYS, EDGE_ONLY_KEYS } from './types';
|
|
|
21
25
|
// Regex patterns
|
|
22
26
|
// ============================================================
|
|
23
27
|
|
|
24
|
-
// Connection: -label-> Target or -> Target (
|
|
25
|
-
const CONNECTION_RE =
|
|
26
|
-
/^-(?:([^-].*?))?->\s*(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
|
|
28
|
+
// Connection: -label-> Target or -> Target (pipe metadata handled by extractPipeMetadata)
|
|
29
|
+
const CONNECTION_RE = /^-(?:([^-].*?))?->\s*(.+?)\s*$/;
|
|
27
30
|
|
|
28
31
|
// Simple connection shorthand: -> Target (no label, no dash prefix needed for edge)
|
|
29
|
-
const SIMPLE_CONNECTION_RE =
|
|
30
|
-
/^->\s*(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
|
|
32
|
+
const SIMPLE_CONNECTION_RE = /^->\s*(.+?)\s*$/;
|
|
31
33
|
|
|
32
|
-
// Async connection: ~label~> Target or ~> Target
|
|
33
|
-
const ASYNC_CONNECTION_RE =
|
|
34
|
-
/^~(?:([^~].*?))?~>\s*(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
|
|
34
|
+
// Async connection: ~label~> Target or ~> Target
|
|
35
|
+
const ASYNC_CONNECTION_RE = /^~(?:([^~].*?))?~>\s*(.+?)\s*$/;
|
|
35
36
|
|
|
36
37
|
// Async simple connection shorthand: ~> Target
|
|
37
|
-
const ASYNC_SIMPLE_CONNECTION_RE =
|
|
38
|
-
/^~>\s*(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
|
|
38
|
+
const ASYNC_SIMPLE_CONNECTION_RE = /^~>\s*(.+?)\s*$/;
|
|
39
39
|
|
|
40
40
|
// Deprecated xN fanout suffix (e.g. "x5" at end of line)
|
|
41
41
|
const DEPRECATED_FANOUT_RE = /\bx(\d+)\s*$/;
|
|
42
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
43
|
// Group declaration: [Group Name] with optional pipe metadata
|
|
50
44
|
const GROUP_RE = /^\[([^\]]+)\]\s*(?:\|\s*(.+))?$/;
|
|
51
45
|
|
|
@@ -74,8 +68,12 @@ const EDGE_NODE_NAMES = new Set(['edge', 'internet']);
|
|
|
74
68
|
|
|
75
69
|
// Known top-level option keys (space-separated, no colon)
|
|
76
70
|
const TOP_LEVEL_OPTIONS = new Set([
|
|
77
|
-
'slo-availability',
|
|
78
|
-
'
|
|
71
|
+
'slo-availability',
|
|
72
|
+
'slo-p90-latency-ms',
|
|
73
|
+
'slo-warning-margin',
|
|
74
|
+
'default-latency-ms',
|
|
75
|
+
'default-uptime',
|
|
76
|
+
'default-rps',
|
|
79
77
|
]);
|
|
80
78
|
|
|
81
79
|
// ============================================================
|
|
@@ -100,9 +98,10 @@ function parsePropertyValue(raw: string): string | number {
|
|
|
100
98
|
return raw.trim();
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
function extractPipeMetadata(
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
function extractPipeMetadata(rest: string): {
|
|
102
|
+
tags: Record<string, string>;
|
|
103
|
+
clean: string;
|
|
104
|
+
} {
|
|
106
105
|
const tags: Record<string, string> = {};
|
|
107
106
|
let clean = rest;
|
|
108
107
|
let match: RegExpExecArray | null;
|
|
@@ -114,6 +113,30 @@ function extractPipeMetadata(
|
|
|
114
113
|
return { tags, clean: clean.trim() };
|
|
115
114
|
}
|
|
116
115
|
|
|
116
|
+
// Detect unparsed pipe metadata left in a target name after extractPipeMetadata.
|
|
117
|
+
// Common case: `split 100%` without a colon isn't picked up by PIPE_META_RE.
|
|
118
|
+
const UNPARSED_SPLIT_RE = /\bsplit\s+(\d+)%/;
|
|
119
|
+
|
|
120
|
+
function warnUnparsedPipeMeta(
|
|
121
|
+
targetName: string,
|
|
122
|
+
lineNumber: number,
|
|
123
|
+
warnFn: (line: number, message: string) => void
|
|
124
|
+
): void {
|
|
125
|
+
if (!targetName.includes('|')) return;
|
|
126
|
+
const splitMatch = targetName.match(UNPARSED_SPLIT_RE);
|
|
127
|
+
if (splitMatch) {
|
|
128
|
+
warnFn(
|
|
129
|
+
lineNumber,
|
|
130
|
+
`'split ${splitMatch[1]}%' needs a colon — use 'split: ${splitMatch[1]}%'`
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
warnFn(
|
|
134
|
+
lineNumber,
|
|
135
|
+
`Unparsed pipe metadata in target — pipe values use 'key: value' syntax`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
117
140
|
// ============================================================
|
|
118
141
|
// Parser
|
|
119
142
|
// ============================================================
|
|
@@ -156,20 +179,26 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
156
179
|
if (currentNode && !nodeMap.has(currentNode.id)) {
|
|
157
180
|
// Validate mutual exclusion: concurrency vs instances/max-rps
|
|
158
181
|
const keys = new Set(currentNode.properties.map((p) => p.key));
|
|
159
|
-
if (
|
|
160
|
-
|
|
182
|
+
if (
|
|
183
|
+
keys.has('concurrency') &&
|
|
184
|
+
(keys.has('instances') || keys.has('max-rps'))
|
|
185
|
+
) {
|
|
186
|
+
const conflicting = [
|
|
187
|
+
keys.has('instances') ? 'instances' : '',
|
|
188
|
+
keys.has('max-rps') ? 'max-rps' : '',
|
|
189
|
+
]
|
|
161
190
|
.filter(Boolean)
|
|
162
191
|
.join(', ');
|
|
163
192
|
warn(
|
|
164
193
|
currentNode.lineNumber,
|
|
165
|
-
`'concurrency' (serverless) is mutually exclusive with ${conflicting}. Serverless nodes scale via concurrency, not instances
|
|
194
|
+
`'concurrency' (serverless) is mutually exclusive with ${conflicting}. Serverless nodes scale via concurrency, not instances.`
|
|
166
195
|
);
|
|
167
196
|
}
|
|
168
197
|
// Validate mutual exclusion: buffer (queue) vs max-rps (service)
|
|
169
198
|
if (keys.has('buffer') && keys.has('max-rps')) {
|
|
170
199
|
warn(
|
|
171
200
|
currentNode.lineNumber,
|
|
172
|
-
`'buffer' (queue) and 'max-rps' (service) represent different capacity models. A queue buffers messages; a service processes them
|
|
201
|
+
`'buffer' (queue) and 'max-rps' (service) represent different capacity models. A queue buffers messages; a service processes them.`
|
|
173
202
|
);
|
|
174
203
|
}
|
|
175
204
|
nodeMap.set(currentNode.id, currentNode);
|
|
@@ -208,7 +237,10 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
208
237
|
const firstLineResult = parseFirstLine(trimmed);
|
|
209
238
|
if (firstLineResult) {
|
|
210
239
|
if (firstLineResult.chartType !== 'infra') {
|
|
211
|
-
setError(
|
|
240
|
+
setError(
|
|
241
|
+
lineNumber,
|
|
242
|
+
`Expected chart type 'infra', got '${firstLineResult.chartType}'`
|
|
243
|
+
);
|
|
212
244
|
}
|
|
213
245
|
if (firstLineResult.title) {
|
|
214
246
|
result.title = firstLineResult.title;
|
|
@@ -217,16 +249,9 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
217
249
|
continue;
|
|
218
250
|
}
|
|
219
251
|
|
|
220
|
-
// direction
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const raw = trimmed.replace(/^(?:direction|orientation)\s+/i, '').trim();
|
|
224
|
-
const dir = normalizeDirection(raw);
|
|
225
|
-
if (dir) {
|
|
226
|
-
result.direction = dir;
|
|
227
|
-
} else {
|
|
228
|
-
warn(lineNumber, `Unknown direction '${raw}'. Expected 'LR', 'TB', 'horizontal', or 'vertical'.`);
|
|
229
|
-
}
|
|
252
|
+
// direction-tb — bare boolean to switch to top-to-bottom (default is LR)
|
|
253
|
+
if (/^direction-tb$/i.test(trimmed)) {
|
|
254
|
+
result.direction = 'TB';
|
|
230
255
|
continue;
|
|
231
256
|
}
|
|
232
257
|
|
|
@@ -247,23 +272,6 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
247
272
|
continue;
|
|
248
273
|
}
|
|
249
274
|
|
|
250
|
-
// scenario: Name — no longer supported
|
|
251
|
-
if (/^scenario\s*:/i.test(trimmed)) {
|
|
252
|
-
setError(lineNumber, `'scenario:' syntax is no longer supported`);
|
|
253
|
-
// Skip indented block
|
|
254
|
-
let si = i + 1;
|
|
255
|
-
while (si < lines.length) {
|
|
256
|
-
const sLine = lines[si];
|
|
257
|
-
const sTrimmed = sLine.trim();
|
|
258
|
-
if (!sTrimmed || sTrimmed.startsWith('#')) { si++; continue; }
|
|
259
|
-
const sIndent = sLine.length - sLine.trimStart().length;
|
|
260
|
-
if (sIndent === 0) break;
|
|
261
|
-
si++;
|
|
262
|
-
}
|
|
263
|
-
i = si - 1;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
275
|
// Tag group: `tag Name [alias]` (via shared matchTagBlockHeading)
|
|
268
276
|
const tagMatch = matchTagBlockHeading(trimmed);
|
|
269
277
|
if (tagMatch) {
|
|
@@ -278,23 +286,6 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
278
286
|
continue;
|
|
279
287
|
}
|
|
280
288
|
|
|
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
289
|
// [Group Name] or [Group Name] | t: Engineering
|
|
299
290
|
const groupMatch = trimmed.match(GROUP_RE);
|
|
300
291
|
if (groupMatch) {
|
|
@@ -302,43 +293,22 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
302
293
|
finishCurrentTagGroup();
|
|
303
294
|
const gLabel = groupMatch[1].trim();
|
|
304
295
|
const gId = groupId(gLabel);
|
|
305
|
-
const groupMeta = groupMatch[2]
|
|
296
|
+
const groupMeta = groupMatch[2]
|
|
297
|
+
? extractPipeMetadata('|' + groupMatch[2]).tags
|
|
298
|
+
: undefined;
|
|
306
299
|
currentGroup = {
|
|
307
300
|
id: gId,
|
|
308
301
|
label: gLabel,
|
|
309
|
-
metadata:
|
|
302
|
+
metadata:
|
|
303
|
+
groupMeta && Object.keys(groupMeta).length > 0
|
|
304
|
+
? groupMeta
|
|
305
|
+
: undefined,
|
|
310
306
|
lineNumber,
|
|
311
307
|
};
|
|
312
308
|
result.groups.push(currentGroup);
|
|
313
309
|
continue;
|
|
314
310
|
}
|
|
315
311
|
|
|
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
|
-
|
|
342
312
|
// Component at top level (no indent)
|
|
343
313
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
344
314
|
if (compMatch) {
|
|
@@ -383,6 +353,11 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
383
353
|
}
|
|
384
354
|
continue;
|
|
385
355
|
}
|
|
356
|
+
warn(
|
|
357
|
+
lineNumber,
|
|
358
|
+
`Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}'.`
|
|
359
|
+
);
|
|
360
|
+
continue;
|
|
386
361
|
}
|
|
387
362
|
|
|
388
363
|
// Inside a [Group] but no current node — group properties or component declaration
|
|
@@ -406,30 +381,8 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
406
381
|
currentGroup.collapsed = val.toLowerCase() === 'true';
|
|
407
382
|
continue;
|
|
408
383
|
}
|
|
409
|
-
|
|
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;
|
|
384
|
+
// Fall through to component matching — could be a component name
|
|
385
|
+
// that happens to match PROPERTY_RE (e.g., "MyService v2")
|
|
433
386
|
}
|
|
434
387
|
|
|
435
388
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
@@ -462,9 +415,17 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
462
415
|
if (currentNode && indent > baseIndent) {
|
|
463
416
|
// Detect deprecated xN fanout syntax
|
|
464
417
|
const deprecatedFanout = trimmed.match(DEPRECATED_FANOUT_RE);
|
|
465
|
-
if (
|
|
418
|
+
if (
|
|
419
|
+
deprecatedFanout &&
|
|
420
|
+
(trimmed.startsWith('->') ||
|
|
421
|
+
trimmed.startsWith('-') ||
|
|
422
|
+
trimmed.startsWith('~'))
|
|
423
|
+
) {
|
|
466
424
|
const n = deprecatedFanout[1];
|
|
467
|
-
setError(
|
|
425
|
+
setError(
|
|
426
|
+
lineNumber,
|
|
427
|
+
`'x${n}' fanout syntax is no longer supported — use '| fanout: ${n}' instead`
|
|
428
|
+
);
|
|
468
429
|
continue;
|
|
469
430
|
}
|
|
470
431
|
|
|
@@ -472,14 +433,20 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
472
433
|
const asyncSimpleConn = trimmed.match(ASYNC_SIMPLE_CONNECTION_RE);
|
|
473
434
|
if (asyncSimpleConn) {
|
|
474
435
|
const targetRaw = asyncSimpleConn[1].trim();
|
|
475
|
-
const splitStr = asyncSimpleConn[2];
|
|
476
436
|
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
477
437
|
const targetName = pipeMeta.clean || targetRaw;
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
438
|
+
warnUnparsedPipeMeta(targetName, lineNumber, warn);
|
|
439
|
+
const split = pipeMeta.tags.split
|
|
440
|
+
? parseFloat(pipeMeta.tags.split)
|
|
441
|
+
: null;
|
|
442
|
+
const fanoutRaw = pipeMeta.tags.fanout
|
|
443
|
+
? parseInt(pipeMeta.tags.fanout, 10)
|
|
444
|
+
: null;
|
|
481
445
|
if (fanoutRaw !== null && fanoutRaw < 1) {
|
|
482
|
-
warn(
|
|
446
|
+
warn(
|
|
447
|
+
lineNumber,
|
|
448
|
+
`Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
|
|
449
|
+
);
|
|
483
450
|
}
|
|
484
451
|
const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
|
|
485
452
|
result.edges.push({
|
|
@@ -499,14 +466,20 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
499
466
|
if (asyncConnMatch) {
|
|
500
467
|
const label = asyncConnMatch[1]?.trim() || '';
|
|
501
468
|
const targetRaw = asyncConnMatch[2].trim();
|
|
502
|
-
const splitStr = asyncConnMatch[3];
|
|
503
469
|
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
504
470
|
const targetName = pipeMeta.clean || targetRaw;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
471
|
+
warnUnparsedPipeMeta(targetName, lineNumber, warn);
|
|
472
|
+
const split = pipeMeta.tags.split
|
|
473
|
+
? parseFloat(pipeMeta.tags.split)
|
|
474
|
+
: null;
|
|
475
|
+
const fanoutRaw = pipeMeta.tags.fanout
|
|
476
|
+
? parseInt(pipeMeta.tags.fanout, 10)
|
|
477
|
+
: null;
|
|
508
478
|
if (fanoutRaw !== null && fanoutRaw < 1) {
|
|
509
|
-
warn(
|
|
479
|
+
warn(
|
|
480
|
+
lineNumber,
|
|
481
|
+
`Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
|
|
482
|
+
);
|
|
510
483
|
}
|
|
511
484
|
const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
|
|
512
485
|
|
|
@@ -534,15 +507,20 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
534
507
|
const simpleConn = trimmed.match(SIMPLE_CONNECTION_RE);
|
|
535
508
|
if (simpleConn) {
|
|
536
509
|
const targetRaw = simpleConn[1].trim();
|
|
537
|
-
const splitStr = simpleConn[2];
|
|
538
|
-
// Parse pipe metadata for fanout/split (and clean target name)
|
|
539
510
|
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
540
511
|
const targetName = pipeMeta.clean || targetRaw;
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
512
|
+
warnUnparsedPipeMeta(targetName, lineNumber, warn);
|
|
513
|
+
const split = pipeMeta.tags.split
|
|
514
|
+
? parseFloat(pipeMeta.tags.split)
|
|
515
|
+
: null;
|
|
516
|
+
const fanoutRaw = pipeMeta.tags.fanout
|
|
517
|
+
? parseInt(pipeMeta.tags.fanout, 10)
|
|
518
|
+
: null;
|
|
544
519
|
if (fanoutRaw !== null && fanoutRaw < 1) {
|
|
545
|
-
warn(
|
|
520
|
+
warn(
|
|
521
|
+
lineNumber,
|
|
522
|
+
`Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
|
|
523
|
+
);
|
|
546
524
|
}
|
|
547
525
|
const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
|
|
548
526
|
result.edges.push({
|
|
@@ -562,15 +540,20 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
562
540
|
if (connMatch) {
|
|
563
541
|
const label = connMatch[1]?.trim() || '';
|
|
564
542
|
const targetRaw = connMatch[2].trim();
|
|
565
|
-
const splitStr = connMatch[3];
|
|
566
|
-
// Parse pipe metadata for fanout/split (and clean target name)
|
|
567
543
|
const pipeMeta = extractPipeMetadata(targetRaw);
|
|
568
544
|
const targetName = pipeMeta.clean || targetRaw;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
545
|
+
warnUnparsedPipeMeta(targetName, lineNumber, warn);
|
|
546
|
+
const split = pipeMeta.tags.split
|
|
547
|
+
? parseFloat(pipeMeta.tags.split)
|
|
548
|
+
: null;
|
|
549
|
+
const fanoutRaw = pipeMeta.tags.fanout
|
|
550
|
+
? parseInt(pipeMeta.tags.fanout, 10)
|
|
551
|
+
: null;
|
|
572
552
|
if (fanoutRaw !== null && fanoutRaw < 1) {
|
|
573
|
-
warn(
|
|
553
|
+
warn(
|
|
554
|
+
lineNumber,
|
|
555
|
+
`Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
|
|
556
|
+
);
|
|
574
557
|
}
|
|
575
558
|
const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
|
|
576
559
|
|
|
@@ -622,7 +605,10 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
622
605
|
|
|
623
606
|
// Validate edge-only keys
|
|
624
607
|
if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
|
|
625
|
-
warn(
|
|
608
|
+
warn(
|
|
609
|
+
lineNumber,
|
|
610
|
+
`Property '${key}' is only valid on the entry point (Edge/Internet).`
|
|
611
|
+
);
|
|
626
612
|
}
|
|
627
613
|
|
|
628
614
|
const value = parsePropertyValue(rawVal);
|
|
@@ -631,7 +617,10 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
631
617
|
}
|
|
632
618
|
|
|
633
619
|
// Unknown indented line
|
|
634
|
-
warn(
|
|
620
|
+
warn(
|
|
621
|
+
lineNumber,
|
|
622
|
+
`Unexpected line inside component '${currentNode.label}'.`
|
|
623
|
+
);
|
|
635
624
|
continue;
|
|
636
625
|
}
|
|
637
626
|
|
|
@@ -639,28 +628,6 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
639
628
|
if (currentGroup && indent > 0) {
|
|
640
629
|
finishCurrentNode();
|
|
641
630
|
|
|
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
|
-
|
|
664
631
|
const compMatch = trimmed.match(COMPONENT_RE);
|
|
665
632
|
if (compMatch) {
|
|
666
633
|
const name = compMatch[1];
|
|
@@ -711,6 +678,9 @@ export function parseInfra(content: string): ParsedInfra {
|
|
|
711
678
|
continue;
|
|
712
679
|
}
|
|
713
680
|
}
|
|
681
|
+
|
|
682
|
+
// Catch-all: nothing matched this line
|
|
683
|
+
warn(lineNumber, `Unexpected line: '${trimmed}'.`);
|
|
714
684
|
}
|
|
715
685
|
|
|
716
686
|
// Flush last open blocks
|
|
@@ -780,7 +750,8 @@ export function extractSymbols(docText: string): DiagramSymbols {
|
|
|
780
750
|
// Recognize new-style bare options (`key value`) and old-style (`key: value`)
|
|
781
751
|
const firstLine = parseFirstLine(line);
|
|
782
752
|
if (firstLine) continue; // chart type line
|
|
783
|
-
if (/^(?:direction|
|
|
753
|
+
if (/^(?:direction-tb|animate|no-animate|slo-|default-)/i.test(line))
|
|
754
|
+
continue;
|
|
784
755
|
if (/^[a-z-]+\s*:/i.test(line)) continue; // legacy colon options
|
|
785
756
|
inMetadata = false;
|
|
786
757
|
} else {
|
|
@@ -790,11 +761,16 @@ export function extractSymbols(docText: string): DiagramSymbols {
|
|
|
790
761
|
|
|
791
762
|
if (!indented) {
|
|
792
763
|
// Root-level: tag group declaration, group header, or component
|
|
793
|
-
if (/^tag\s/i.test(line)) {
|
|
794
|
-
|
|
764
|
+
if (/^tag\s/i.test(line)) {
|
|
765
|
+
inTagGroup = true;
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (/^tag\s*:/i.test(line)) {
|
|
769
|
+
inTagGroup = true;
|
|
770
|
+
continue;
|
|
771
|
+
} // legacy
|
|
795
772
|
inTagGroup = false;
|
|
796
773
|
if (/^\[/.test(line)) continue; // [Group] header
|
|
797
|
-
if (/^#\s/.test(line)) continue; // # Group header
|
|
798
774
|
const m = COMPONENT_RE.exec(line);
|
|
799
775
|
if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
|
|
800
776
|
} else {
|
|
@@ -807,7 +783,15 @@ export function extractSymbols(docText: string): DiagramSymbols {
|
|
|
807
783
|
if (/^\w[\w-]*\s*:/.test(line)) continue; // property (key: value) legacy
|
|
808
784
|
// New-style property: first token is a known behavior/property key
|
|
809
785
|
const firstToken = line.split(/\s/)[0].toLowerCase();
|
|
810
|
-
if (
|
|
786
|
+
if (
|
|
787
|
+
(INFRA_BEHAVIOR_KEYS.has(firstToken) ||
|
|
788
|
+
EDGE_ONLY_KEYS.has(firstToken) ||
|
|
789
|
+
firstToken === 'description' ||
|
|
790
|
+
firstToken === 'instances' ||
|
|
791
|
+
firstToken === 'collapsed') &&
|
|
792
|
+
/\s/.test(line)
|
|
793
|
+
)
|
|
794
|
+
continue;
|
|
811
795
|
const m = COMPONENT_RE.exec(line);
|
|
812
796
|
if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
|
|
813
797
|
}
|