@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.
- package/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +452 -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 +188 -185
- package/dist/editor.cjs +338 -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 +307 -0
- package/dist/editor.js.map +1 -0
- package/dist/highlight.cjs +560 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.cts +32 -0
- package/dist/highlight.d.ts +32 -0
- package/dist/highlight.js +530 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +3467 -1078
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +3466 -1078
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +46 -37
- 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 +9 -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 +71 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +100 -55
- package/src/chart.ts +91 -28
- package/src/class/parser.ts +41 -12
- package/src/cli.ts +211 -62
- package/src/completion.ts +378 -183
- package/src/d3.ts +1044 -303
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +69 -23
- package/src/echarts.ts +646 -153
- 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-api.ts +444 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +222 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +48 -14
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +197 -71
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +46 -25
- package/src/graph/state-parser.ts +47 -17
- package/src/index.ts +96 -31
- package/src/infra/parser.ts +157 -53
- package/src/infra/renderer.ts +723 -271
- package/src/initiative-status/parser.ts +138 -44
- package/src/kanban/parser.ts +25 -14
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +69 -22
- package/src/palettes/index.ts +3 -2
- package/src/sequence/parser.ts +193 -61
- package/src/sitemap/parser.ts +65 -29
- 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 +75 -31
package/src/c4/parser.ts
CHANGED
|
@@ -39,7 +39,8 @@ const ELEMENT_RE = /^(person|system|container|component)\s+(.+)$/i;
|
|
|
39
39
|
const IS_A_RE = /\s+is\s+a(?:n)?\s+(\w+)\s*$/i;
|
|
40
40
|
|
|
41
41
|
/** Matches `Name is a <type>` declarations (new preferred syntax) */
|
|
42
|
-
const C4_IS_A_RE =
|
|
42
|
+
const C4_IS_A_RE =
|
|
43
|
+
/^([^:]+?)\s+is\s+an?\s+(person|system|container|component|external|database)\b(.*)$/i;
|
|
43
44
|
|
|
44
45
|
/** Matches relationship arrows: `->`, `~>`, `<->`, `<~>` */
|
|
45
46
|
const RELATIONSHIP_RE = /^(<?-?>|<?~?>)\s*(.+)$/;
|
|
@@ -63,7 +64,6 @@ const METADATA_RE = /^([a-z][a-z0-9-]*):\s+(.+)$/i;
|
|
|
63
64
|
// Helpers
|
|
64
65
|
// ============================================================
|
|
65
66
|
|
|
66
|
-
|
|
67
67
|
const VALID_ELEMENT_TYPES = new Set<string>([
|
|
68
68
|
'person',
|
|
69
69
|
'system',
|
|
@@ -81,14 +81,10 @@ const VALID_SHAPES = new Set<string>([
|
|
|
81
81
|
]);
|
|
82
82
|
|
|
83
83
|
/** Known top-level option keys for C4 diagrams. */
|
|
84
|
-
const KNOWN_C4_OPTIONS = new Set<string>([
|
|
85
|
-
'layout',
|
|
86
|
-
]);
|
|
84
|
+
const KNOWN_C4_OPTIONS = new Set<string>(['layout']);
|
|
87
85
|
|
|
88
86
|
/** Known C4 boolean options (bare keyword = on). */
|
|
89
|
-
const KNOWN_C4_BOOLEANS = new Set<string>([
|
|
90
|
-
'direction-tb',
|
|
91
|
-
]);
|
|
87
|
+
const KNOWN_C4_BOOLEANS = new Set<string>(['direction-tb']);
|
|
92
88
|
|
|
93
89
|
const ALL_CHART_TYPES = [
|
|
94
90
|
'c4',
|
|
@@ -110,9 +106,7 @@ const ALL_CHART_TYPES = [
|
|
|
110
106
|
];
|
|
111
107
|
|
|
112
108
|
/** Map from ParticipantType inference → C4Shape */
|
|
113
|
-
function participantTypeToC4Shape(
|
|
114
|
-
pType: string,
|
|
115
|
-
): C4Shape {
|
|
109
|
+
function participantTypeToC4Shape(pType: string): C4Shape {
|
|
116
110
|
switch (pType) {
|
|
117
111
|
case 'database':
|
|
118
112
|
return 'database';
|
|
@@ -155,7 +149,6 @@ function parseArrowType(arrow: string): C4ArrowType | null {
|
|
|
155
149
|
}
|
|
156
150
|
}
|
|
157
151
|
|
|
158
|
-
|
|
159
152
|
// ============================================================
|
|
160
153
|
// Stack entry types
|
|
161
154
|
// ============================================================
|
|
@@ -196,10 +189,7 @@ type StackEntry =
|
|
|
196
189
|
// Parser
|
|
197
190
|
// ============================================================
|
|
198
191
|
|
|
199
|
-
export function parseC4(
|
|
200
|
-
content: string,
|
|
201
|
-
palette?: PaletteColors,
|
|
202
|
-
): ParsedC4 {
|
|
192
|
+
export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
|
|
203
193
|
const result: ParsedC4 = {
|
|
204
194
|
title: null,
|
|
205
195
|
titleLineNumber: null,
|
|
@@ -212,10 +202,15 @@ export function parseC4(
|
|
|
212
202
|
error: null,
|
|
213
203
|
};
|
|
214
204
|
|
|
215
|
-
const pushError = (
|
|
205
|
+
const pushError = (
|
|
206
|
+
line: number,
|
|
207
|
+
message: string,
|
|
208
|
+
severity: 'error' | 'warning' = 'error'
|
|
209
|
+
): void => {
|
|
216
210
|
const diag = makeDgmoError(line, message, severity);
|
|
217
211
|
result.diagnostics.push(diag);
|
|
218
|
-
if (!result.error && severity === 'error')
|
|
212
|
+
if (!result.error && severity === 'error')
|
|
213
|
+
result.error = formatDgmoError(diag);
|
|
219
214
|
};
|
|
220
215
|
|
|
221
216
|
const fail = (line: number, message: string): ParsedC4 => {
|
|
@@ -297,7 +292,10 @@ export function parseC4(
|
|
|
297
292
|
lineNumber,
|
|
298
293
|
};
|
|
299
294
|
if (tagBlockMatch.alias) {
|
|
300
|
-
aliasMap.set(
|
|
295
|
+
aliasMap.set(
|
|
296
|
+
tagBlockMatch.alias.toLowerCase(),
|
|
297
|
+
tagBlockMatch.name.toLowerCase()
|
|
298
|
+
);
|
|
301
299
|
}
|
|
302
300
|
result.tagGroups.push(currentTagGroup);
|
|
303
301
|
continue;
|
|
@@ -329,7 +327,7 @@ export function parseC4(
|
|
|
329
327
|
if (!color) {
|
|
330
328
|
pushError(
|
|
331
329
|
lineNumber,
|
|
332
|
-
`Expected 'Value(color)' in tag group '${currentTagGroup.name}'
|
|
330
|
+
`Expected 'Value(color)' in tag group '${currentTagGroup.name}'`
|
|
333
331
|
);
|
|
334
332
|
continue;
|
|
335
333
|
}
|
|
@@ -344,7 +342,7 @@ export function parseC4(
|
|
|
344
342
|
});
|
|
345
343
|
continue;
|
|
346
344
|
}
|
|
347
|
-
currentTagGroup = null;
|
|
345
|
+
currentTagGroup = null; // eslint-disable-line no-useless-assignment
|
|
348
346
|
}
|
|
349
347
|
|
|
350
348
|
// --- Content phase ---
|
|
@@ -367,7 +365,10 @@ export function parseC4(
|
|
|
367
365
|
}
|
|
368
366
|
|
|
369
367
|
// Check for top-level non-deployment content (section ended)
|
|
370
|
-
if (
|
|
368
|
+
if (
|
|
369
|
+
indent === 0 &&
|
|
370
|
+
(C4_IS_A_RE.test(trimmed) || ELEMENT_RE.test(trimmed))
|
|
371
|
+
) {
|
|
371
372
|
inDeployment = false;
|
|
372
373
|
// Fall through to element parsing below
|
|
373
374
|
} else {
|
|
@@ -377,10 +378,13 @@ export function parseC4(
|
|
|
377
378
|
const refName = refMatch[1].trim();
|
|
378
379
|
if (deployStack.length > 0) {
|
|
379
380
|
deployStack[deployStack.length - 1].node.containerRefs.push(
|
|
380
|
-
refName
|
|
381
|
+
refName
|
|
381
382
|
);
|
|
382
383
|
} else {
|
|
383
|
-
pushError(
|
|
384
|
+
pushError(
|
|
385
|
+
lineNumber,
|
|
386
|
+
`"container ${refName}" must be inside a deployment node`
|
|
387
|
+
);
|
|
384
388
|
}
|
|
385
389
|
continue;
|
|
386
390
|
}
|
|
@@ -388,8 +392,13 @@ export function parseC4(
|
|
|
388
392
|
// Otherwise it's a deployment node (possibly with pipe metadata)
|
|
389
393
|
const segments = trimmed.split('|').map((s) => s.trim());
|
|
390
394
|
const nodeName = segments[0];
|
|
391
|
-
const metadata = parsePipeMetadata(segments, aliasMap, () =>
|
|
392
|
-
|
|
395
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () =>
|
|
396
|
+
pushError(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
397
|
+
);
|
|
398
|
+
const shape = inferC4Shape(
|
|
399
|
+
nodeName,
|
|
400
|
+
metadata.tech ?? metadata.technology
|
|
401
|
+
);
|
|
393
402
|
|
|
394
403
|
const dNode: C4DeploymentNode = {
|
|
395
404
|
name: nodeName,
|
|
@@ -423,8 +432,9 @@ export function parseC4(
|
|
|
423
432
|
// containers / components must be inside an element
|
|
424
433
|
const parentEntry = findParentElement(indent, stack);
|
|
425
434
|
if (parentEntry) {
|
|
426
|
-
parentEntry.element.sectionHeader =
|
|
427
|
-
|
|
435
|
+
parentEntry.element.sectionHeader = sectionType as
|
|
436
|
+
| 'containers'
|
|
437
|
+
| 'components';
|
|
428
438
|
parentEntry.element.sectionHeaderLineNumber = lineNumber;
|
|
429
439
|
stack.push({
|
|
430
440
|
kind: 'section',
|
|
@@ -433,10 +443,7 @@ export function parseC4(
|
|
|
433
443
|
indent,
|
|
434
444
|
});
|
|
435
445
|
} else {
|
|
436
|
-
pushError(
|
|
437
|
-
lineNumber,
|
|
438
|
-
`"${sectionType}" must be inside an element`,
|
|
439
|
-
);
|
|
446
|
+
pushError(lineNumber, `"${sectionType}" must be inside an element`);
|
|
440
447
|
}
|
|
441
448
|
continue;
|
|
442
449
|
}
|
|
@@ -493,9 +500,15 @@ export function parseC4(
|
|
|
493
500
|
if (!rawLabel) break; // empty label — fall through to plain arrow
|
|
494
501
|
|
|
495
502
|
// Reject bidirectional arrows
|
|
496
|
-
if (
|
|
503
|
+
if (
|
|
504
|
+
arrowType === 'bidirectional' ||
|
|
505
|
+
arrowType === 'bidirectional-async'
|
|
506
|
+
) {
|
|
497
507
|
const source = findParentElement(indent, stack)?.element.name ?? '?';
|
|
498
|
-
pushError(
|
|
508
|
+
pushError(
|
|
509
|
+
lineNumber,
|
|
510
|
+
`Bidirectional arrows are no longer supported. Replace with two separate arrows:\n -${rawLabel}-> ${targetBody}\n ${targetBody} -${rawLabel}-> ${source}`
|
|
511
|
+
);
|
|
499
512
|
labeledHandled = true;
|
|
500
513
|
break;
|
|
501
514
|
}
|
|
@@ -552,11 +565,17 @@ export function parseC4(
|
|
|
552
565
|
const arrowType = parseArrowType(relMatch[1]);
|
|
553
566
|
if (arrowType) {
|
|
554
567
|
// Reject bidirectional arrows
|
|
555
|
-
if (
|
|
568
|
+
if (
|
|
569
|
+
arrowType === 'bidirectional' ||
|
|
570
|
+
arrowType === 'bidirectional-async'
|
|
571
|
+
) {
|
|
556
572
|
const arrow = relMatch[1];
|
|
557
573
|
const target = relMatch[2].trim();
|
|
558
574
|
const source = findParentElement(indent, stack)?.element.name ?? '?';
|
|
559
|
-
pushError(
|
|
575
|
+
pushError(
|
|
576
|
+
lineNumber,
|
|
577
|
+
`'${arrow}' bidirectional arrows are no longer supported. Replace with two separate arrows:\n -> ${target}\n ${target} -> ${source}`
|
|
578
|
+
);
|
|
560
579
|
continue;
|
|
561
580
|
}
|
|
562
581
|
|
|
@@ -605,14 +624,22 @@ export function parseC4(
|
|
|
605
624
|
let segments: string[];
|
|
606
625
|
if (remainderTrimmed.startsWith('|')) {
|
|
607
626
|
// remainder has pipe metadata: "| tech: PostgreSQL, team: Data"
|
|
608
|
-
segments = [
|
|
627
|
+
segments = [
|
|
628
|
+
'',
|
|
629
|
+
...remainderTrimmed
|
|
630
|
+
.substring(1)
|
|
631
|
+
.split('|')
|
|
632
|
+
.map((s) => s.trim()),
|
|
633
|
+
];
|
|
609
634
|
} else {
|
|
610
635
|
segments = [remainderTrimmed];
|
|
611
636
|
}
|
|
612
637
|
|
|
613
638
|
// Check for additional `is a <shape>` in the name (e.g., already stripped by C4_IS_A_RE won't happen,
|
|
614
639
|
// but handle remainder like "is a cylinder" after type)
|
|
615
|
-
const remainderIsA = remainderTrimmed.match(
|
|
640
|
+
const remainderIsA = remainderTrimmed.match(
|
|
641
|
+
/^\s*is\s+a(?:n)?\s+(\w+)\s*(.*)$/i
|
|
642
|
+
);
|
|
616
643
|
if (remainderIsA) {
|
|
617
644
|
const shapeName = remainderIsA[1].toLowerCase();
|
|
618
645
|
if (VALID_SHAPES.has(shapeName)) {
|
|
@@ -620,13 +647,19 @@ export function parseC4(
|
|
|
620
647
|
} else {
|
|
621
648
|
pushError(
|
|
622
649
|
lineNumber,
|
|
623
|
-
`Unknown shape "${remainderIsA[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}
|
|
650
|
+
`Unknown shape "${remainderIsA[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
|
|
624
651
|
);
|
|
625
652
|
}
|
|
626
653
|
// Re-parse remainder after shape
|
|
627
654
|
const afterShape = remainderIsA[2].trim();
|
|
628
655
|
if (afterShape.startsWith('|')) {
|
|
629
|
-
segments = [
|
|
656
|
+
segments = [
|
|
657
|
+
'',
|
|
658
|
+
...afterShape
|
|
659
|
+
.substring(1)
|
|
660
|
+
.split('|')
|
|
661
|
+
.map((s) => s.trim()),
|
|
662
|
+
];
|
|
630
663
|
} else {
|
|
631
664
|
segments = [afterShape];
|
|
632
665
|
}
|
|
@@ -641,13 +674,15 @@ export function parseC4(
|
|
|
641
674
|
} else {
|
|
642
675
|
pushError(
|
|
643
676
|
lineNumber,
|
|
644
|
-
`Unknown shape "${nameIsAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}
|
|
677
|
+
`Unknown shape "${nameIsAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
|
|
645
678
|
);
|
|
646
679
|
}
|
|
647
680
|
namePart = namePart.substring(0, nameIsAMatch.index!).trim();
|
|
648
681
|
}
|
|
649
682
|
|
|
650
|
-
const metadata = parsePipeMetadata(segments, aliasMap, () =>
|
|
683
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () =>
|
|
684
|
+
pushError(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
685
|
+
);
|
|
651
686
|
|
|
652
687
|
const shape =
|
|
653
688
|
explicitShape ??
|
|
@@ -669,7 +704,7 @@ export function parseC4(
|
|
|
669
704
|
if (existingLine !== undefined) {
|
|
670
705
|
pushError(
|
|
671
706
|
lineNumber,
|
|
672
|
-
`Duplicate element name "${namePart}" (first defined on line ${existingLine})
|
|
707
|
+
`Duplicate element name "${namePart}" (first defined on line ${existingLine})`
|
|
673
708
|
);
|
|
674
709
|
} else {
|
|
675
710
|
knownNames.set(namePart.toLowerCase(), lineNumber);
|
|
@@ -683,7 +718,7 @@ export function parseC4(
|
|
|
683
718
|
const elementMatch = trimmed.match(ELEMENT_RE);
|
|
684
719
|
if (elementMatch) {
|
|
685
720
|
const elementType = elementMatch[1].toLowerCase() as C4ElementType;
|
|
686
|
-
|
|
721
|
+
const nameAndRest = elementMatch[2];
|
|
687
722
|
|
|
688
723
|
// Split on pipe for inline metadata
|
|
689
724
|
const segments = nameAndRest.split('|').map((s) => s.trim());
|
|
@@ -699,7 +734,7 @@ export function parseC4(
|
|
|
699
734
|
} else {
|
|
700
735
|
pushError(
|
|
701
736
|
lineNumber,
|
|
702
|
-
`Unknown shape "${isAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}
|
|
737
|
+
`Unknown shape "${isAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`
|
|
703
738
|
);
|
|
704
739
|
}
|
|
705
740
|
namePart = namePart.substring(0, isAMatch.index!).trim();
|
|
@@ -708,10 +743,12 @@ export function parseC4(
|
|
|
708
743
|
// Emit deprecation error with migration hint
|
|
709
744
|
pushError(
|
|
710
745
|
lineNumber,
|
|
711
|
-
`'${elementMatch[1]} ${namePart}' prefix syntax is no longer supported — use '${namePart} is a ${elementType}' instead
|
|
746
|
+
`'${elementMatch[1]} ${namePart}' prefix syntax is no longer supported — use '${namePart} is a ${elementType}' instead`
|
|
712
747
|
);
|
|
713
748
|
|
|
714
|
-
const metadata = parsePipeMetadata(segments, aliasMap, () =>
|
|
749
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () =>
|
|
750
|
+
pushError(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
751
|
+
);
|
|
715
752
|
|
|
716
753
|
// Determine shape: explicit > inference
|
|
717
754
|
const shape =
|
|
@@ -734,7 +771,7 @@ export function parseC4(
|
|
|
734
771
|
if (existingLine !== undefined) {
|
|
735
772
|
pushError(
|
|
736
773
|
lineNumber,
|
|
737
|
-
`Duplicate element name "${namePart}" (first defined on line ${existingLine})
|
|
774
|
+
`Duplicate element name "${namePart}" (first defined on line ${existingLine})`
|
|
738
775
|
);
|
|
739
776
|
} else {
|
|
740
777
|
knownNames.set(namePart.toLowerCase(), lineNumber);
|
|
@@ -798,7 +835,7 @@ export function parseC4(
|
|
|
798
835
|
/** Find the nearest parent element entry on the stack at shallower indent. */
|
|
799
836
|
function findParentElement(
|
|
800
837
|
indent: number,
|
|
801
|
-
stack: StackEntry[]
|
|
838
|
+
stack: StackEntry[]
|
|
802
839
|
): ElementStackEntry | null {
|
|
803
840
|
for (let i = stack.length - 1; i >= 0; i--) {
|
|
804
841
|
const entry = stack[i];
|
|
@@ -824,7 +861,7 @@ function attachElement(
|
|
|
824
861
|
element: C4Element,
|
|
825
862
|
indent: number,
|
|
826
863
|
stack: StackEntry[],
|
|
827
|
-
result: ParsedC4
|
|
864
|
+
result: ParsedC4
|
|
828
865
|
): void {
|
|
829
866
|
// Find the immediate context: group, section, or parent element
|
|
830
867
|
let attached = false;
|
|
@@ -866,7 +903,11 @@ function attachElement(
|
|
|
866
903
|
function validateRelationshipTargets(
|
|
867
904
|
result: ParsedC4,
|
|
868
905
|
knownNames: Map<string, number>,
|
|
869
|
-
pushWarning: (
|
|
906
|
+
pushWarning: (
|
|
907
|
+
line: number,
|
|
908
|
+
message: string,
|
|
909
|
+
severity?: 'error' | 'warning'
|
|
910
|
+
) => void
|
|
870
911
|
): void {
|
|
871
912
|
function walkRels(elements: C4Element[]) {
|
|
872
913
|
for (const el of elements) {
|
|
@@ -875,7 +916,7 @@ function validateRelationshipTargets(
|
|
|
875
916
|
pushWarning(
|
|
876
917
|
rel.lineNumber,
|
|
877
918
|
`Relationship target "${rel.target}" not found`,
|
|
878
|
-
'warning'
|
|
919
|
+
'warning'
|
|
879
920
|
);
|
|
880
921
|
}
|
|
881
922
|
}
|
|
@@ -893,7 +934,7 @@ function validateRelationshipTargets(
|
|
|
893
934
|
pushWarning(
|
|
894
935
|
rel.lineNumber,
|
|
895
936
|
`Relationship target "${rel.target}" not found`,
|
|
896
|
-
'warning'
|
|
937
|
+
'warning'
|
|
897
938
|
);
|
|
898
939
|
}
|
|
899
940
|
}
|
|
@@ -902,7 +943,11 @@ function validateRelationshipTargets(
|
|
|
902
943
|
function validateDeploymentRefs(
|
|
903
944
|
result: ParsedC4,
|
|
904
945
|
knownNames: Map<string, number>,
|
|
905
|
-
pushWarning: (
|
|
946
|
+
pushWarning: (
|
|
947
|
+
line: number,
|
|
948
|
+
message: string,
|
|
949
|
+
severity?: 'error' | 'warning'
|
|
950
|
+
) => void
|
|
906
951
|
): void {
|
|
907
952
|
function walkDeploy(nodes: C4DeploymentNode[]) {
|
|
908
953
|
for (const node of nodes) {
|
|
@@ -911,7 +956,7 @@ function validateDeploymentRefs(
|
|
|
911
956
|
pushWarning(
|
|
912
957
|
node.lineNumber,
|
|
913
958
|
`Deployment reference "container ${ref}" not found`,
|
|
914
|
-
'warning'
|
|
959
|
+
'warning'
|
|
915
960
|
);
|
|
916
961
|
}
|
|
917
962
|
}
|
package/src/chart.ts
CHANGED
|
@@ -21,9 +21,9 @@ export interface ChartDataPoint {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface ChartEra {
|
|
24
|
-
start: string;
|
|
25
|
-
end: string;
|
|
26
|
-
label: string;
|
|
24
|
+
start: string; // exact category label, e.g. "'77"
|
|
25
|
+
end: string; // exact category label, e.g. "'81"
|
|
26
|
+
label: string; // display name, e.g. "Carter"
|
|
27
27
|
color: string | null; // resolved CSS color, or null → palette default
|
|
28
28
|
lineNumber: number;
|
|
29
29
|
}
|
|
@@ -62,7 +62,11 @@ export interface ParsedChart {
|
|
|
62
62
|
import { resolveColor } from './colors';
|
|
63
63
|
import type { PaletteColors } from './palettes';
|
|
64
64
|
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
65
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
extractColor,
|
|
67
|
+
parseFirstLine,
|
|
68
|
+
parseSeriesNames,
|
|
69
|
+
} from './utils/parsing';
|
|
66
70
|
|
|
67
71
|
// ============================================================
|
|
68
72
|
// Parser
|
|
@@ -85,15 +89,20 @@ const TYPE_ALIASES: Record<string, ChartType> = {
|
|
|
85
89
|
|
|
86
90
|
/** Known option keywords for the simple chart parser. */
|
|
87
91
|
const KNOWN_OPTIONS = new Set([
|
|
88
|
-
'chart',
|
|
89
|
-
'
|
|
92
|
+
'chart',
|
|
93
|
+
'title',
|
|
94
|
+
'series',
|
|
95
|
+
'x-label',
|
|
96
|
+
'y-label',
|
|
97
|
+
'label',
|
|
98
|
+
'no-label-name',
|
|
99
|
+
'no-label-value',
|
|
100
|
+
'no-label-percent',
|
|
90
101
|
'color',
|
|
91
102
|
]);
|
|
92
103
|
|
|
93
104
|
/** Known boolean options for the simple chart parser. */
|
|
94
|
-
const KNOWN_BOOLEANS = new Set([
|
|
95
|
-
'orientation-horizontal',
|
|
96
|
-
]);
|
|
105
|
+
const KNOWN_BOOLEANS = new Set(['orientation-horizontal']);
|
|
97
106
|
|
|
98
107
|
/**
|
|
99
108
|
* Parses the simple chart text format into a structured object.
|
|
@@ -114,7 +123,12 @@ export function parseChart(
|
|
|
114
123
|
): ParsedChart {
|
|
115
124
|
const lines = content.split('\n');
|
|
116
125
|
const parsedEras: ChartEra[] = [];
|
|
117
|
-
const rawEras: {
|
|
126
|
+
const rawEras: {
|
|
127
|
+
start: string;
|
|
128
|
+
afterArrow: string;
|
|
129
|
+
color: string | null;
|
|
130
|
+
lineNumber: number;
|
|
131
|
+
}[] = [];
|
|
118
132
|
const result: ParsedChart = {
|
|
119
133
|
type: 'bar',
|
|
120
134
|
data: [],
|
|
@@ -141,7 +155,12 @@ export function parseChart(
|
|
|
141
155
|
|
|
142
156
|
// Reject legacy ## section headers
|
|
143
157
|
if (/^#{2,}\s+/.test(trimmed)) {
|
|
144
|
-
result.diagnostics.push(
|
|
158
|
+
result.diagnostics.push(
|
|
159
|
+
makeDgmoError(
|
|
160
|
+
lineNumber,
|
|
161
|
+
`'${trimmed}' — ## syntax is no longer supported. Use [Group] containers instead`
|
|
162
|
+
)
|
|
163
|
+
);
|
|
145
164
|
continue;
|
|
146
165
|
}
|
|
147
166
|
|
|
@@ -171,7 +190,11 @@ export function parseChart(
|
|
|
171
190
|
}
|
|
172
191
|
// If the first line is a single word (no spaces, no colon, no numbers),
|
|
173
192
|
// treat it as an unrecognized chart type rather than falling through
|
|
174
|
-
if (
|
|
193
|
+
if (
|
|
194
|
+
!trimmed.includes(' ') &&
|
|
195
|
+
!trimmed.includes(':') &&
|
|
196
|
+
!/\d/.test(trimmed)
|
|
197
|
+
) {
|
|
175
198
|
let msg = `Unsupported chart type: ${trimmed}. Supported types: ${[...VALID_TYPES].join(', ')}.`;
|
|
176
199
|
const hint = suggest(trimmed.toLowerCase(), [...VALID_TYPES]);
|
|
177
200
|
if (hint) msg += ` ${hint}`;
|
|
@@ -181,7 +204,9 @@ export function parseChart(
|
|
|
181
204
|
}
|
|
182
205
|
|
|
183
206
|
// Era line: era Day 1 -> Day 3 Rough Seas (blue) — colon-free
|
|
184
|
-
const eraMatch = trimmed.match(
|
|
207
|
+
const eraMatch = trimmed.match(
|
|
208
|
+
/^era\s+(.+?)\s*->\s*(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
209
|
+
);
|
|
185
210
|
if (eraMatch) {
|
|
186
211
|
// Store start and raw afterArrow — resolved against data labels after parsing
|
|
187
212
|
const afterArrow = eraMatch[2].trim();
|
|
@@ -199,7 +224,9 @@ export function parseChart(
|
|
|
199
224
|
|
|
200
225
|
// Extract first token to check for known options
|
|
201
226
|
const spaceIdx = trimmed.indexOf(' ');
|
|
202
|
-
const firstToken = (
|
|
227
|
+
const firstToken = (
|
|
228
|
+
spaceIdx >= 0 ? trimmed.substring(0, spaceIdx) : trimmed
|
|
229
|
+
).toLowerCase();
|
|
203
230
|
|
|
204
231
|
// Bare boolean options (e.g. orientation-horizontal)
|
|
205
232
|
if (KNOWN_BOOLEANS.has(firstToken) && spaceIdx < 0) {
|
|
@@ -233,13 +260,13 @@ export function parseChart(
|
|
|
233
260
|
continue;
|
|
234
261
|
}
|
|
235
262
|
|
|
236
|
-
if (firstToken === '
|
|
263
|
+
if (firstToken === 'x-label') {
|
|
237
264
|
result.xlabel = value;
|
|
238
265
|
result.xlabelLineNumber = lineNumber;
|
|
239
266
|
continue;
|
|
240
267
|
}
|
|
241
268
|
|
|
242
|
-
if (firstToken === '
|
|
269
|
+
if (firstToken === 'y-label') {
|
|
243
270
|
result.ylabel = value;
|
|
244
271
|
result.ylabelLineNumber = lineNumber;
|
|
245
272
|
continue;
|
|
@@ -264,15 +291,25 @@ export function parseChart(
|
|
|
264
291
|
result.seriesNames = parsed.names;
|
|
265
292
|
result.seriesNameLineNumbers = parsed.nameLineNumbers;
|
|
266
293
|
}
|
|
267
|
-
if (parsed.nameColors.some(Boolean))
|
|
294
|
+
if (parsed.nameColors.some(Boolean))
|
|
295
|
+
result.seriesNameColors = parsed.nameColors;
|
|
268
296
|
continue;
|
|
269
297
|
}
|
|
270
298
|
}
|
|
271
299
|
|
|
272
300
|
// Bare boolean options: no-label-name, no-label-value, no-label-percent
|
|
273
|
-
if (firstToken === 'no-label-name') {
|
|
274
|
-
|
|
275
|
-
|
|
301
|
+
if (firstToken === 'no-label-name') {
|
|
302
|
+
result.noLabelName = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (firstToken === 'no-label-value') {
|
|
306
|
+
result.noLabelValue = true;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (firstToken === 'no-label-percent') {
|
|
310
|
+
result.noLabelPercent = true;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
276
313
|
|
|
277
314
|
// Bare "series" keyword with no value — collect indented names
|
|
278
315
|
if (firstToken === 'series' && spaceIdx === -1) {
|
|
@@ -284,7 +321,8 @@ export function parseChart(
|
|
|
284
321
|
result.seriesNames = parsed.names;
|
|
285
322
|
result.seriesNameLineNumbers = parsed.nameLineNumbers;
|
|
286
323
|
}
|
|
287
|
-
if (parsed.nameColors.some(Boolean))
|
|
324
|
+
if (parsed.nameColors.some(Boolean))
|
|
325
|
+
result.seriesNameColors = parsed.nameColors;
|
|
288
326
|
continue;
|
|
289
327
|
}
|
|
290
328
|
|
|
@@ -295,7 +333,10 @@ export function parseChart(
|
|
|
295
333
|
const multiValue = (result.seriesNames?.length ?? 0) >= 2;
|
|
296
334
|
const dataValues = parseDataRowValues(trimmed, { multiValue });
|
|
297
335
|
if (dataValues) {
|
|
298
|
-
const { label: rawLabel, color: pointColor } = extractColor(
|
|
336
|
+
const { label: rawLabel, color: pointColor } = extractColor(
|
|
337
|
+
dataValues.label,
|
|
338
|
+
palette
|
|
339
|
+
);
|
|
299
340
|
const [first, ...rest] = dataValues.values;
|
|
300
341
|
result.data.push({
|
|
301
342
|
label: rawLabel,
|
|
@@ -304,7 +345,14 @@ export function parseChart(
|
|
|
304
345
|
...(pointColor && { color: pointColor }),
|
|
305
346
|
lineNumber,
|
|
306
347
|
});
|
|
348
|
+
continue;
|
|
307
349
|
}
|
|
350
|
+
|
|
351
|
+
// Catch-all: nothing matched this line
|
|
352
|
+
let msg = `Unexpected line: '${trimmed}'.`;
|
|
353
|
+
const hint = suggest(firstToken, [...KNOWN_OPTIONS, ...KNOWN_BOOLEANS]);
|
|
354
|
+
if (hint) msg += ` ${hint}`;
|
|
355
|
+
result.diagnostics.push(makeDgmoError(lineNumber, msg, 'warning'));
|
|
308
356
|
}
|
|
309
357
|
|
|
310
358
|
// Resolve raw eras against known data labels (longest-prefix match for multi-word labels)
|
|
@@ -329,7 +377,13 @@ export function parseChart(
|
|
|
329
377
|
end = words[0];
|
|
330
378
|
label = words.slice(1).join(' ');
|
|
331
379
|
}
|
|
332
|
-
parsedEras.push({
|
|
380
|
+
parsedEras.push({
|
|
381
|
+
start: raw.start,
|
|
382
|
+
end,
|
|
383
|
+
label,
|
|
384
|
+
color: raw.color,
|
|
385
|
+
lineNumber: raw.lineNumber,
|
|
386
|
+
});
|
|
333
387
|
}
|
|
334
388
|
|
|
335
389
|
// Eras are only valid for line, multi-line (aliased to 'line'), and area chart types
|
|
@@ -353,7 +407,10 @@ export function parseChart(
|
|
|
353
407
|
}
|
|
354
408
|
|
|
355
409
|
if (!result.error && result.type === 'bar-stacked' && !result.seriesNames) {
|
|
356
|
-
setChartError(
|
|
410
|
+
setChartError(
|
|
411
|
+
1,
|
|
412
|
+
'Chart type "bar-stacked" requires multiple series names. Use: series Name1, Name2, Name3'
|
|
413
|
+
);
|
|
357
414
|
}
|
|
358
415
|
|
|
359
416
|
if (!result.error && result.seriesNames) {
|
|
@@ -361,7 +418,10 @@ export function parseChart(
|
|
|
361
418
|
for (const dp of result.data) {
|
|
362
419
|
const actualCount = 1 + (dp.extraValues?.length ?? 0);
|
|
363
420
|
if (actualCount !== expectedCount) {
|
|
364
|
-
warn(
|
|
421
|
+
warn(
|
|
422
|
+
dp.lineNumber,
|
|
423
|
+
`Data point "${dp.label}" has ${actualCount} value(s), but ${expectedCount} series defined. Each row must have ${expectedCount} values.`
|
|
424
|
+
);
|
|
365
425
|
}
|
|
366
426
|
}
|
|
367
427
|
// Filter out mismatched data points so renderers get clean data
|
|
@@ -395,7 +455,7 @@ export function parseChart(
|
|
|
395
455
|
*/
|
|
396
456
|
export function parseDataRowValues(
|
|
397
457
|
line: string,
|
|
398
|
-
options?: { multiValue?: boolean }
|
|
458
|
+
options?: { multiValue?: boolean }
|
|
399
459
|
): { label: string; values: number[] } | null {
|
|
400
460
|
// First, normalize comma-grouped numbers: replace patterns like "1,087" with "1087"
|
|
401
461
|
// We need to be careful: commas also separate multi-values.
|
|
@@ -423,7 +483,6 @@ export function parseDataRowValues(
|
|
|
423
483
|
const prevMatch = prevSeg.match(/(\d{1,3})$/);
|
|
424
484
|
if (prevMatch) {
|
|
425
485
|
// Tentatively merge and validate
|
|
426
|
-
const mergedTail = prevMatch[1] + ',' + seg;
|
|
427
486
|
// Build full token by looking at what's left in normalized
|
|
428
487
|
// Simple approach: just merge
|
|
429
488
|
normalized[normalized.length - 1] = prevSeg + seg;
|
|
@@ -465,7 +524,11 @@ export function parseDataRowValues(
|
|
|
465
524
|
const lastSpaceIdx = firstPart.lastIndexOf(' ');
|
|
466
525
|
if (lastSpaceIdx >= 0) {
|
|
467
526
|
const possibleFirstVal = firstPart.substring(lastSpaceIdx + 1).trim();
|
|
468
|
-
if (
|
|
527
|
+
if (
|
|
528
|
+
possibleFirstVal &&
|
|
529
|
+
!isNaN(parseFloat(possibleFirstVal)) &&
|
|
530
|
+
isFinite(Number(possibleFirstVal))
|
|
531
|
+
) {
|
|
469
532
|
const label = firstPart.substring(0, lastSpaceIdx).trim();
|
|
470
533
|
if (label) {
|
|
471
534
|
const values = [parseFloat(possibleFirstVal)];
|