@diagrammo/dgmo 0.8.3 → 0.8.5

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.
Files changed (122) hide show
  1. package/.claude/commands/dgmo-diagram-this.md +60 -0
  2. package/.claude/commands/dgmo-document-project.md +128 -0
  3. package/.claude/commands/dgmo.md +452 -50
  4. package/.cursorrules +32 -37
  5. package/.github/copilot-instructions.md +35 -44
  6. package/.windsurfrules +32 -37
  7. package/README.md +4 -4
  8. package/dist/cli.cjs +188 -185
  9. package/dist/editor.cjs +338 -0
  10. package/dist/editor.cjs.map +1 -0
  11. package/dist/editor.d.cts +27 -0
  12. package/dist/editor.d.ts +27 -0
  13. package/dist/editor.js +307 -0
  14. package/dist/editor.js.map +1 -0
  15. package/dist/highlight.cjs +560 -0
  16. package/dist/highlight.cjs.map +1 -0
  17. package/dist/highlight.d.cts +32 -0
  18. package/dist/highlight.d.ts +32 -0
  19. package/dist/highlight.js +530 -0
  20. package/dist/highlight.js.map +1 -0
  21. package/dist/index.cjs +3467 -1078
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +22 -1
  24. package/dist/index.d.ts +22 -1
  25. package/dist/index.js +3466 -1078
  26. package/dist/index.js.map +1 -1
  27. package/docs/language-reference.md +46 -37
  28. package/gallery/fixtures/arc.dgmo +18 -0
  29. package/gallery/fixtures/area.dgmo +19 -0
  30. package/gallery/fixtures/bar-stacked.dgmo +10 -0
  31. package/gallery/fixtures/bar.dgmo +10 -0
  32. package/gallery/fixtures/c4-full.dgmo +52 -0
  33. package/gallery/fixtures/c4.dgmo +17 -0
  34. package/gallery/fixtures/chord.dgmo +12 -0
  35. package/gallery/fixtures/class-basic.dgmo +14 -0
  36. package/gallery/fixtures/class-full.dgmo +43 -0
  37. package/gallery/fixtures/doughnut.dgmo +8 -0
  38. package/gallery/fixtures/flowchart-basic.dgmo +3 -0
  39. package/gallery/fixtures/flowchart-colors.dgmo +5 -0
  40. package/gallery/fixtures/flowchart-complex.dgmo +17 -0
  41. package/gallery/fixtures/flowchart-decision.dgmo +5 -0
  42. package/gallery/fixtures/flowchart-full.dgmo +13 -0
  43. package/gallery/fixtures/flowchart-groups.dgmo +10 -0
  44. package/gallery/fixtures/flowchart-loop.dgmo +7 -0
  45. package/gallery/fixtures/flowchart-nested.dgmo +7 -0
  46. package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
  47. package/gallery/fixtures/function.dgmo +8 -0
  48. package/gallery/fixtures/funnel.dgmo +7 -0
  49. package/gallery/fixtures/gantt-full.dgmo +49 -0
  50. package/gallery/fixtures/gantt.dgmo +42 -0
  51. package/gallery/fixtures/heatmap.dgmo +8 -0
  52. package/gallery/fixtures/infra-full.dgmo +78 -0
  53. package/gallery/fixtures/infra-overload.dgmo +25 -0
  54. package/gallery/fixtures/infra.dgmo +47 -0
  55. package/gallery/fixtures/initiative-status-full.dgmo +46 -0
  56. package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
  57. package/gallery/fixtures/initiative-status.dgmo +9 -0
  58. package/gallery/fixtures/line.dgmo +19 -0
  59. package/gallery/fixtures/multi-line.dgmo +11 -0
  60. package/gallery/fixtures/org-basic.dgmo +16 -0
  61. package/gallery/fixtures/org-full.dgmo +69 -0
  62. package/gallery/fixtures/org-teams.dgmo +25 -0
  63. package/gallery/fixtures/pie.dgmo +9 -0
  64. package/gallery/fixtures/polar-area.dgmo +8 -0
  65. package/gallery/fixtures/quadrant.dgmo +18 -0
  66. package/gallery/fixtures/radar.dgmo +8 -0
  67. package/gallery/fixtures/sankey.dgmo +31 -0
  68. package/gallery/fixtures/scatter.dgmo +21 -0
  69. package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
  70. package/gallery/fixtures/sequence-tags.dgmo +41 -0
  71. package/gallery/fixtures/sequence.dgmo +35 -0
  72. package/gallery/fixtures/sitemap-basic.dgmo +12 -0
  73. package/gallery/fixtures/sitemap-full.dgmo +156 -0
  74. package/gallery/fixtures/slope.dgmo +9 -0
  75. package/gallery/fixtures/spr-eras.dgmo +62 -0
  76. package/gallery/fixtures/state.dgmo +30 -0
  77. package/gallery/fixtures/timeline-intraday.dgmo +14 -0
  78. package/gallery/fixtures/timeline.dgmo +32 -0
  79. package/gallery/fixtures/venn.dgmo +10 -0
  80. package/gallery/fixtures/wordcloud.dgmo +24 -0
  81. package/package.json +71 -2
  82. package/src/c4/layout.ts +372 -90
  83. package/src/c4/parser.ts +100 -55
  84. package/src/chart.ts +91 -28
  85. package/src/class/parser.ts +41 -12
  86. package/src/cli.ts +211 -62
  87. package/src/completion.ts +378 -183
  88. package/src/d3.ts +1044 -303
  89. package/src/dgmo-mermaid.ts +16 -13
  90. package/src/dgmo-router.ts +69 -23
  91. package/src/echarts.ts +646 -153
  92. package/src/editor/dgmo.grammar +69 -0
  93. package/src/editor/dgmo.grammar.d.ts +2 -0
  94. package/src/editor/dgmo.grammar.js +18 -0
  95. package/src/editor/dgmo.grammar.terms.d.ts +5 -0
  96. package/src/editor/dgmo.grammar.terms.js +35 -0
  97. package/src/editor/highlight-api.ts +444 -0
  98. package/src/editor/highlight.ts +36 -0
  99. package/src/editor/index.ts +28 -0
  100. package/src/editor/keywords.ts +222 -0
  101. package/src/editor/tokens.ts +30 -0
  102. package/src/er/parser.ts +48 -14
  103. package/src/er/renderer.ts +112 -53
  104. package/src/gantt/calculator.ts +91 -29
  105. package/src/gantt/parser.ts +197 -71
  106. package/src/gantt/renderer.ts +1120 -350
  107. package/src/graph/flowchart-parser.ts +46 -25
  108. package/src/graph/state-parser.ts +47 -17
  109. package/src/index.ts +96 -31
  110. package/src/infra/parser.ts +157 -53
  111. package/src/infra/renderer.ts +723 -271
  112. package/src/initiative-status/parser.ts +138 -44
  113. package/src/kanban/parser.ts +25 -14
  114. package/src/org/layout.ts +111 -44
  115. package/src/org/parser.ts +69 -22
  116. package/src/palettes/index.ts +3 -2
  117. package/src/sequence/parser.ts +193 -61
  118. package/src/sitemap/parser.ts +65 -29
  119. package/src/utils/arrows.ts +2 -22
  120. package/src/utils/duration.ts +39 -21
  121. package/src/utils/legend-constants.ts +0 -2
  122. package/src/utils/parsing.ts +75 -31
@@ -7,7 +7,11 @@
7
7
  // and connections, [Group] containers, tag groups, pipe metadata.
8
8
 
9
9
  import { makeDgmoError, formatDgmoError, suggest } from '../diagnostics';
10
- import { measureIndent, parseFirstLine, OPTION_NOCOLON_RE } from '../utils/parsing';
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,21 +25,17 @@ import { INFRA_BEHAVIOR_KEYS, EDGE_ONLY_KEYS } from './types';
21
25
  // Regex patterns
22
26
  // ============================================================
23
27
 
24
- // Connection: -label-> Target or -> Target (with optional | split: N% or pipe metadata)
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 (with optional | split: N% or pipe metadata)
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*$/;
@@ -68,8 +68,12 @@ const EDGE_NODE_NAMES = new Set(['edge', 'internet']);
68
68
 
69
69
  // Known top-level option keys (space-separated, no colon)
70
70
  const TOP_LEVEL_OPTIONS = new Set([
71
- 'slo-availability', 'slo-p90-latency-ms', 'slo-warning-margin',
72
- 'default-latency-ms', 'default-uptime', 'default-rps',
71
+ 'slo-availability',
72
+ 'slo-p90-latency-ms',
73
+ 'slo-warning-margin',
74
+ 'default-latency-ms',
75
+ 'default-uptime',
76
+ 'default-rps',
73
77
  ]);
74
78
 
75
79
  // ============================================================
@@ -94,9 +98,10 @@ function parsePropertyValue(raw: string): string | number {
94
98
  return raw.trim();
95
99
  }
96
100
 
97
- function extractPipeMetadata(
98
- rest: string,
99
- ): { tags: Record<string, string>; clean: string } {
101
+ function extractPipeMetadata(rest: string): {
102
+ tags: Record<string, string>;
103
+ clean: string;
104
+ } {
100
105
  const tags: Record<string, string> = {};
101
106
  let clean = rest;
102
107
  let match: RegExpExecArray | null;
@@ -108,6 +113,30 @@ function extractPipeMetadata(
108
113
  return { tags, clean: clean.trim() };
109
114
  }
110
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
+
111
140
  // ============================================================
112
141
  // Parser
113
142
  // ============================================================
@@ -150,20 +179,26 @@ export function parseInfra(content: string): ParsedInfra {
150
179
  if (currentNode && !nodeMap.has(currentNode.id)) {
151
180
  // Validate mutual exclusion: concurrency vs instances/max-rps
152
181
  const keys = new Set(currentNode.properties.map((p) => p.key));
153
- if (keys.has('concurrency') && (keys.has('instances') || keys.has('max-rps'))) {
154
- const conflicting = [keys.has('instances') ? 'instances' : '', keys.has('max-rps') ? 'max-rps' : '']
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
+ ]
155
190
  .filter(Boolean)
156
191
  .join(', ');
157
192
  warn(
158
193
  currentNode.lineNumber,
159
- `'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.`
160
195
  );
161
196
  }
162
197
  // Validate mutual exclusion: buffer (queue) vs max-rps (service)
163
198
  if (keys.has('buffer') && keys.has('max-rps')) {
164
199
  warn(
165
200
  currentNode.lineNumber,
166
- `'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.`
167
202
  );
168
203
  }
169
204
  nodeMap.set(currentNode.id, currentNode);
@@ -202,7 +237,10 @@ export function parseInfra(content: string): ParsedInfra {
202
237
  const firstLineResult = parseFirstLine(trimmed);
203
238
  if (firstLineResult) {
204
239
  if (firstLineResult.chartType !== 'infra') {
205
- setError(lineNumber, `Expected chart type 'infra', got '${firstLineResult.chartType}'`);
240
+ setError(
241
+ lineNumber,
242
+ `Expected chart type 'infra', got '${firstLineResult.chartType}'`
243
+ );
206
244
  }
207
245
  if (firstLineResult.title) {
208
246
  result.title = firstLineResult.title;
@@ -255,11 +293,16 @@ export function parseInfra(content: string): ParsedInfra {
255
293
  finishCurrentTagGroup();
256
294
  const gLabel = groupMatch[1].trim();
257
295
  const gId = groupId(gLabel);
258
- const groupMeta = groupMatch[2] ? extractPipeMetadata('|' + groupMatch[2]).tags : undefined;
296
+ const groupMeta = groupMatch[2]
297
+ ? extractPipeMetadata('|' + groupMatch[2]).tags
298
+ : undefined;
259
299
  currentGroup = {
260
300
  id: gId,
261
301
  label: gLabel,
262
- metadata: groupMeta && Object.keys(groupMeta).length > 0 ? groupMeta : undefined,
302
+ metadata:
303
+ groupMeta && Object.keys(groupMeta).length > 0
304
+ ? groupMeta
305
+ : undefined,
263
306
  lineNumber,
264
307
  };
265
308
  result.groups.push(currentGroup);
@@ -310,6 +353,11 @@ export function parseInfra(content: string): ParsedInfra {
310
353
  }
311
354
  continue;
312
355
  }
356
+ warn(
357
+ lineNumber,
358
+ `Invalid tag value '${trimmed}' in tag group '${currentTagGroup.name}'.`
359
+ );
360
+ continue;
313
361
  }
314
362
 
315
363
  // Inside a [Group] but no current node — group properties or component declaration
@@ -333,6 +381,8 @@ export function parseInfra(content: string): ParsedInfra {
333
381
  currentGroup.collapsed = val.toLowerCase() === 'true';
334
382
  continue;
335
383
  }
384
+ // Fall through to component matching — could be a component name
385
+ // that happens to match PROPERTY_RE (e.g., "MyService v2")
336
386
  }
337
387
 
338
388
  const compMatch = trimmed.match(COMPONENT_RE);
@@ -365,9 +415,17 @@ export function parseInfra(content: string): ParsedInfra {
365
415
  if (currentNode && indent > baseIndent) {
366
416
  // Detect deprecated xN fanout syntax
367
417
  const deprecatedFanout = trimmed.match(DEPRECATED_FANOUT_RE);
368
- if (deprecatedFanout && (trimmed.startsWith('->') || trimmed.startsWith('-') || trimmed.startsWith('~'))) {
418
+ if (
419
+ deprecatedFanout &&
420
+ (trimmed.startsWith('->') ||
421
+ trimmed.startsWith('-') ||
422
+ trimmed.startsWith('~'))
423
+ ) {
369
424
  const n = deprecatedFanout[1];
370
- setError(lineNumber, `'x${n}' fanout syntax is no longer supported — use '| fanout: ${n}' instead`);
425
+ setError(
426
+ lineNumber,
427
+ `'x${n}' fanout syntax is no longer supported — use '| fanout: ${n}' instead`
428
+ );
371
429
  continue;
372
430
  }
373
431
 
@@ -375,14 +433,20 @@ export function parseInfra(content: string): ParsedInfra {
375
433
  const asyncSimpleConn = trimmed.match(ASYNC_SIMPLE_CONNECTION_RE);
376
434
  if (asyncSimpleConn) {
377
435
  const targetRaw = asyncSimpleConn[1].trim();
378
- const splitStr = asyncSimpleConn[2];
379
436
  const pipeMeta = extractPipeMetadata(targetRaw);
380
437
  const targetName = pipeMeta.clean || targetRaw;
381
- const split = splitStr ? parseFloat(splitStr)
382
- : pipeMeta.tags.split ? parseFloat(pipeMeta.tags.split) : null;
383
- const fanoutRaw = pipeMeta.tags.fanout ? parseInt(pipeMeta.tags.fanout, 10) : null;
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;
384
445
  if (fanoutRaw !== null && fanoutRaw < 1) {
385
- warn(lineNumber, `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`);
446
+ warn(
447
+ lineNumber,
448
+ `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
449
+ );
386
450
  }
387
451
  const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
388
452
  result.edges.push({
@@ -402,14 +466,20 @@ export function parseInfra(content: string): ParsedInfra {
402
466
  if (asyncConnMatch) {
403
467
  const label = asyncConnMatch[1]?.trim() || '';
404
468
  const targetRaw = asyncConnMatch[2].trim();
405
- const splitStr = asyncConnMatch[3];
406
469
  const pipeMeta = extractPipeMetadata(targetRaw);
407
470
  const targetName = pipeMeta.clean || targetRaw;
408
- const split = splitStr ? parseFloat(splitStr)
409
- : pipeMeta.tags.split ? parseFloat(pipeMeta.tags.split) : null;
410
- const fanoutRaw = pipeMeta.tags.fanout ? parseInt(pipeMeta.tags.fanout, 10) : null;
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;
411
478
  if (fanoutRaw !== null && fanoutRaw < 1) {
412
- warn(lineNumber, `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`);
479
+ warn(
480
+ lineNumber,
481
+ `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
482
+ );
413
483
  }
414
484
  const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
415
485
 
@@ -437,15 +507,20 @@ export function parseInfra(content: string): ParsedInfra {
437
507
  const simpleConn = trimmed.match(SIMPLE_CONNECTION_RE);
438
508
  if (simpleConn) {
439
509
  const targetRaw = simpleConn[1].trim();
440
- const splitStr = simpleConn[2];
441
- // Parse pipe metadata for fanout/split (and clean target name)
442
510
  const pipeMeta = extractPipeMetadata(targetRaw);
443
511
  const targetName = pipeMeta.clean || targetRaw;
444
- const split = splitStr ? parseFloat(splitStr)
445
- : pipeMeta.tags.split ? parseFloat(pipeMeta.tags.split) : null;
446
- const fanoutRaw = pipeMeta.tags.fanout ? parseInt(pipeMeta.tags.fanout, 10) : null;
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;
447
519
  if (fanoutRaw !== null && fanoutRaw < 1) {
448
- warn(lineNumber, `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`);
520
+ warn(
521
+ lineNumber,
522
+ `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
523
+ );
449
524
  }
450
525
  const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
451
526
  result.edges.push({
@@ -465,15 +540,20 @@ export function parseInfra(content: string): ParsedInfra {
465
540
  if (connMatch) {
466
541
  const label = connMatch[1]?.trim() || '';
467
542
  const targetRaw = connMatch[2].trim();
468
- const splitStr = connMatch[3];
469
- // Parse pipe metadata for fanout/split (and clean target name)
470
543
  const pipeMeta = extractPipeMetadata(targetRaw);
471
544
  const targetName = pipeMeta.clean || targetRaw;
472
- const split = splitStr ? parseFloat(splitStr)
473
- : pipeMeta.tags.split ? parseFloat(pipeMeta.tags.split) : null;
474
- const fanoutRaw = pipeMeta.tags.fanout ? parseInt(pipeMeta.tags.fanout, 10) : null;
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;
475
552
  if (fanoutRaw !== null && fanoutRaw < 1) {
476
- warn(lineNumber, `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`);
553
+ warn(
554
+ lineNumber,
555
+ `Fan-out multiplier must be at least 1 (got fanout: ${fanoutRaw}). Ignoring.`
556
+ );
477
557
  }
478
558
  const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
479
559
 
@@ -525,7 +605,10 @@ export function parseInfra(content: string): ParsedInfra {
525
605
 
526
606
  // Validate edge-only keys
527
607
  if (EDGE_ONLY_KEYS.has(key) && !currentNode.isEdge) {
528
- warn(lineNumber, `Property '${key}' is only valid on the entry point (Edge/Internet).`);
608
+ warn(
609
+ lineNumber,
610
+ `Property '${key}' is only valid on the entry point (Edge/Internet).`
611
+ );
529
612
  }
530
613
 
531
614
  const value = parsePropertyValue(rawVal);
@@ -534,7 +617,10 @@ export function parseInfra(content: string): ParsedInfra {
534
617
  }
535
618
 
536
619
  // Unknown indented line
537
- warn(lineNumber, `Unexpected line inside component '${currentNode.label}'.`);
620
+ warn(
621
+ lineNumber,
622
+ `Unexpected line inside component '${currentNode.label}'.`
623
+ );
538
624
  continue;
539
625
  }
540
626
 
@@ -592,6 +678,9 @@ export function parseInfra(content: string): ParsedInfra {
592
678
  continue;
593
679
  }
594
680
  }
681
+
682
+ // Catch-all: nothing matched this line
683
+ warn(lineNumber, `Unexpected line: '${trimmed}'.`);
595
684
  }
596
685
 
597
686
  // Flush last open blocks
@@ -661,7 +750,8 @@ export function extractSymbols(docText: string): DiagramSymbols {
661
750
  // Recognize new-style bare options (`key value`) and old-style (`key: value`)
662
751
  const firstLine = parseFirstLine(line);
663
752
  if (firstLine) continue; // chart type line
664
- if (/^(?:direction-tb|animate|no-animate|slo-|default-)/i.test(line)) continue;
753
+ if (/^(?:direction-tb|animate|no-animate|slo-|default-)/i.test(line))
754
+ continue;
665
755
  if (/^[a-z-]+\s*:/i.test(line)) continue; // legacy colon options
666
756
  inMetadata = false;
667
757
  } else {
@@ -671,8 +761,14 @@ export function extractSymbols(docText: string): DiagramSymbols {
671
761
 
672
762
  if (!indented) {
673
763
  // Root-level: tag group declaration, group header, or component
674
- if (/^tag\s/i.test(line)) { inTagGroup = true; continue; }
675
- if (/^tag\s*:/i.test(line)) { inTagGroup = true; continue; } // legacy
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
676
772
  inTagGroup = false;
677
773
  if (/^\[/.test(line)) continue; // [Group] header
678
774
  const m = COMPONENT_RE.exec(line);
@@ -687,7 +783,15 @@ export function extractSymbols(docText: string): DiagramSymbols {
687
783
  if (/^\w[\w-]*\s*:/.test(line)) continue; // property (key: value) legacy
688
784
  // New-style property: first token is a known behavior/property key
689
785
  const firstToken = line.split(/\s/)[0].toLowerCase();
690
- if ((INFRA_BEHAVIOR_KEYS.has(firstToken) || EDGE_ONLY_KEYS.has(firstToken) || firstToken === 'description' || firstToken === 'instances' || firstToken === 'collapsed') && /\s/.test(line)) continue;
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;
691
795
  const m = COMPONENT_RE.exec(line);
692
796
  if (m && !entities.includes(m[1]!)) entities.push(m[1]!);
693
797
  }