@zombie-mermaid/mermaid-parser 3.0.0 → 3.2.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/dist/index.cjs +3 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +225 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +462 -138
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/architecture-parser.test.ts +145 -0
- package/src/__tests__/architecture-to-graph.test.ts +169 -0
- package/src/__tests__/c4-parser.test.ts +265 -0
- package/src/__tests__/c4-upstream-parser.test.ts +340 -0
- package/src/architecture/parser.ts +187 -0
- package/src/architecture/to-graph.ts +141 -0
- package/src/architecture/types.ts +49 -0
- package/src/c4/format.ts +120 -0
- package/src/c4/parser.ts +271 -0
- package/src/c4/types.ts +136 -0
- package/src/index.ts +8 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Architecture diagram types (Mermaid `architecture-beta`)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
/** A port on a service/junction: left, right, top, bottom. */
|
|
6
|
+
export type ArchitecturePort = 'L' | 'R' | 'T' | 'B'
|
|
7
|
+
|
|
8
|
+
export interface ArchitectureGroup {
|
|
9
|
+
id: string
|
|
10
|
+
/** Icon name from `(icon)`; kept for consumers, not drawn (see to-graph). */
|
|
11
|
+
icon?: string
|
|
12
|
+
/** `[Title]`; falls back to the id. */
|
|
13
|
+
title: string
|
|
14
|
+
/** Enclosing group id, from `in <parent>`. */
|
|
15
|
+
parent?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ArchitectureService {
|
|
19
|
+
id: string
|
|
20
|
+
icon?: string
|
|
21
|
+
title: string
|
|
22
|
+
parent?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ArchitectureJunction {
|
|
26
|
+
id: string
|
|
27
|
+
parent?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ArchitectureEdge {
|
|
31
|
+
source: string
|
|
32
|
+
sourcePort: ArchitecturePort
|
|
33
|
+
/** `{group}` modifier: the edge attaches to the enclosing group's border. */
|
|
34
|
+
sourceGroup: boolean
|
|
35
|
+
target: string
|
|
36
|
+
targetPort: ArchitecturePort
|
|
37
|
+
targetGroup: boolean
|
|
38
|
+
/** `<--` / `<-->`: arrowhead at the source end. */
|
|
39
|
+
arrowStart: boolean
|
|
40
|
+
/** `-->` / `<-->`: arrowhead at the target end. */
|
|
41
|
+
arrowEnd: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ArchitectureDiagram {
|
|
45
|
+
groups: ArchitectureGroup[]
|
|
46
|
+
services: ArchitectureService[]
|
|
47
|
+
junctions: ArchitectureJunction[]
|
|
48
|
+
edges: ArchitectureEdge[]
|
|
49
|
+
}
|
package/src/c4/format.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { C4Boundary, C4Element, C4Relationship } from './types.ts'
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// C4 text and layout-hint helpers shared by the SVG and ASCII renderers, so
|
|
5
|
+
// both draw the same words and honor the same `Rel_U/D/L/R` hints.
|
|
6
|
+
// ============================================================================
|
|
7
|
+
|
|
8
|
+
/** Greedy word wrap; a word longer than `width` stays on its own line. */
|
|
9
|
+
export function wrapC4Text(text: string, width: number): string[] {
|
|
10
|
+
const out: string[] = []
|
|
11
|
+
for (const paragraph of text.split('\n')) {
|
|
12
|
+
let cur = ''
|
|
13
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
14
|
+
if (cur && cur.length + 1 + word.length > width) {
|
|
15
|
+
out.push(cur)
|
|
16
|
+
cur = word
|
|
17
|
+
} else {
|
|
18
|
+
cur = cur ? `${cur} ${word}` : word
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
out.push(cur)
|
|
22
|
+
}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const STEREOTYPES: Record<C4Element['kind'], string> = {
|
|
27
|
+
person: 'Person',
|
|
28
|
+
system: 'Software System',
|
|
29
|
+
container: 'Container',
|
|
30
|
+
component: 'Component',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The bracketed type line under an element's name, as Mermaid draws it:
|
|
35
|
+
* `[Person]`, `[Software System]`, `[Container: PostgreSQL]`. External,
|
|
36
|
+
* database and queue variants share their base kind's line; the fill and the
|
|
37
|
+
* shape tell them apart.
|
|
38
|
+
*/
|
|
39
|
+
export function c4TypeLine(el: C4Element): string {
|
|
40
|
+
const tech =
|
|
41
|
+
(el.kind === 'container' || el.kind === 'component') && el.technology
|
|
42
|
+
? `: ${el.technology}`
|
|
43
|
+
: ''
|
|
44
|
+
return `[${STEREOTYPES[el.kind]}${tech}]`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A boundary's type line (`[ENTERPRISE]`, `[Ubuntu]`), when it has a type. */
|
|
48
|
+
export function c4BoundaryTypeLine(b: C4Boundary): string | undefined {
|
|
49
|
+
return b.type ? `[${b.type}]` : undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The text lines of a relationship label: the label itself (prefixed with its
|
|
54
|
+
* sequence number in a C4Dynamic diagram), then the technology in brackets on its
|
|
55
|
+
* own line. Empty when the relationship has neither.
|
|
56
|
+
*/
|
|
57
|
+
export function c4RelLabelLines(rel: C4Relationship): string[] {
|
|
58
|
+
const lines: string[] = []
|
|
59
|
+
const head = [rel.index ? `${rel.index}:` : '', rel.label]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(' ')
|
|
62
|
+
if (head) lines.push(head)
|
|
63
|
+
if (rel.technology) lines.push(`[${rel.technology}]`)
|
|
64
|
+
return lines
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* How a relationship constrains placement. `source` is meant to sit before
|
|
69
|
+
* `target` along `axis`: above it for `'vertical'`, left of it for
|
|
70
|
+
* `'horizontal'`. `Rel_U`/`Rel_L` reverse the pair (the target sits above /
|
|
71
|
+
* left of the source); plain `Rel` is a vertical hint, since C4 diagrams flow
|
|
72
|
+
* top to bottom by default.
|
|
73
|
+
*/
|
|
74
|
+
export interface C4Placement {
|
|
75
|
+
source: string
|
|
76
|
+
target: string
|
|
77
|
+
axis: 'vertical' | 'horizontal'
|
|
78
|
+
/** True when the hint came from an explicit `Rel_U/D/L/R`. */
|
|
79
|
+
explicit: boolean
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function c4Placement(rel: C4Relationship): C4Placement {
|
|
83
|
+
switch (rel.layout) {
|
|
84
|
+
case 'up':
|
|
85
|
+
return {
|
|
86
|
+
source: rel.to,
|
|
87
|
+
target: rel.from,
|
|
88
|
+
axis: 'vertical',
|
|
89
|
+
explicit: true,
|
|
90
|
+
}
|
|
91
|
+
case 'left':
|
|
92
|
+
return {
|
|
93
|
+
source: rel.to,
|
|
94
|
+
target: rel.from,
|
|
95
|
+
axis: 'horizontal',
|
|
96
|
+
explicit: true,
|
|
97
|
+
}
|
|
98
|
+
case 'right':
|
|
99
|
+
return {
|
|
100
|
+
source: rel.from,
|
|
101
|
+
target: rel.to,
|
|
102
|
+
axis: 'horizontal',
|
|
103
|
+
explicit: true,
|
|
104
|
+
}
|
|
105
|
+
case 'down':
|
|
106
|
+
return {
|
|
107
|
+
source: rel.from,
|
|
108
|
+
target: rel.to,
|
|
109
|
+
axis: 'vertical',
|
|
110
|
+
explicit: true,
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
return {
|
|
114
|
+
source: rel.from,
|
|
115
|
+
target: rel.to,
|
|
116
|
+
axis: 'vertical',
|
|
117
|
+
explicit: false,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/c4/parser.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { normalizeBrTags, type Statement } from '@zombie-mermaid/core'
|
|
2
|
+
import type {
|
|
3
|
+
C4Boundary,
|
|
4
|
+
C4Diagram,
|
|
5
|
+
C4Element,
|
|
6
|
+
C4ElementKind,
|
|
7
|
+
C4LayoutHint,
|
|
8
|
+
C4ElementShape,
|
|
9
|
+
C4Relationship,
|
|
10
|
+
C4Variant,
|
|
11
|
+
} from './types.ts'
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// C4 diagram parser
|
|
15
|
+
//
|
|
16
|
+
// Supported syntax (Mermaid C4 macros):
|
|
17
|
+
// Person / Person_Ext (alias, label, descr)
|
|
18
|
+
// System[Db|Queue][_Ext] (alias, label, descr)
|
|
19
|
+
// Container[Db|Queue][_Ext] (alias, label, techn, descr)
|
|
20
|
+
// Component[Db|Queue][_Ext] (alias, label, techn, descr)
|
|
21
|
+
// Boundary / Enterprise_Boundary / System_Boundary / Container_Boundary
|
|
22
|
+
// (alias, label[, type]) { ... }
|
|
23
|
+
// Deployment_Node / Node / Node_L / Node_R (alias, label, type, descr) { ... }
|
|
24
|
+
// Rel / BiRel / Rel_U|Up|D|Down|L|Left|R|Right|Back (from, to, label, techn)
|
|
25
|
+
// RelIndex (index, from, to, label, techn)
|
|
26
|
+
// title <text>
|
|
27
|
+
//
|
|
28
|
+
// `Rel_U/D/L/R` record a placement hint (`C4Relationship.layout`) the
|
|
29
|
+
// renderers use to steer layout. Accepted and ignored (styling / layout
|
|
30
|
+
// config this renderer does not honor): UpdateElementStyle, UpdateRelStyle, UpdateLayoutConfig, LAYOUT_*,
|
|
31
|
+
// SHOW_LEGEND, SHOW_FLOATING_LEGEND, AddElementTag, AddRelTag,
|
|
32
|
+
// AddBoundaryTag, RoleTag. Named arguments (`$tags="x"`, `$link=...`) are
|
|
33
|
+
// dropped.
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
const HEADERS: Record<string, C4Variant> = {
|
|
37
|
+
c4context: 'context',
|
|
38
|
+
c4container: 'container',
|
|
39
|
+
c4component: 'component',
|
|
40
|
+
c4dynamic: 'dynamic',
|
|
41
|
+
c4deployment: 'deployment',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const IGNORED_MACRO =
|
|
45
|
+
/^(?:UpdateElementStyle|UpdateRelStyle|UpdateLayoutConfig|LAYOUT_[A-Z_]+|SHOW_LEGEND|SHOW_FLOATING_LEGEND|AddElementTag|AddRelTag|AddBoundaryTag|RoleTag)\b/
|
|
46
|
+
|
|
47
|
+
const ELEMENT_NAME = /^(Person|System|Container|Component)(Db|Queue)?(_Ext)?$/
|
|
48
|
+
const BOUNDARY_NAME =
|
|
49
|
+
/^(?:Boundary|Enterprise_Boundary|System_Boundary|Container_Boundary)$/
|
|
50
|
+
const NODE_NAME = /^(?:Deployment_Node|Node|Node_L|Node_R)$/
|
|
51
|
+
const REL_NAME =
|
|
52
|
+
/^(Rel|BiRel|Rel_U|Rel_Up|Rel_D|Rel_Down|Rel_L|Rel_Left|Rel_R|Rel_Right|Rel_Back|RelIndex)$/
|
|
53
|
+
|
|
54
|
+
const BOUNDARY_TYPES: Record<string, string> = {
|
|
55
|
+
Enterprise_Boundary: 'ENTERPRISE',
|
|
56
|
+
System_Boundary: 'SYSTEM',
|
|
57
|
+
Container_Boundary: 'CONTAINER',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const REL_HINTS: Record<string, C4LayoutHint> = {
|
|
61
|
+
Rel_U: 'up',
|
|
62
|
+
Rel_Up: 'up',
|
|
63
|
+
Rel_D: 'down',
|
|
64
|
+
Rel_Down: 'down',
|
|
65
|
+
Rel_L: 'left',
|
|
66
|
+
Rel_Left: 'left',
|
|
67
|
+
Rel_R: 'right',
|
|
68
|
+
Rel_Right: 'right',
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function unquote(a: string): string {
|
|
72
|
+
const t = a.trim()
|
|
73
|
+
const inner =
|
|
74
|
+
t.length >= 2 && t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t
|
|
75
|
+
return normalizeBrTags(inner)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Split a macro's argument list on top-level commas. Double-quoted strings
|
|
80
|
+
* may contain commas and parentheses; `$name=value` named arguments are
|
|
81
|
+
* dropped. Returns null when a quote is unbalanced.
|
|
82
|
+
*/
|
|
83
|
+
function splitArgs(inner: string): string[] | null {
|
|
84
|
+
const raw: string[] = []
|
|
85
|
+
let cur = ''
|
|
86
|
+
let inQuote = false
|
|
87
|
+
for (const ch of inner) {
|
|
88
|
+
if (ch === '"') inQuote = !inQuote
|
|
89
|
+
if (ch === ',' && !inQuote) {
|
|
90
|
+
raw.push(cur)
|
|
91
|
+
cur = ''
|
|
92
|
+
} else {
|
|
93
|
+
cur += ch
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (inQuote) return null
|
|
97
|
+
raw.push(cur)
|
|
98
|
+
return raw
|
|
99
|
+
.map((a) => a.trim())
|
|
100
|
+
.filter((a) => !a.startsWith('$'))
|
|
101
|
+
.map(unquote)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `Name(args)` optionally followed by ` {`. */
|
|
105
|
+
const MACRO = /^([A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*(\{)?\s*$/
|
|
106
|
+
|
|
107
|
+
function fail(stmt: Statement, msg: string): never {
|
|
108
|
+
throw new Error(`C4 diagram, line ${stmt.line}: ${msg} — "${stmt.text}"`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parse a Mermaid C4 diagram. Expects the first statement to be a
|
|
113
|
+
* `C4Context` / `C4Container` / `C4Component` / `C4Dynamic` /
|
|
114
|
+
* `C4Deployment` header.
|
|
115
|
+
*/
|
|
116
|
+
export function parseC4Diagram(lines: Statement[]): C4Diagram {
|
|
117
|
+
const variant = HEADERS[lines[0]?.text.trim().toLowerCase() ?? '']
|
|
118
|
+
if (!variant) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`C4 diagram: expected a header of C4Context, C4Container, C4Component, C4Dynamic or C4Deployment, got "${lines[0]?.text ?? ''}"`,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const diagram: C4Diagram = {
|
|
125
|
+
variant,
|
|
126
|
+
elements: [],
|
|
127
|
+
boundaries: [],
|
|
128
|
+
relationships: [],
|
|
129
|
+
}
|
|
130
|
+
const aliases = new Set<string>()
|
|
131
|
+
const stack: C4Boundary[] = []
|
|
132
|
+
/** Boundary declared on the previous statement without an inline `{`. */
|
|
133
|
+
let pendingBoundary: C4Boundary | undefined
|
|
134
|
+
|
|
135
|
+
const claim = (stmt: Statement, alias: string | undefined): string => {
|
|
136
|
+
if (!alias) fail(stmt, 'missing alias (first argument)')
|
|
137
|
+
if (aliases.has(alias)) fail(stmt, `duplicate alias "${alias}"`)
|
|
138
|
+
aliases.add(alias)
|
|
139
|
+
return alias
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (let i = 1; i < lines.length; i++) {
|
|
143
|
+
const stmt = lines[i]!
|
|
144
|
+
const line = stmt.text
|
|
145
|
+
|
|
146
|
+
if (line === '}') {
|
|
147
|
+
if (stack.length === 0) fail(stmt, 'unmatched "}"')
|
|
148
|
+
stack.pop()
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
// Brace on its own line, directly after a boundary macro.
|
|
152
|
+
if (line === '{') {
|
|
153
|
+
if (!pendingBoundary) fail(stmt, 'unexpected "{"')
|
|
154
|
+
stack.push(pendingBoundary)
|
|
155
|
+
pendingBoundary = undefined
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
pendingBoundary = undefined
|
|
159
|
+
|
|
160
|
+
const titleMatch = line.match(/^title(?:\s+(.*))?$/i)
|
|
161
|
+
if (titleMatch) {
|
|
162
|
+
diagram.title = normalizeBrTags((titleMatch[1] ?? '').trim())
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (IGNORED_MACRO.test(line)) continue
|
|
167
|
+
|
|
168
|
+
const m = line.match(MACRO)
|
|
169
|
+
if (!m) fail(stmt, 'unrecognized statement')
|
|
170
|
+
const name = m[1]!
|
|
171
|
+
const opensBlock = m[3] === '{'
|
|
172
|
+
const args = splitArgs(m[2]!)
|
|
173
|
+
if (!args) fail(stmt, 'unbalanced quotes')
|
|
174
|
+
|
|
175
|
+
const el = name.match(ELEMENT_NAME)
|
|
176
|
+
if (el) {
|
|
177
|
+
if (opensBlock) fail(stmt, `"${name}" cannot contain a block`)
|
|
178
|
+
const kind = el[1]!.toLowerCase() as C4ElementKind
|
|
179
|
+
const shape: C4ElementShape =
|
|
180
|
+
el[2] === 'Db' ? 'db' : el[2] === 'Queue' ? 'queue' : 'default'
|
|
181
|
+
const hasTech = kind === 'container' || kind === 'component'
|
|
182
|
+
const alias = claim(stmt, args[0])
|
|
183
|
+
const element: C4Element = {
|
|
184
|
+
alias,
|
|
185
|
+
kind,
|
|
186
|
+
shape,
|
|
187
|
+
external: el[3] !== undefined,
|
|
188
|
+
label: args[1] || alias,
|
|
189
|
+
}
|
|
190
|
+
const technology = hasTech ? args[2] : undefined
|
|
191
|
+
const description = hasTech ? args[3] : args[2]
|
|
192
|
+
if (technology) element.technology = technology
|
|
193
|
+
if (description) element.description = description
|
|
194
|
+
diagram.elements.push(element)
|
|
195
|
+
stack.at(-1)?.elementAliases.push(alias)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (BOUNDARY_NAME.test(name) || NODE_NAME.test(name)) {
|
|
200
|
+
const isNode = NODE_NAME.test(name)
|
|
201
|
+
const alias = claim(stmt, args[0])
|
|
202
|
+
const boundary: C4Boundary = {
|
|
203
|
+
alias,
|
|
204
|
+
label: args[1] || alias,
|
|
205
|
+
elementAliases: [],
|
|
206
|
+
children: [],
|
|
207
|
+
}
|
|
208
|
+
// Mermaid labels every frame with a type: the macro's own for the
|
|
209
|
+
// typed boundaries, else the explicit argument, else a default.
|
|
210
|
+
// (System_/Container_/Enterprise_Boundary take no type arg.)
|
|
211
|
+
const typed = BOUNDARY_TYPES[name]
|
|
212
|
+
if (typed) boundary.type = typed
|
|
213
|
+
else if (args[2] && (isNode || name === 'Boundary'))
|
|
214
|
+
boundary.type = args[2]
|
|
215
|
+
else boundary.type = isNode ? 'node' : 'system'
|
|
216
|
+
if (isNode && args[3]) boundary.description = args[3]
|
|
217
|
+
const parent = stack.at(-1)
|
|
218
|
+
if (parent) parent.children.push(boundary)
|
|
219
|
+
else diagram.boundaries.push(boundary)
|
|
220
|
+
if (opensBlock) stack.push(boundary)
|
|
221
|
+
else pendingBoundary = boundary
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rel = name.match(REL_NAME)
|
|
226
|
+
if (rel) {
|
|
227
|
+
const indexed = name === 'RelIndex'
|
|
228
|
+
const a = indexed ? args.slice(1) : args
|
|
229
|
+
if (!a[0] || !a[1]) {
|
|
230
|
+
fail(stmt, 'a relationship needs "from" and "to" aliases')
|
|
231
|
+
}
|
|
232
|
+
const r: C4Relationship = {
|
|
233
|
+
from: a[0],
|
|
234
|
+
to: a[1],
|
|
235
|
+
label: a[2] ?? '',
|
|
236
|
+
bidirectional: name === 'BiRel',
|
|
237
|
+
}
|
|
238
|
+
if (a[3]) r.technology = a[3]
|
|
239
|
+
if (name === 'Rel_Back') r.reversed = true
|
|
240
|
+
const hint = REL_HINTS[name]
|
|
241
|
+
if (hint) r.layout = hint
|
|
242
|
+
diagram.relationships.push(r)
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fail(stmt, `unknown C4 macro "${name}"`)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (stack.length > 0) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`C4 diagram: boundary "${stack.at(-1)!.alias}" is missing its closing "}"`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
// Mermaid ignores RelIndex's index argument: in C4Dynamic every relationship
|
|
255
|
+
// is numbered by its position in the source, and no other variant numbers.
|
|
256
|
+
if (diagram.variant === 'dynamic') {
|
|
257
|
+
diagram.relationships.forEach((r, i) => {
|
|
258
|
+
r.index = String(i + 1)
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
for (const r of diagram.relationships) {
|
|
262
|
+
for (const end of [r.from, r.to]) {
|
|
263
|
+
if (!aliases.has(end)) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
`C4 diagram: relationship refers to undeclared alias "${end}"`,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return diagram
|
|
271
|
+
}
|
package/src/c4/types.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { Direction, Point } from '@zombie-mermaid/core'
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// C4 diagram model (C4Context / C4Container / C4Component / C4Dynamic /
|
|
5
|
+
// C4Deployment). Prior art: lukilabs/beautiful-mermaid#34 and #71
|
|
6
|
+
// (kristjanakkermann, devx) — see the changeset for attribution.
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
export type C4Variant =
|
|
10
|
+
'context' | 'container' | 'component' | 'dynamic' | 'deployment'
|
|
11
|
+
|
|
12
|
+
export type C4ElementKind = 'person' | 'system' | 'container' | 'component'
|
|
13
|
+
|
|
14
|
+
/** Visual variant of an element: plain box, database cylinder, or queue. */
|
|
15
|
+
export type C4ElementShape = 'default' | 'db' | 'queue'
|
|
16
|
+
|
|
17
|
+
export interface C4Element {
|
|
18
|
+
alias: string
|
|
19
|
+
kind: C4ElementKind
|
|
20
|
+
shape: C4ElementShape
|
|
21
|
+
/** `*_Ext` variants: outside the system being described. */
|
|
22
|
+
external: boolean
|
|
23
|
+
label: string
|
|
24
|
+
technology?: string
|
|
25
|
+
description?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface C4Boundary {
|
|
29
|
+
alias: string
|
|
30
|
+
label: string
|
|
31
|
+
/** Boundary type (`Boundary(a, "x", "type")`) or deployment node type. */
|
|
32
|
+
type?: string
|
|
33
|
+
description?: string
|
|
34
|
+
/** Aliases of elements declared directly inside this boundary. */
|
|
35
|
+
elementAliases: string[]
|
|
36
|
+
children: C4Boundary[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Layout placement hint carried by `Rel_U`/`Rel_D`/`Rel_L`/`Rel_R` (and their
|
|
41
|
+
* long-form aliases): where `to` should sit relative to `from`. Plain `Rel`,
|
|
42
|
+
* `BiRel`, `RelIndex` and `Rel_Back` carry none.
|
|
43
|
+
*/
|
|
44
|
+
export type C4LayoutHint = 'up' | 'down' | 'left' | 'right'
|
|
45
|
+
|
|
46
|
+
export interface C4Relationship {
|
|
47
|
+
from: string
|
|
48
|
+
to: string
|
|
49
|
+
label: string
|
|
50
|
+
technology?: string
|
|
51
|
+
/** `BiRel`: arrowheads at both ends. */
|
|
52
|
+
bidirectional: boolean
|
|
53
|
+
/**
|
|
54
|
+
* `Rel_Back`: the arrowhead points at `from` instead of `to`. `from`/`to`
|
|
55
|
+
* stay as declared, so layout and placement hints still follow them.
|
|
56
|
+
*/
|
|
57
|
+
reversed?: boolean
|
|
58
|
+
/** `RelIndex(n, ...)` in C4Dynamic diagrams. */
|
|
59
|
+
index?: string
|
|
60
|
+
/** Placement hint from a directional `Rel_*` macro; see `C4LayoutHint`. */
|
|
61
|
+
layout?: C4LayoutHint
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface C4Diagram {
|
|
65
|
+
variant: C4Variant
|
|
66
|
+
/**
|
|
67
|
+
* Top-level layout direction. Never set by the parser (C4 sources have no
|
|
68
|
+
* direction statement); only `RenderOptions.direction` sets it, via
|
|
69
|
+
* `withDirectionOverride`. Unset lays out top-to-bottom.
|
|
70
|
+
*/
|
|
71
|
+
direction?: Direction
|
|
72
|
+
title?: string
|
|
73
|
+
/** Every element in declaration order, wherever it is nested. */
|
|
74
|
+
elements: C4Element[]
|
|
75
|
+
/** Top-level boundaries; elements outside any boundary are not listed. */
|
|
76
|
+
boundaries: C4Boundary[]
|
|
77
|
+
relationships: C4Relationship[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ----------------------------------------------------------------------------
|
|
81
|
+
// Positioned model (SVG layout output)
|
|
82
|
+
// ----------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
export interface PositionedC4Element extends C4Element {
|
|
85
|
+
x: number
|
|
86
|
+
y: number
|
|
87
|
+
width: number
|
|
88
|
+
height: number
|
|
89
|
+
/** Name wrapped to the box width, one entry per line. */
|
|
90
|
+
nameLines: string[]
|
|
91
|
+
/** Description wrapped to the box width, one entry per line. */
|
|
92
|
+
descriptionLines: string[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface PositionedC4Boundary {
|
|
96
|
+
alias: string
|
|
97
|
+
label: string
|
|
98
|
+
type?: string
|
|
99
|
+
description?: string
|
|
100
|
+
x: number
|
|
101
|
+
y: number
|
|
102
|
+
width: number
|
|
103
|
+
height: number
|
|
104
|
+
/** Nesting depth, 0 for a top-level boundary. */
|
|
105
|
+
depth: number
|
|
106
|
+
/** Centre line of the title, below the frame's top edge (Mermaid's layout). */
|
|
107
|
+
labelY?: number
|
|
108
|
+
/** Centre line of the `[type]` text, below the frame's top edge. */
|
|
109
|
+
typeY?: number
|
|
110
|
+
/** Centre line of the description (deployment nodes), below the top edge. */
|
|
111
|
+
descrY?: number
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface PositionedC4Relationship extends C4Relationship {
|
|
115
|
+
/** The start and end of the line (a straight chord when `curve` is unset). */
|
|
116
|
+
points: Point[]
|
|
117
|
+
/** Control point of a quadratic curve from the first to the last point. */
|
|
118
|
+
curve?: Point
|
|
119
|
+
/** Centre of the label block, when the relationship has label text. */
|
|
120
|
+
labelPosition?: Point
|
|
121
|
+
/** Centre x of the `[technology]` line, which Mermaid lays out on its own. */
|
|
122
|
+
technologyX?: number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface PositionedC4Diagram {
|
|
126
|
+
variant: C4Variant
|
|
127
|
+
title?: string
|
|
128
|
+
width: number
|
|
129
|
+
height: number
|
|
130
|
+
/** Where the title baseline sits (centre x, y), when there is a title. */
|
|
131
|
+
titlePosition?: Point
|
|
132
|
+
elements: PositionedC4Element[]
|
|
133
|
+
/** Outer boundaries first, so inner ones paint on top. */
|
|
134
|
+
boundaries: PositionedC4Boundary[]
|
|
135
|
+
relationships: PositionedC4Relationship[]
|
|
136
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
// creating a cycle — see `packages/core/src/direction.ts`'s header.
|
|
29
29
|
// ============================================================================
|
|
30
30
|
|
|
31
|
+
export * from './architecture/parser.ts'
|
|
32
|
+
export * from './architecture/types.ts'
|
|
33
|
+
export * from './architecture/to-graph.ts'
|
|
34
|
+
|
|
35
|
+
export * from './c4/parser.ts'
|
|
36
|
+
export * from './c4/types.ts'
|
|
37
|
+
export * from './c4/format.ts'
|
|
38
|
+
|
|
31
39
|
export * from './class/parser.ts'
|
|
32
40
|
export * from './class/types.ts'
|
|
33
41
|
export * from './class/format.ts'
|