@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/package.json
CHANGED
package/src/c4/layout.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import dagre from '@dagrejs/dagre';
|
|
6
6
|
import type { ParsedC4, C4Element, C4Relationship, C4ArrowType, C4Shape, C4DeploymentNode } from './types';
|
|
7
|
-
import type {
|
|
7
|
+
import type { TagGroup } from '../utils/tag-groups';
|
|
8
|
+
import { LEGEND_PILL_FONT_SIZE, LEGEND_ENTRY_FONT_SIZE, measureLegendText } from '../utils/legend-constants';
|
|
8
9
|
|
|
9
10
|
// ============================================================
|
|
10
11
|
// Types
|
|
@@ -94,12 +95,8 @@ const GROUP_BOUNDARY_PAD = 24;
|
|
|
94
95
|
|
|
95
96
|
// Legend constants (match org)
|
|
96
97
|
const LEGEND_HEIGHT = 28;
|
|
97
|
-
const LEGEND_PILL_FONT_SIZE = 11;
|
|
98
|
-
const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
|
|
99
98
|
const LEGEND_PILL_PAD = 16;
|
|
100
99
|
const LEGEND_DOT_R = 4;
|
|
101
|
-
const LEGEND_ENTRY_FONT_SIZE = 10;
|
|
102
|
-
const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
|
|
103
100
|
const LEGEND_ENTRY_DOT_GAP = 4;
|
|
104
101
|
const LEGEND_ENTRY_TRAIL = 8;
|
|
105
102
|
const LEGEND_CAPSULE_PAD = 4;
|
|
@@ -530,7 +527,7 @@ export function rollUpContextRelationships(parsed: ParsedC4): ContextRelationshi
|
|
|
530
527
|
|
|
531
528
|
function resolveNodeColor(
|
|
532
529
|
el: C4Element,
|
|
533
|
-
tagGroups:
|
|
530
|
+
tagGroups: TagGroup[],
|
|
534
531
|
activeGroupName: string | null,
|
|
535
532
|
ancestors?: C4Element[]
|
|
536
533
|
): string | undefined {
|
|
@@ -661,7 +658,7 @@ export function computeC4NodeDimensions(
|
|
|
661
658
|
// Legend Helpers
|
|
662
659
|
// ============================================================
|
|
663
660
|
|
|
664
|
-
function computeLegendGroups(tagGroups:
|
|
661
|
+
function computeLegendGroups(tagGroups: TagGroup[]): C4LegendGroup[] {
|
|
665
662
|
const result: C4LegendGroup[] = [];
|
|
666
663
|
|
|
667
664
|
for (const group of tagGroups) {
|
|
@@ -672,13 +669,13 @@ function computeLegendGroups(tagGroups: OrgTagGroup[]): C4LegendGroup[] {
|
|
|
672
669
|
if (entries.length === 0) continue;
|
|
673
670
|
|
|
674
671
|
// Compute pill width: group name + entries
|
|
675
|
-
const nameW = group.name
|
|
672
|
+
const nameW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD * 2;
|
|
676
673
|
let capsuleW = LEGEND_CAPSULE_PAD;
|
|
677
674
|
for (const e of entries) {
|
|
678
675
|
capsuleW +=
|
|
679
676
|
LEGEND_DOT_R * 2 +
|
|
680
677
|
LEGEND_ENTRY_DOT_GAP +
|
|
681
|
-
e.value
|
|
678
|
+
measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
682
679
|
LEGEND_ENTRY_TRAIL;
|
|
683
680
|
}
|
|
684
681
|
capsuleW += LEGEND_CAPSULE_PAD;
|
package/src/c4/parser.ts
CHANGED
|
@@ -12,9 +12,8 @@ import {
|
|
|
12
12
|
extractColor,
|
|
13
13
|
parsePipeMetadata,
|
|
14
14
|
MULTIPLE_PIPE_WARNING,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
OPTION_RE,
|
|
15
|
+
parseFirstLine,
|
|
16
|
+
OPTION_NOCOLON_RE,
|
|
18
17
|
} from '../utils/parsing';
|
|
19
18
|
import type {
|
|
20
19
|
ParsedC4,
|
|
@@ -39,23 +38,26 @@ const ELEMENT_RE = /^(person|system|container|component)\s+(.+)$/i;
|
|
|
39
38
|
/** Matches `is a <shape>` in the element name portion */
|
|
40
39
|
const IS_A_RE = /\s+is\s+a(?:n)?\s+(\w+)\s*$/i;
|
|
41
40
|
|
|
41
|
+
/** Matches `Name is a <type>` declarations (new preferred syntax) */
|
|
42
|
+
const C4_IS_A_RE = /^([^:]+?)\s+is\s+an?\s+(person|system|container|component|external|database)\b(.*)$/i;
|
|
43
|
+
|
|
42
44
|
/** Matches relationship arrows: `->`, `~>`, `<->`, `<~>` */
|
|
43
|
-
const RELATIONSHIP_RE = /^(<?-?>|<?~?>)\s
|
|
45
|
+
const RELATIONSHIP_RE = /^(<?-?>|<?~?>)\s*(.+)$/;
|
|
44
46
|
|
|
45
47
|
/** Labeled arrow relationships: -label->, ~label~>, <-label->, <~label~> */
|
|
46
|
-
const C4_LABELED_SYNC_RE = /^-(.+)->\s
|
|
47
|
-
const C4_LABELED_ASYNC_RE = /^~(.+)~>\s
|
|
48
|
-
const C4_LABELED_BIDI_SYNC_RE = /^<-(.+)->\s
|
|
49
|
-
const C4_LABELED_BIDI_ASYNC_RE = /^<~(.+)~>\s
|
|
48
|
+
const C4_LABELED_SYNC_RE = /^-(.+)->\s*(.+)$/;
|
|
49
|
+
const C4_LABELED_ASYNC_RE = /^~(.+)~>\s*(.+)$/;
|
|
50
|
+
const C4_LABELED_BIDI_SYNC_RE = /^<-(.+)->\s*(.+)$/;
|
|
51
|
+
const C4_LABELED_BIDI_ASYNC_RE = /^<~(.+)~>\s*(.+)$/;
|
|
50
52
|
|
|
51
|
-
/** Matches section headers: `containers
|
|
52
|
-
const SECTION_HEADER_RE = /^(containers|components|deployment)\s
|
|
53
|
+
/** Matches section headers: `containers`, `components`, `deployment` (bare keyword) */
|
|
54
|
+
const SECTION_HEADER_RE = /^(containers|components|deployment)\s*$/i;
|
|
53
55
|
|
|
54
56
|
/** Matches `container X` references inside deployment nodes */
|
|
55
57
|
const CONTAINER_REF_RE = /^container\s+(.+)$/i;
|
|
56
58
|
|
|
57
|
-
/** Matches indented metadata: `key
|
|
58
|
-
const METADATA_RE = /^([
|
|
59
|
+
/** Matches indented metadata: `key value` (space-separated, no colon) */
|
|
60
|
+
const METADATA_RE = /^([a-z][a-z0-9-]*)\s+(.+)$/i;
|
|
59
61
|
|
|
60
62
|
// ============================================================
|
|
61
63
|
// Helpers
|
|
@@ -78,6 +80,12 @@ const VALID_SHAPES = new Set<string>([
|
|
|
78
80
|
'external',
|
|
79
81
|
]);
|
|
80
82
|
|
|
83
|
+
/** Known top-level option keys for C4 diagrams. */
|
|
84
|
+
const KNOWN_C4_OPTIONS = new Set<string>([
|
|
85
|
+
'layout',
|
|
86
|
+
'direction',
|
|
87
|
+
]);
|
|
88
|
+
|
|
81
89
|
const ALL_CHART_TYPES = [
|
|
82
90
|
'c4',
|
|
83
91
|
'org',
|
|
@@ -143,35 +151,6 @@ function parseArrowType(arrow: string): C4ArrowType | null {
|
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
|
|
146
|
-
/** Parse relationship label and optional [technology] annotation. */
|
|
147
|
-
function parseRelationshipBody(
|
|
148
|
-
body: string,
|
|
149
|
-
): { target: string; label?: string; technology?: string } {
|
|
150
|
-
// Format: `Target: label [tech]` or `Target: label` or `Target`
|
|
151
|
-
const colonIdx = body.indexOf(':');
|
|
152
|
-
let target: string;
|
|
153
|
-
let rest: string;
|
|
154
|
-
|
|
155
|
-
if (colonIdx > 0) {
|
|
156
|
-
target = body.substring(0, colonIdx).trim();
|
|
157
|
-
rest = body.substring(colonIdx + 1).trim();
|
|
158
|
-
} else {
|
|
159
|
-
target = body.trim();
|
|
160
|
-
rest = '';
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (!rest) return { target };
|
|
164
|
-
|
|
165
|
-
// Extract [technology] from end of rest
|
|
166
|
-
const techMatch = rest.match(/\[([^\]]+)\]\s*$/);
|
|
167
|
-
if (techMatch) {
|
|
168
|
-
const label = rest.substring(0, techMatch.index!).trim() || undefined;
|
|
169
|
-
return { target, label, technology: techMatch[1].trim() };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return { target, label: rest };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
154
|
|
|
176
155
|
// ============================================================
|
|
177
156
|
// Stack entry types
|
|
@@ -280,28 +259,21 @@ export function parseC4(
|
|
|
280
259
|
|
|
281
260
|
// --- Header phase ---
|
|
282
261
|
|
|
283
|
-
//
|
|
284
|
-
if (!contentStarted) {
|
|
285
|
-
const
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const hint = suggest(chartType, ALL_CHART_TYPES);
|
|
262
|
+
// First line: `c4` or `c4 My Title`
|
|
263
|
+
if (!contentStarted && !sawChartType) {
|
|
264
|
+
const firstLine = parseFirstLine(trimmed);
|
|
265
|
+
if (firstLine) {
|
|
266
|
+
if (firstLine.chartType !== 'c4') {
|
|
267
|
+
let msg = `Expected chart type "c4", got "${firstLine.chartType}"`;
|
|
268
|
+
const hint = suggest(firstLine.chartType, ALL_CHART_TYPES);
|
|
291
269
|
if (hint) msg += `. ${hint}`;
|
|
292
270
|
return fail(lineNumber, msg);
|
|
293
271
|
}
|
|
294
272
|
sawChartType = true;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// title: value
|
|
300
|
-
if (!contentStarted) {
|
|
301
|
-
const titleMatch = trimmed.match(TITLE_RE);
|
|
302
|
-
if (titleMatch) {
|
|
303
|
-
result.title = titleMatch[1].trim();
|
|
304
|
-
result.titleLineNumber = lineNumber;
|
|
273
|
+
if (firstLine.title) {
|
|
274
|
+
result.title = firstLine.title;
|
|
275
|
+
result.titleLineNumber = lineNumber;
|
|
276
|
+
}
|
|
305
277
|
continue;
|
|
306
278
|
}
|
|
307
279
|
}
|
|
@@ -315,7 +287,8 @@ export function parseC4(
|
|
|
315
287
|
continue;
|
|
316
288
|
}
|
|
317
289
|
if (tagBlockMatch.deprecated) {
|
|
318
|
-
pushError(lineNumber, `'## ${tagBlockMatch.name}' is
|
|
290
|
+
pushError(lineNumber, `'## ${tagBlockMatch.name}' is no longer supported — use 'tag: ${tagBlockMatch.name}' instead`);
|
|
291
|
+
continue;
|
|
319
292
|
}
|
|
320
293
|
currentTagGroup = {
|
|
321
294
|
name: tagBlockMatch.name,
|
|
@@ -330,27 +303,23 @@ export function parseC4(
|
|
|
330
303
|
continue;
|
|
331
304
|
}
|
|
332
305
|
|
|
333
|
-
// Generic header options
|
|
306
|
+
// Generic header options (space-separated: `key value`)
|
|
334
307
|
if (!contentStarted && !currentTagGroup && measureIndent(line) === 0) {
|
|
335
|
-
const optMatch = trimmed.match(
|
|
308
|
+
const optMatch = trimmed.match(OPTION_NOCOLON_RE);
|
|
336
309
|
if (optMatch) {
|
|
337
310
|
const key = optMatch[1].trim().toLowerCase();
|
|
338
|
-
if (key
|
|
311
|
+
if (KNOWN_C4_OPTIONS.has(key)) {
|
|
339
312
|
result.options[key] = optMatch[2].trim();
|
|
340
313
|
continue;
|
|
341
314
|
}
|
|
342
315
|
}
|
|
343
316
|
}
|
|
344
317
|
|
|
345
|
-
// Tag group entries
|
|
318
|
+
// Tag group entries — first entry is the default (no `default` keyword)
|
|
346
319
|
if (currentTagGroup && !contentStarted) {
|
|
347
320
|
const indent = measureIndent(line);
|
|
348
321
|
if (indent > 0) {
|
|
349
|
-
const
|
|
350
|
-
const entryText = isDefault
|
|
351
|
-
? trimmed.replace(/\s+default\s*$/, '').trim()
|
|
352
|
-
: trimmed;
|
|
353
|
-
const { label, color } = extractColor(entryText, palette);
|
|
322
|
+
const { label, color } = extractColor(trimmed, palette);
|
|
354
323
|
if (!color) {
|
|
355
324
|
pushError(
|
|
356
325
|
lineNumber,
|
|
@@ -358,7 +327,8 @@ export function parseC4(
|
|
|
358
327
|
);
|
|
359
328
|
continue;
|
|
360
329
|
}
|
|
361
|
-
|
|
330
|
+
// First entry becomes the default
|
|
331
|
+
if (currentTagGroup.entries.length === 0) {
|
|
362
332
|
currentTagGroup.defaultValue = label;
|
|
363
333
|
}
|
|
364
334
|
currentTagGroup.entries.push({
|
|
@@ -376,7 +346,7 @@ export function parseC4(
|
|
|
376
346
|
currentTagGroup = null;
|
|
377
347
|
|
|
378
348
|
if (!sawChartType) {
|
|
379
|
-
return fail(lineNumber, 'Missing "
|
|
349
|
+
return fail(lineNumber, 'Missing "c4" header');
|
|
380
350
|
}
|
|
381
351
|
|
|
382
352
|
const indent = measureIndent(line);
|
|
@@ -391,7 +361,7 @@ export function parseC4(
|
|
|
391
361
|
}
|
|
392
362
|
|
|
393
363
|
// Check for top-level non-deployment content (section ended)
|
|
394
|
-
if (indent === 0 && ELEMENT_RE.test(trimmed)) {
|
|
364
|
+
if (indent === 0 && (C4_IS_A_RE.test(trimmed) || ELEMENT_RE.test(trimmed))) {
|
|
395
365
|
inDeployment = false;
|
|
396
366
|
// Fall through to element parsing below
|
|
397
367
|
} else {
|
|
@@ -444,7 +414,7 @@ export function parseC4(
|
|
|
444
414
|
continue;
|
|
445
415
|
}
|
|
446
416
|
|
|
447
|
-
// containers
|
|
417
|
+
// containers / components must be inside an element
|
|
448
418
|
const parentEntry = findParentElement(indent, stack);
|
|
449
419
|
if (parentEntry) {
|
|
450
420
|
parentEntry.element.sectionHeader =
|
|
@@ -459,7 +429,7 @@ export function parseC4(
|
|
|
459
429
|
} else {
|
|
460
430
|
pushError(
|
|
461
431
|
lineNumber,
|
|
462
|
-
`"${sectionType}
|
|
432
|
+
`"${sectionType}" must be inside an element`,
|
|
463
433
|
);
|
|
464
434
|
}
|
|
465
435
|
continue;
|
|
@@ -516,6 +486,14 @@ export function parseC4(
|
|
|
516
486
|
const targetBody = m[2].trim();
|
|
517
487
|
if (!rawLabel) break; // empty label — fall through to plain arrow
|
|
518
488
|
|
|
489
|
+
// Reject bidirectional arrows
|
|
490
|
+
if (arrowType === 'bidirectional' || arrowType === 'bidirectional-async') {
|
|
491
|
+
const source = findParentElement(indent, stack)?.element.name ?? '?';
|
|
492
|
+
pushError(lineNumber, `Bidirectional arrows are no longer supported. Replace with two separate arrows:\n -${rawLabel}-> ${targetBody}\n ${targetBody} -${rawLabel}-> ${source}`);
|
|
493
|
+
labeledHandled = true;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
|
|
519
497
|
// Extract [technology] from end of label
|
|
520
498
|
let label: string | undefined = rawLabel;
|
|
521
499
|
let technology: string | undefined;
|
|
@@ -525,8 +503,25 @@ export function parseC4(
|
|
|
525
503
|
technology = techMatch[1].trim();
|
|
526
504
|
}
|
|
527
505
|
|
|
506
|
+
// Extract pipe metadata from target body (e.g. "Database | tech: SQL")
|
|
507
|
+
let target = targetBody;
|
|
508
|
+
const pipeIdx = targetBody.indexOf('|');
|
|
509
|
+
if (pipeIdx !== -1) {
|
|
510
|
+
target = targetBody.substring(0, pipeIdx).trim();
|
|
511
|
+
const metaPart = targetBody.substring(pipeIdx + 1).trim();
|
|
512
|
+
// parsePipeMetadata expects segments split by |; first segment is pre-pipe
|
|
513
|
+
const meta = parsePipeMetadata(['', metaPart], aliasMap);
|
|
514
|
+
// tech/technology on pipe overrides [tech] in label
|
|
515
|
+
if (meta.tech) {
|
|
516
|
+
technology = meta.tech;
|
|
517
|
+
}
|
|
518
|
+
if (meta.technology) {
|
|
519
|
+
technology = meta.technology;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
528
523
|
const rel: C4Relationship = {
|
|
529
|
-
target
|
|
524
|
+
target,
|
|
530
525
|
label,
|
|
531
526
|
technology,
|
|
532
527
|
arrowType,
|
|
@@ -545,18 +540,24 @@ export function parseC4(
|
|
|
545
540
|
if (labeledHandled) continue;
|
|
546
541
|
}
|
|
547
542
|
|
|
548
|
-
// ── Relationships
|
|
543
|
+
// ── Relationships (plain arrows: ->, ~>) ─────────────────
|
|
549
544
|
const relMatch = trimmed.match(RELATIONSHIP_RE);
|
|
550
545
|
if (relMatch) {
|
|
551
546
|
const arrowType = parseArrowType(relMatch[1]);
|
|
552
547
|
if (arrowType) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
548
|
+
// Reject bidirectional arrows
|
|
549
|
+
if (arrowType === 'bidirectional' || arrowType === 'bidirectional-async') {
|
|
550
|
+
const arrow = relMatch[1];
|
|
551
|
+
const target = relMatch[2].trim();
|
|
552
|
+
const source = findParentElement(indent, stack)?.element.name ?? '?';
|
|
553
|
+
pushError(lineNumber, `'${arrow}' bidirectional arrows are no longer supported. Replace with two separate arrows:\n -> ${target}\n ${target} -> ${source}`);
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Plain arrow: entire body is the target (no colon label, no technology)
|
|
558
|
+
const target = relMatch[2].trim();
|
|
556
559
|
const rel: C4Relationship = {
|
|
557
560
|
target,
|
|
558
|
-
label,
|
|
559
|
-
technology,
|
|
560
561
|
arrowType,
|
|
561
562
|
lineNumber,
|
|
562
563
|
};
|
|
@@ -573,7 +574,106 @@ export function parseC4(
|
|
|
573
574
|
}
|
|
574
575
|
}
|
|
575
576
|
|
|
576
|
-
// ──
|
|
577
|
+
// ── "Name is a type" declarations (preferred syntax) ────
|
|
578
|
+
const isATypeMatch = trimmed.match(C4_IS_A_RE);
|
|
579
|
+
if (isATypeMatch) {
|
|
580
|
+
let namePart = isATypeMatch[1].trim();
|
|
581
|
+
const rawType = isATypeMatch[2].toLowerCase();
|
|
582
|
+
const remainder = isATypeMatch[3];
|
|
583
|
+
|
|
584
|
+
// Map external/database to shape overrides with default element type
|
|
585
|
+
let elementType: C4ElementType;
|
|
586
|
+
let explicitShape: C4Shape | null = null;
|
|
587
|
+
if (rawType === 'external') {
|
|
588
|
+
elementType = 'system';
|
|
589
|
+
explicitShape = 'external';
|
|
590
|
+
} else if (rawType === 'database') {
|
|
591
|
+
elementType = 'container';
|
|
592
|
+
explicitShape = 'database';
|
|
593
|
+
} else {
|
|
594
|
+
elementType = rawType as C4ElementType;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Parse pipe metadata from remainder
|
|
598
|
+
const remainderTrimmed = remainder.trim();
|
|
599
|
+
let segments: string[];
|
|
600
|
+
if (remainderTrimmed.startsWith('|')) {
|
|
601
|
+
// remainder has pipe metadata: "| tech: PostgreSQL, team: Data"
|
|
602
|
+
segments = ['', ...remainderTrimmed.substring(1).split('|').map((s) => s.trim())];
|
|
603
|
+
} else {
|
|
604
|
+
segments = [remainderTrimmed];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Check for additional `is a <shape>` in the name (e.g., already stripped by C4_IS_A_RE won't happen,
|
|
608
|
+
// but handle remainder like "is a cylinder" after type)
|
|
609
|
+
const remainderIsA = remainderTrimmed.match(/^\s*is\s+a(?:n)?\s+(\w+)\s*(.*)$/i);
|
|
610
|
+
if (remainderIsA) {
|
|
611
|
+
const shapeName = remainderIsA[1].toLowerCase();
|
|
612
|
+
if (VALID_SHAPES.has(shapeName)) {
|
|
613
|
+
explicitShape = shapeName as C4Shape;
|
|
614
|
+
} else {
|
|
615
|
+
pushError(
|
|
616
|
+
lineNumber,
|
|
617
|
+
`Unknown shape "${remainderIsA[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
// Re-parse remainder after shape
|
|
621
|
+
const afterShape = remainderIsA[2].trim();
|
|
622
|
+
if (afterShape.startsWith('|')) {
|
|
623
|
+
segments = ['', ...afterShape.substring(1).split('|').map((s) => s.trim())];
|
|
624
|
+
} else {
|
|
625
|
+
segments = [afterShape];
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Also check for `is a <shape>` within the name part itself
|
|
630
|
+
const nameIsAMatch = namePart.match(IS_A_RE);
|
|
631
|
+
if (nameIsAMatch) {
|
|
632
|
+
const shapeName = nameIsAMatch[1].toLowerCase();
|
|
633
|
+
if (VALID_SHAPES.has(shapeName)) {
|
|
634
|
+
explicitShape = shapeName as C4Shape;
|
|
635
|
+
} else {
|
|
636
|
+
pushError(
|
|
637
|
+
lineNumber,
|
|
638
|
+
`Unknown shape "${nameIsAMatch[1]}". Valid shapes: ${[...VALID_SHAPES].join(', ')}`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
namePart = namePart.substring(0, nameIsAMatch.index!).trim();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning'));
|
|
645
|
+
|
|
646
|
+
const shape =
|
|
647
|
+
explicitShape ??
|
|
648
|
+
inferC4Shape(namePart, metadata.tech ?? metadata.technology);
|
|
649
|
+
|
|
650
|
+
const element: C4Element = {
|
|
651
|
+
name: namePart,
|
|
652
|
+
type: elementType,
|
|
653
|
+
shape,
|
|
654
|
+
metadata,
|
|
655
|
+
children: [],
|
|
656
|
+
groups: [],
|
|
657
|
+
relationships: [],
|
|
658
|
+
lineNumber,
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// Check for duplicate name
|
|
662
|
+
const existingLine = knownNames.get(namePart.toLowerCase());
|
|
663
|
+
if (existingLine !== undefined) {
|
|
664
|
+
pushError(
|
|
665
|
+
lineNumber,
|
|
666
|
+
`Duplicate element name "${namePart}" (first defined on line ${existingLine})`,
|
|
667
|
+
);
|
|
668
|
+
} else {
|
|
669
|
+
knownNames.set(namePart.toLowerCase(), lineNumber);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
attachElement(element, indent, stack, result);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ── Element declarations (deprecated prefix syntax) ─────
|
|
577
677
|
const elementMatch = trimmed.match(ELEMENT_RE);
|
|
578
678
|
if (elementMatch) {
|
|
579
679
|
const elementType = elementMatch[1].toLowerCase() as C4ElementType;
|
|
@@ -599,6 +699,12 @@ export function parseC4(
|
|
|
599
699
|
namePart = namePart.substring(0, isAMatch.index!).trim();
|
|
600
700
|
}
|
|
601
701
|
|
|
702
|
+
// Emit deprecation error with migration hint
|
|
703
|
+
pushError(
|
|
704
|
+
lineNumber,
|
|
705
|
+
`'${elementMatch[1]} ${namePart}' prefix syntax is no longer supported — use '${namePart} is a ${elementType}' instead`,
|
|
706
|
+
);
|
|
707
|
+
|
|
602
708
|
const metadata = parsePipeMetadata(segments, aliasMap, () => pushError(lineNumber, MULTIPLE_PIPE_WARNING, 'warning'));
|
|
603
709
|
|
|
604
710
|
// Determine shape: explicit > inference
|
|
@@ -641,7 +747,7 @@ export function parseC4(
|
|
|
641
747
|
if (parentEntry) {
|
|
642
748
|
const rawKey = metadataMatch[1].trim().toLowerCase();
|
|
643
749
|
|
|
644
|
-
// Special case: `import
|
|
750
|
+
// Special case: `import file.dgmo`
|
|
645
751
|
if (rawKey === 'import') {
|
|
646
752
|
parentEntry.element.importPath = metadataMatch[2].trim();
|
|
647
753
|
continue;
|
package/src/c4/renderer.ts
CHANGED
|
@@ -15,16 +15,16 @@ import { layoutC4Context, layoutC4Containers, layoutC4Components, layoutC4Deploy
|
|
|
15
15
|
import {
|
|
16
16
|
LEGEND_HEIGHT,
|
|
17
17
|
LEGEND_PILL_FONT_SIZE,
|
|
18
|
-
LEGEND_PILL_FONT_W,
|
|
19
18
|
LEGEND_PILL_PAD,
|
|
20
19
|
LEGEND_DOT_R,
|
|
21
20
|
LEGEND_ENTRY_FONT_SIZE,
|
|
22
|
-
LEGEND_ENTRY_FONT_W,
|
|
23
21
|
LEGEND_ENTRY_DOT_GAP,
|
|
24
22
|
LEGEND_ENTRY_TRAIL,
|
|
25
23
|
LEGEND_CAPSULE_PAD,
|
|
26
24
|
LEGEND_GROUP_GAP,
|
|
25
|
+
measureLegendText,
|
|
27
26
|
} from '../utils/legend-constants';
|
|
27
|
+
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
|
|
28
28
|
|
|
29
29
|
// ============================================================
|
|
30
30
|
// Constants
|
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
const DIAGRAM_PADDING = 20;
|
|
34
34
|
const MAX_SCALE = 3;
|
|
35
35
|
const TITLE_HEIGHT = 30;
|
|
36
|
-
const TITLE_FONT_SIZE = 20;
|
|
37
36
|
const TYPE_FONT_SIZE = 10;
|
|
38
37
|
const NAME_FONT_SIZE = 14;
|
|
39
38
|
const DESC_FONT_SIZE = 11;
|
|
@@ -307,8 +306,8 @@ export function renderC4Context(
|
|
|
307
306
|
.attr('y', 30)
|
|
308
307
|
.attr('text-anchor', 'middle')
|
|
309
308
|
.attr('fill', palette.text)
|
|
310
|
-
.attr('font-size',
|
|
311
|
-
.attr('font-weight',
|
|
309
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
310
|
+
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
312
311
|
.style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
|
|
313
312
|
.text(parsed.title);
|
|
314
313
|
|
|
@@ -1143,7 +1142,7 @@ function renderLegend(
|
|
|
1143
1142
|
? layout.legend.filter((g) => g.name.toLowerCase() === (activeTagGroup ?? '').toLowerCase())
|
|
1144
1143
|
: layout.legend;
|
|
1145
1144
|
|
|
1146
|
-
const pillWidthOf = (g: C4LegendGroup) => g.name
|
|
1145
|
+
const pillWidthOf = (g: C4LegendGroup) => measureLegendText(g.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1147
1146
|
const effectiveW = (g: C4LegendGroup) => activeTagGroup != null ? g.width : pillWidthOf(g);
|
|
1148
1147
|
|
|
1149
1148
|
// In fixed mode, compute centered x-positions
|
|
@@ -1250,7 +1249,7 @@ function renderLegend(
|
|
|
1250
1249
|
.attr('fill', palette.textMuted)
|
|
1251
1250
|
.text(entry.value);
|
|
1252
1251
|
|
|
1253
|
-
entryX = textX + entry.value
|
|
1252
|
+
entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
|
|
1254
1253
|
}
|
|
1255
1254
|
}
|
|
1256
1255
|
}
|
|
@@ -1347,8 +1346,8 @@ export function renderC4Containers(
|
|
|
1347
1346
|
.attr('y', 30)
|
|
1348
1347
|
.attr('text-anchor', 'middle')
|
|
1349
1348
|
.attr('fill', palette.text)
|
|
1350
|
-
.attr('font-size',
|
|
1351
|
-
.attr('font-weight',
|
|
1349
|
+
.attr('font-size', TITLE_FONT_SIZE)
|
|
1350
|
+
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
1352
1351
|
.style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
|
|
1353
1352
|
.text(parsed.title);
|
|
1354
1353
|
|