@zombie-mermaid/mermaid-parser 3.1.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.
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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'