@zombie-mermaid/svg-renderer 4.2.3 → 5.0.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/README.md +15 -1
- package/dist/index.cjs +52 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +890 -850
- package/dist/index.js.map +1 -1
- package/package.json +14 -6
- package/src/c4/arial-widths.ts +0 -59
- package/src/c4/layout.ts +0 -680
- package/src/c4/metrics.ts +0 -261
- package/src/c4/renderer.ts +0 -366
- package/src/class/layout.ts +0 -525
- package/src/class/renderer.ts +0 -669
- package/src/edge-curves.ts +0 -205
- package/src/elk-instance.ts +0 -292
- package/src/er/layout.ts +0 -216
- package/src/er/renderer.ts +0 -516
- package/src/generated/mono-font-subset.ts +0 -32
- package/src/index.ts +0 -310
- package/src/layout-engine/back-edges.ts +0 -73
- package/src/layout-engine/compound-flat.ts +0 -916
- package/src/layout-engine/constants.ts +0 -19
- package/src/layout-engine/edge-bundling.ts +0 -458
- package/src/layout-engine/elk-adapter-utils.ts +0 -81
- package/src/layout-engine/elk-graph-builder.ts +0 -245
- package/src/layout-engine/from-elk.ts +0 -767
- package/src/layout-engine/inner-edges.ts +0 -329
- package/src/layout-engine/jog-straightening.ts +0 -52
- package/src/layout-engine/layer-alignment.ts +0 -174
- package/src/layout-engine/layout-hints.ts +0 -27
- package/src/layout-engine/to-elk.ts +0 -855
- package/src/layout-engine.ts +0 -115
- package/src/pie/layout.ts +0 -234
- package/src/pie/renderer.ts +0 -161
- package/src/registry.ts +0 -441
- package/src/renderer.ts +0 -1510
- package/src/resolve-colors.ts +0 -339
- package/src/sequence/layout.ts +0 -766
- package/src/sequence/renderer.ts +0 -687
- package/src/shape-clipping.ts +0 -197
- package/src/styles.ts +0 -145
- package/src/title-gaps.ts +0 -161
- package/src/xychart/layout.ts +0 -727
- package/src/xychart/renderer.ts +0 -676
package/src/c4/metrics.ts
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import type { C4Element } from '@zombie-mermaid/mermaid-parser'
|
|
2
|
-
import { estimateTextWidth } from '../styles.ts'
|
|
3
|
-
import { arialAdvance } from './arial-widths.ts'
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// C4 SVG geometry, shared by the layout (which sizes and places shapes) and
|
|
7
|
-
// the renderer (which draws text and glyphs inside them), so the two can't
|
|
8
|
-
// drift.
|
|
9
|
-
//
|
|
10
|
-
// The numbers follow Mermaid 11.17's C4 renderer: its default `c4` config for
|
|
11
|
-
// spacing, and sizes fitted to what it draws for each shape (a fixed 216px
|
|
12
|
-
// width, 20px padding, a 14px name, a 0.75em type line and a 0.82em
|
|
13
|
-
// description). Text is measured by summing Arial advances instead of in the
|
|
14
|
-
// browser, which matches the recorded Mermaid diagrams to a fraction of a pixel.
|
|
15
|
-
// ============================================================================
|
|
16
|
-
|
|
17
|
-
export const C4 = {
|
|
18
|
-
// Mermaid's default `c4` config.
|
|
19
|
-
width: 216,
|
|
20
|
-
diagramMarginX: 50,
|
|
21
|
-
diagramMarginY: 10,
|
|
22
|
-
shapeMargin: 50,
|
|
23
|
-
shapePadding: 20,
|
|
24
|
-
shapeInRow: 4,
|
|
25
|
-
boundaryInRow: 2,
|
|
26
|
-
nextLinePaddingX: 0,
|
|
27
|
-
/**
|
|
28
|
-
* Mermaid limits a row to the browser's `screen.availWidth`, which a server
|
|
29
|
-
* cannot know; a common desktop width keeps the output stable.
|
|
30
|
-
*/
|
|
31
|
-
screenWidth: 1440,
|
|
32
|
-
// Text.
|
|
33
|
-
nameSize: 14,
|
|
34
|
-
typeSize: 10.5,
|
|
35
|
-
descrSize: 11.5,
|
|
36
|
-
messageSize: 12,
|
|
37
|
-
boundarySize: 14,
|
|
38
|
-
// Line heights fitted to Mermaid: one more name line adds 15.35px, and a
|
|
39
|
-
// name plus its type line make 31.07px; each description line adds 17.1px.
|
|
40
|
-
nameLine: 15.35,
|
|
41
|
-
typeLine: 15.72,
|
|
42
|
-
descrLine: 17.1,
|
|
43
|
-
/** Each wrapped description line after the first (one `1.1em` row). */
|
|
44
|
-
descrExtraLine: 12.48,
|
|
45
|
-
/**
|
|
46
|
-
* For text outside printable ASCII, which has no Arial advance table:
|
|
47
|
-
* Arial is about 8% narrower than this package's width estimate (Inter).
|
|
48
|
-
*/
|
|
49
|
-
textScale: 0.922,
|
|
50
|
-
// Heights Mermaid measures for a boundary's heading text (fitted): the
|
|
51
|
-
// title at 16px bold, the `[type]` at 14px, a description at 12px.
|
|
52
|
-
boundaryLabelHeight: 18,
|
|
53
|
-
boundaryTypeHeight: 16.1,
|
|
54
|
-
boundaryDescrHeight: 14.4,
|
|
55
|
-
/** Height Mermaid reserves for the unnamed root boundary's heading. */
|
|
56
|
-
rootHeadingHeight: 47,
|
|
57
|
-
/** Extra height added to a diagram with a title (Mermaid's `extraVertForTitle`). */
|
|
58
|
-
titleExtra: 60,
|
|
59
|
-
// Shapes.
|
|
60
|
-
cornerRadius: 12,
|
|
61
|
-
strokeWidth: 2,
|
|
62
|
-
/**
|
|
63
|
-
* Person proportions, from Mermaid's person shape: the pill's corner is
|
|
64
|
-
* 17.7% of the width (at most 45% of the pill's height), the head's radius
|
|
65
|
-
* is 23% of the width clamped to 16..56, and the head overlaps the pill by
|
|
66
|
-
* 27% of its radius.
|
|
67
|
-
*/
|
|
68
|
-
personRx: 0.177,
|
|
69
|
-
personRxMaxOfHeight: 0.45,
|
|
70
|
-
personHeadRatio: 0.23,
|
|
71
|
-
personHeadMin: 16,
|
|
72
|
-
personHeadMax: 56,
|
|
73
|
-
personHeadOverlap: 0.27,
|
|
74
|
-
/** Cylinder height lost where its text block meets the lower ellipse. */
|
|
75
|
-
dbTrim: 3.91,
|
|
76
|
-
/** Queue height over its text block. */
|
|
77
|
-
queueExtra: 10,
|
|
78
|
-
} as const
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Width the text of an element wraps to: the shape's width less its padding
|
|
82
|
-
* on both sides (Mermaid's `c4LabelHelper`).
|
|
83
|
-
*/
|
|
84
|
-
export const C4_TEXT_WIDTH = C4.width - 2 * C4.shapePadding
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Greedy word wrap by estimated pixel width. Mermaid decides where to break
|
|
88
|
-
* with the text at regular weight and draws a name in bold afterwards, so a
|
|
89
|
-
* bold name can come out wider than the limit.
|
|
90
|
-
*/
|
|
91
|
-
export function wrapToWidth(
|
|
92
|
-
text: string,
|
|
93
|
-
maxWidth: number,
|
|
94
|
-
fontSize: number,
|
|
95
|
-
): string[] {
|
|
96
|
-
const lines: string[] = []
|
|
97
|
-
for (const paragraph of text.split('\n')) {
|
|
98
|
-
let cur = ''
|
|
99
|
-
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
100
|
-
const next = cur ? `${cur} ${word}` : word
|
|
101
|
-
if (cur && c4TextWidth(next, fontSize, 400) > maxWidth) {
|
|
102
|
-
lines.push(cur)
|
|
103
|
-
cur = word
|
|
104
|
-
} else {
|
|
105
|
-
cur = next
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
lines.push(cur)
|
|
109
|
-
}
|
|
110
|
-
return lines
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* A person's pill corner, head radius and pill top for a box of this width
|
|
115
|
-
* (and, when known, this height: a short pill gets smaller corners).
|
|
116
|
-
*/
|
|
117
|
-
export function c4PersonGeometry(
|
|
118
|
-
width: number,
|
|
119
|
-
height?: number,
|
|
120
|
-
): {
|
|
121
|
-
rx: number
|
|
122
|
-
headRadius: number
|
|
123
|
-
pillTop: number
|
|
124
|
-
} {
|
|
125
|
-
const headRadius = Math.min(
|
|
126
|
-
Math.max(width * C4.personHeadRatio, C4.personHeadMin),
|
|
127
|
-
C4.personHeadMax,
|
|
128
|
-
)
|
|
129
|
-
// The head's diameter less the part that overlaps the pill.
|
|
130
|
-
const pillTop = headRadius * (2 - C4.personHeadOverlap)
|
|
131
|
-
const pillHeight = height === undefined ? Infinity : height - pillTop
|
|
132
|
-
return {
|
|
133
|
-
rx: Math.min(width * C4.personRx, pillHeight * C4.personRxMaxOfHeight),
|
|
134
|
-
headRadius,
|
|
135
|
-
pillTop,
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface C4ShapeSize {
|
|
140
|
-
width: number
|
|
141
|
-
height: number
|
|
142
|
-
/** Height of the text block (name, type, description lines). */
|
|
143
|
-
textHeight: number
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Height of an element's text block for its wrapped lines. */
|
|
147
|
-
export function c4TextBlockHeight(
|
|
148
|
-
nameLines: number,
|
|
149
|
-
descriptionLines: number,
|
|
150
|
-
): number {
|
|
151
|
-
return (
|
|
152
|
-
nameLines * C4.nameLine +
|
|
153
|
-
C4.typeLine +
|
|
154
|
-
c4DescriptionHeight(descriptionLines)
|
|
155
|
-
)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Height of a description wrapped to this many lines. */
|
|
159
|
-
export function c4DescriptionHeight(lines: number): number {
|
|
160
|
-
return lines === 0 ? 0 : C4.descrLine + (lines - 1) * C4.descrExtraLine
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Cylinder cap radii for a box of this width: `rx` across, `ry` down. */
|
|
164
|
-
export function c4CylinderCap(width: number): { rx: number; ry: number } {
|
|
165
|
-
const rx = width / 2
|
|
166
|
-
return { rx, ry: rx / (2.5 + width / 50) }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Queue cap radii for a pipe of this height: `rx` across, `ry` down. */
|
|
170
|
-
export function c4QueueCap(height: number): { rx: number; ry: number } {
|
|
171
|
-
const ry = height / 2
|
|
172
|
-
return { rx: ry / (2.5 + height / 50), ry }
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Estimated width of a text line as Mermaid would measure it: sans-serif
|
|
177
|
-
* advances for printable ASCII, this package's own estimate (scaled to match)
|
|
178
|
-
* for anything else.
|
|
179
|
-
*/
|
|
180
|
-
export function c4TextWidth(
|
|
181
|
-
text: string,
|
|
182
|
-
fontSize: number,
|
|
183
|
-
weight: number,
|
|
184
|
-
): number {
|
|
185
|
-
let width = 0
|
|
186
|
-
for (const ch of text) {
|
|
187
|
-
const em = arialAdvance(ch, weight >= 600)
|
|
188
|
-
width +=
|
|
189
|
-
em === undefined
|
|
190
|
-
? estimateTextWidth(ch, fontSize, weight) * C4.textScale
|
|
191
|
-
: em * fontSize
|
|
192
|
-
}
|
|
193
|
-
return width
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* The size Mermaid gives an element: the standard width unless its text is
|
|
198
|
-
* wider (then the text plus padding both sides), and a height that follows
|
|
199
|
-
* its wrapped text lines.
|
|
200
|
-
*/
|
|
201
|
-
export function c4ShapeSize(
|
|
202
|
-
el: Pick<C4Element, 'kind' | 'shape'>,
|
|
203
|
-
nameLines: number,
|
|
204
|
-
descriptionLines: number,
|
|
205
|
-
textWidth: number,
|
|
206
|
-
): C4ShapeSize {
|
|
207
|
-
const textHeight = c4TextBlockHeight(nameLines, descriptionLines)
|
|
208
|
-
const boxHeight = textHeight + 2 * C4.shapePadding
|
|
209
|
-
const width = Math.max(C4.width, textWidth + 2 * C4.shapePadding)
|
|
210
|
-
if (el.kind === 'person') {
|
|
211
|
-
return {
|
|
212
|
-
width,
|
|
213
|
-
// The head rises above the pill, which is as tall as the text needs.
|
|
214
|
-
height: c4PersonGeometry(width).pillTop + boxHeight,
|
|
215
|
-
textHeight,
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (el.shape === 'db') {
|
|
219
|
-
const { ry } = c4CylinderCap(width)
|
|
220
|
-
return { width, height: boxHeight + 2 * ry - C4.dbTrim, textHeight }
|
|
221
|
-
}
|
|
222
|
-
if (el.shape === 'queue') {
|
|
223
|
-
const height = textHeight + C4.queueExtra
|
|
224
|
-
const { rx } = c4QueueCap(height)
|
|
225
|
-
return { width: width + 3 * rx, height, textHeight }
|
|
226
|
-
}
|
|
227
|
-
return { width, height: boxHeight, textHeight }
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Mermaid's C4 colours by kind and externality. */
|
|
231
|
-
export interface C4Palette {
|
|
232
|
-
fill: string
|
|
233
|
-
stroke: string
|
|
234
|
-
text: string
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function c4Palette(el: Pick<C4Element, 'kind' | 'external'>): C4Palette {
|
|
238
|
-
const text = '#ffffff'
|
|
239
|
-
if (el.external) {
|
|
240
|
-
switch (el.kind) {
|
|
241
|
-
case 'person':
|
|
242
|
-
return { fill: '#686868', stroke: '#8a8a8a', text }
|
|
243
|
-
case 'system':
|
|
244
|
-
return { fill: '#999999', stroke: '#8a8a8a', text }
|
|
245
|
-
case 'container':
|
|
246
|
-
return { fill: '#b3b3b3', stroke: '#a6a6a6', text }
|
|
247
|
-
case 'component':
|
|
248
|
-
return { fill: '#cccccc', stroke: '#bfbfbf', text }
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
switch (el.kind) {
|
|
252
|
-
case 'person':
|
|
253
|
-
return { fill: '#08427b', stroke: '#073b6f', text }
|
|
254
|
-
case 'system':
|
|
255
|
-
return { fill: '#1168bd', stroke: '#3c7fc0', text }
|
|
256
|
-
case 'container':
|
|
257
|
-
return { fill: '#438dd5', stroke: '#3c7fc0', text }
|
|
258
|
-
case 'component':
|
|
259
|
-
return { fill: '#85bbf0', stroke: '#78a8d8', text }
|
|
260
|
-
}
|
|
261
|
-
}
|
package/src/c4/renderer.ts
DELETED
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PositionedC4Boundary,
|
|
3
|
-
PositionedC4Diagram,
|
|
4
|
-
PositionedC4Element,
|
|
5
|
-
PositionedC4Relationship,
|
|
6
|
-
} from '@zombie-mermaid/mermaid-parser'
|
|
7
|
-
import {
|
|
8
|
-
c4BoundaryTypeLine,
|
|
9
|
-
c4RelLabelLines,
|
|
10
|
-
c4TypeLine,
|
|
11
|
-
} from '@zombie-mermaid/mermaid-parser'
|
|
12
|
-
import type { DiagramColors, Point, SvgEmitOptions } from '@zombie-mermaid/core'
|
|
13
|
-
import {
|
|
14
|
-
svgOpenTag,
|
|
15
|
-
buildStyleBlock,
|
|
16
|
-
escapeXml,
|
|
17
|
-
escapeAttr,
|
|
18
|
-
f,
|
|
19
|
-
} from '@zombie-mermaid/core'
|
|
20
|
-
import { withDataSrc } from '../renderer.ts'
|
|
21
|
-
import { FONT_SIZES, TEXT_BASELINE_SHIFT } from '../styles.ts'
|
|
22
|
-
import type { FontSizes } from '../styles.ts'
|
|
23
|
-
import {
|
|
24
|
-
C4,
|
|
25
|
-
c4CylinderCap,
|
|
26
|
-
c4DescriptionHeight,
|
|
27
|
-
c4Palette,
|
|
28
|
-
c4PersonGeometry,
|
|
29
|
-
c4QueueCap,
|
|
30
|
-
c4TextBlockHeight,
|
|
31
|
-
} from './metrics.ts'
|
|
32
|
-
import type { C4Palette } from './metrics.ts'
|
|
33
|
-
|
|
34
|
-
// ============================================================================
|
|
35
|
-
// C4 diagram SVG renderer, drawn the way Mermaid draws C4.
|
|
36
|
-
//
|
|
37
|
-
// Render order (as in Mermaid): boundaries, elements, then relationships,
|
|
38
|
-
// each with its label on top. Elements use Mermaid's C4 colours and stay
|
|
39
|
-
// fixed across themes, as C4 colours carry meaning; boundaries, lines and
|
|
40
|
-
// labels follow the theme's CSS variables.
|
|
41
|
-
// ============================================================================
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Render a positioned C4 diagram as an SVG string. Parameters mirror
|
|
45
|
-
* `renderErSvg`; see there for the `embedSource` / `title` / `decorative` /
|
|
46
|
-
* `emit` contract.
|
|
47
|
-
*/
|
|
48
|
-
export function renderC4Svg(
|
|
49
|
-
diagram: PositionedC4Diagram,
|
|
50
|
-
colors: DiagramColors,
|
|
51
|
-
font: string = 'Inter',
|
|
52
|
-
transparent: boolean = false,
|
|
53
|
-
_fontSizes: FontSizes = FONT_SIZES,
|
|
54
|
-
embedSource?: string,
|
|
55
|
-
title?: string,
|
|
56
|
-
decorative?: boolean,
|
|
57
|
-
emit: SvgEmitOptions = {},
|
|
58
|
-
): string {
|
|
59
|
-
const parts: string[] = []
|
|
60
|
-
parts.push(
|
|
61
|
-
withDataSrc(
|
|
62
|
-
svgOpenTag(
|
|
63
|
-
diagram.width,
|
|
64
|
-
diagram.height,
|
|
65
|
-
colors,
|
|
66
|
-
transparent,
|
|
67
|
-
title ?? diagram.title,
|
|
68
|
-
decorative,
|
|
69
|
-
undefined,
|
|
70
|
-
emit.styleAttribute,
|
|
71
|
-
),
|
|
72
|
-
embedSource,
|
|
73
|
-
),
|
|
74
|
-
)
|
|
75
|
-
parts.push(buildStyleBlock(font, false, emit.nonce))
|
|
76
|
-
|
|
77
|
-
if (diagram.title && diagram.titlePosition) {
|
|
78
|
-
// Mermaid sets the title as plain text at its start point.
|
|
79
|
-
parts.push(
|
|
80
|
-
f`<text class="c4-title" x="${diagram.titlePosition.x}" y="${diagram.titlePosition.y}" dy="${TEXT_BASELINE_SHIFT}" ` +
|
|
81
|
-
f`font-size="16" fill="var(--_text)">${escapeXml(diagram.title)}</text>`,
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
for (const b of diagram.boundaries) parts.push(renderBoundary(b))
|
|
86
|
-
for (const el of diagram.elements) parts.push(renderElement(el))
|
|
87
|
-
for (const r of diagram.relationships) {
|
|
88
|
-
parts.push(renderRelationshipLine(r))
|
|
89
|
-
parts.push(renderRelationshipLabel(r))
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
parts.push('</svg>')
|
|
93
|
-
return parts.filter(Boolean).join('\n')
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// Boundaries
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
/** One centred line of text. */
|
|
101
|
-
function centred(
|
|
102
|
-
text: string,
|
|
103
|
-
x: number,
|
|
104
|
-
y: number,
|
|
105
|
-
size: number,
|
|
106
|
-
extra: string,
|
|
107
|
-
): string {
|
|
108
|
-
return (
|
|
109
|
-
f` <text x="${x}" y="${y}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" font-size="${size}" ` +
|
|
110
|
-
f`${extra}>${escapeXml(text)}</text>`
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function renderBoundary(b: PositionedC4Boundary): string {
|
|
115
|
-
const parts: string[] = []
|
|
116
|
-
parts.push(
|
|
117
|
-
f`<g class="c4-boundary" data-id="${escapeAttr(b.alias)}" data-label="${escapeAttr(b.label)}" data-depth="${b.depth}">`,
|
|
118
|
-
)
|
|
119
|
-
// A dashed square-cornered frame with no fill, as Mermaid draws it.
|
|
120
|
-
parts.push(
|
|
121
|
-
f` <rect x="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}" rx="2.5" ry="2.5" ` +
|
|
122
|
-
`fill="none" stroke="var(--_text)" stroke-width="1" stroke-dasharray="7,7" />`,
|
|
123
|
-
)
|
|
124
|
-
const cx = b.x + b.width / 2
|
|
125
|
-
parts.push(
|
|
126
|
-
centred(
|
|
127
|
-
b.label,
|
|
128
|
-
cx,
|
|
129
|
-
b.y + (b.labelY ?? 8),
|
|
130
|
-
C4.boundarySize + 2,
|
|
131
|
-
'font-weight="700" fill="var(--_text)"',
|
|
132
|
-
),
|
|
133
|
-
)
|
|
134
|
-
const typeLine = c4BoundaryTypeLine({
|
|
135
|
-
alias: b.alias,
|
|
136
|
-
label: b.label,
|
|
137
|
-
...(b.type ? { type: b.type } : {}),
|
|
138
|
-
elementAliases: [],
|
|
139
|
-
children: [],
|
|
140
|
-
})
|
|
141
|
-
if (typeLine && b.typeY !== undefined) {
|
|
142
|
-
parts.push(
|
|
143
|
-
centred(
|
|
144
|
-
typeLine,
|
|
145
|
-
cx,
|
|
146
|
-
b.y + b.typeY,
|
|
147
|
-
C4.boundarySize,
|
|
148
|
-
'fill="var(--_text)"',
|
|
149
|
-
),
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
if (b.description && b.descrY !== undefined) {
|
|
153
|
-
parts.push(
|
|
154
|
-
centred(
|
|
155
|
-
b.description,
|
|
156
|
-
cx,
|
|
157
|
-
b.y + b.descrY,
|
|
158
|
-
C4.boundarySize - 2,
|
|
159
|
-
'fill="var(--_text)"',
|
|
160
|
-
),
|
|
161
|
-
)
|
|
162
|
-
}
|
|
163
|
-
parts.push('</g>')
|
|
164
|
-
return parts.join('\n')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
// Elements
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
|
|
171
|
-
function renderElement(el: PositionedC4Element): string {
|
|
172
|
-
const pal = c4Palette(el)
|
|
173
|
-
const { x, y, width: w, height: h } = el
|
|
174
|
-
const cx = x + w / 2
|
|
175
|
-
const textH = c4TextBlockHeight(
|
|
176
|
-
el.nameLines.length,
|
|
177
|
-
el.descriptionLines.length,
|
|
178
|
-
)
|
|
179
|
-
const parts: string[] = []
|
|
180
|
-
parts.push(
|
|
181
|
-
f`<g class="c4-element" data-id="${escapeAttr(el.alias)}" data-kind="${el.kind}" data-shape="${el.shape}" ` +
|
|
182
|
-
f`data-external="${el.external}" data-label="${escapeAttr(el.label)}">`,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
// Where the text block starts, and the x it is centred on.
|
|
186
|
-
let top: number
|
|
187
|
-
let textCx = cx
|
|
188
|
-
if (el.kind === 'person') {
|
|
189
|
-
parts.push(...personShape(el, pal))
|
|
190
|
-
const g = c4PersonGeometry(w)
|
|
191
|
-
top = y + g.pillTop + C4.shapePadding
|
|
192
|
-
} else if (el.shape === 'db') {
|
|
193
|
-
parts.push(...cylinderShape(el, pal))
|
|
194
|
-
const { ry } = c4CylinderCap(w)
|
|
195
|
-
// Centred in the body below the upper ellipse.
|
|
196
|
-
top = y + (2 * ry + h) / 2 - textH / 2
|
|
197
|
-
} else if (el.shape === 'queue') {
|
|
198
|
-
parts.push(...queueShape(el, pal))
|
|
199
|
-
const { rx } = c4QueueCap(h)
|
|
200
|
-
// Mermaid centres the text one cap radius left of the pipe's centre.
|
|
201
|
-
textCx = cx - rx
|
|
202
|
-
top = y + h / 2 - textH / 2
|
|
203
|
-
} else {
|
|
204
|
-
parts.push(
|
|
205
|
-
f` <rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${C4.cornerRadius}" ry="${C4.cornerRadius}" ` +
|
|
206
|
-
f`fill="${pal.fill}" stroke="${pal.stroke}" stroke-width="${C4.strokeWidth}" />`,
|
|
207
|
-
)
|
|
208
|
-
top = y + C4.shapePadding
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
let cursor = top
|
|
212
|
-
for (const line of el.nameLines) {
|
|
213
|
-
parts.push(
|
|
214
|
-
centred(
|
|
215
|
-
line,
|
|
216
|
-
textCx,
|
|
217
|
-
cursor + C4.nameLine / 2,
|
|
218
|
-
C4.nameSize,
|
|
219
|
-
f`font-weight="700" fill="${pal.text}"`,
|
|
220
|
-
),
|
|
221
|
-
)
|
|
222
|
-
cursor += C4.nameLine
|
|
223
|
-
}
|
|
224
|
-
parts.push(
|
|
225
|
-
centred(
|
|
226
|
-
c4TypeLine(el),
|
|
227
|
-
textCx,
|
|
228
|
-
cursor + C4.typeLine / 2,
|
|
229
|
-
C4.typeSize,
|
|
230
|
-
f`fill="${pal.text}"`,
|
|
231
|
-
),
|
|
232
|
-
)
|
|
233
|
-
cursor += C4.typeLine
|
|
234
|
-
el.descriptionLines.forEach((line, i) => {
|
|
235
|
-
// The first line is as tall as Mermaid measures a one-line text; each
|
|
236
|
-
// wrapped line after it is one `1.1em` row.
|
|
237
|
-
parts.push(
|
|
238
|
-
centred(
|
|
239
|
-
line,
|
|
240
|
-
textCx,
|
|
241
|
-
cursor + C4.descrLine / 2 + i * C4.descrExtraLine,
|
|
242
|
-
C4.descrSize,
|
|
243
|
-
f`fill="${pal.text}"`,
|
|
244
|
-
),
|
|
245
|
-
)
|
|
246
|
-
})
|
|
247
|
-
cursor += c4DescriptionHeight(el.descriptionLines.length)
|
|
248
|
-
parts.push('</g>')
|
|
249
|
-
return parts.join('\n')
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/** A rounded pill with a round head rising out of its top, as Mermaid draws a person. */
|
|
253
|
-
function personShape(el: PositionedC4Element, pal: C4Palette): string[] {
|
|
254
|
-
const { x, width: w } = el
|
|
255
|
-
const { rx, headRadius, pillTop: rise } = c4PersonGeometry(w, el.height)
|
|
256
|
-
const y = el.y
|
|
257
|
-
const cx = x + w / 2
|
|
258
|
-
const pillTop = y + rise
|
|
259
|
-
const pillHeight = el.y + el.height - pillTop
|
|
260
|
-
const style = f`fill="${pal.fill}" stroke="${pal.stroke}" stroke-width="${C4.strokeWidth}"`
|
|
261
|
-
return [
|
|
262
|
-
f` <rect x="${x}" y="${pillTop}" width="${w}" height="${pillHeight}" rx="${rx}" ry="${rx}" ${style} />`,
|
|
263
|
-
f` <circle cx="${cx}" cy="${y + headRadius}" r="${headRadius}" ${style} />`,
|
|
264
|
-
]
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function cylinderShape(el: PositionedC4Element, pal: C4Palette): string[] {
|
|
268
|
-
const { x, y, width: w, height: h } = el
|
|
269
|
-
const { rx, ry } = c4CylinderCap(w)
|
|
270
|
-
const style = f`fill="${pal.fill}" stroke="${pal.stroke}" stroke-width="${C4.strokeWidth}"`
|
|
271
|
-
return [
|
|
272
|
-
f` <path d="M ${x} ${y + ry} L ${x} ${y + h - ry} A ${rx} ${ry} 0 0 0 ${x + w} ${y + h - ry} L ${x + w} ${y + ry} Z" ${style} />`,
|
|
273
|
-
f` <ellipse cx="${x + rx}" cy="${y + ry}" rx="${rx}" ry="${ry}" ${style} />`,
|
|
274
|
-
]
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* A pipe on its side, as Mermaid draws it: rounded at the left, with the
|
|
279
|
-
* ellipse face showing at the right end.
|
|
280
|
-
*/
|
|
281
|
-
function queueShape(el: PositionedC4Element, pal: C4Palette): string[] {
|
|
282
|
-
const { x, y, width: w, height: h } = el
|
|
283
|
-
const { rx, ry } = c4QueueCap(h)
|
|
284
|
-
const style = f`fill="${pal.fill}" stroke="${pal.stroke}" stroke-width="${C4.strokeWidth}"`
|
|
285
|
-
return [
|
|
286
|
-
f` <path d="M ${x + w - rx} ${y} H ${x + rx} A ${rx} ${ry} 0 0 0 ${x + rx} ${y + h} H ${x + w - rx} Z" ${style} />`,
|
|
287
|
-
f` <ellipse cx="${x + w - rx}" cy="${y + ry}" rx="${rx}" ry="${ry}" ${style} />`,
|
|
288
|
-
]
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
|
-
// Relationships
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
/** Mermaid's arrowhead: a 10 by 10 triangle whose tip sits on the line's end. */
|
|
296
|
-
function arrowHead(tip: Point, from: Point): string {
|
|
297
|
-
const dx = tip.x - from.x
|
|
298
|
-
const dy = tip.y - from.y
|
|
299
|
-
const len = Math.hypot(dx, dy)
|
|
300
|
-
if (len === 0) return ''
|
|
301
|
-
const ux = dx / len
|
|
302
|
-
const uy = dy / len
|
|
303
|
-
const L = 10
|
|
304
|
-
const W = 5
|
|
305
|
-
const bx = tip.x - ux * L
|
|
306
|
-
const by = tip.y - uy * L
|
|
307
|
-
return (
|
|
308
|
-
f`<polygon class="c4-arrowhead" points="${tip.x},${tip.y} ${bx - uy * W},${by + ux * W} ${bx + uy * W},${by - ux * W}" ` +
|
|
309
|
-
`fill="var(--_text)" />`
|
|
310
|
-
)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function renderRelationshipLine(rel: PositionedC4Relationship): string {
|
|
314
|
-
const [start, end] = [rel.points[0], rel.points[rel.points.length - 1]]
|
|
315
|
-
if (!start || !end || start === end) return ''
|
|
316
|
-
const attrs = [
|
|
317
|
-
'class="c4-relationship"',
|
|
318
|
-
f`data-from="${escapeAttr(rel.from)}"`,
|
|
319
|
-
f`data-to="${escapeAttr(rel.to)}"`,
|
|
320
|
-
]
|
|
321
|
-
if (rel.label) attrs.push(f`data-label="${escapeAttr(rel.label)}"`)
|
|
322
|
-
if (rel.technology) {
|
|
323
|
-
attrs.push(f`data-technology="${escapeAttr(rel.technology)}"`)
|
|
324
|
-
}
|
|
325
|
-
const d = rel.curve
|
|
326
|
-
? f`M ${start.x},${start.y} Q ${rel.curve.x},${rel.curve.y} ${end.x},${end.y}`
|
|
327
|
-
: f`M ${start.x},${start.y} L ${end.x},${end.y}`
|
|
328
|
-
const line = f`<path ${attrs.join(' ')} d="${d}" fill="none" stroke="var(--_text)" stroke-width="1" />`
|
|
329
|
-
// A curve leaves and arrives along its control point.
|
|
330
|
-
const beforeEnd = rel.curve ?? start
|
|
331
|
-
const afterStart = rel.curve ?? end
|
|
332
|
-
const endHead = arrowHead(end, beforeEnd)
|
|
333
|
-
const startHead = arrowHead(start, afterStart)
|
|
334
|
-
const heads = rel.bidirectional
|
|
335
|
-
? [endHead, startHead]
|
|
336
|
-
: [rel.reversed ? startHead : endHead]
|
|
337
|
-
return [line, ...heads].filter(Boolean).join('\n')
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/** Plain text at the middle of the chord: the label, then `[technology]` below. */
|
|
341
|
-
function renderRelationshipLabel(rel: PositionedC4Relationship): string {
|
|
342
|
-
const lines = c4RelLabelLines(rel)
|
|
343
|
-
if (lines.length === 0 || !rel.labelPosition) return ''
|
|
344
|
-
const { x, y } = rel.labelPosition
|
|
345
|
-
const parts = [
|
|
346
|
-
f`<g class="c4-relationship-label" data-from="${escapeAttr(rel.from)}" data-to="${escapeAttr(rel.to)}">`,
|
|
347
|
-
]
|
|
348
|
-
lines.forEach((line, i) => {
|
|
349
|
-
const isTech = rel.technology !== undefined && i === lines.length - 1
|
|
350
|
-
// Mermaid puts the technology one font size plus 5px below the label.
|
|
351
|
-
const cy = y + (i === 0 ? 0 : C4.messageSize + 5)
|
|
352
|
-
parts.push(
|
|
353
|
-
centred(
|
|
354
|
-
line,
|
|
355
|
-
isTech && rel.technologyX !== undefined ? rel.technologyX : x,
|
|
356
|
-
cy,
|
|
357
|
-
C4.messageSize,
|
|
358
|
-
isTech
|
|
359
|
-
? 'font-style="italic" fill="var(--_text)"'
|
|
360
|
-
: 'fill="var(--_text)"',
|
|
361
|
-
),
|
|
362
|
-
)
|
|
363
|
-
})
|
|
364
|
-
parts.push('</g>')
|
|
365
|
-
return parts.join('\n')
|
|
366
|
-
}
|