@zombie-mermaid/svg-renderer 4.2.2 → 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.
Files changed (72) hide show
  1. package/README.md +15 -1
  2. package/dist/index.cjs +52 -52
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +23 -4
  5. package/dist/index.d.ts +23 -4
  6. package/dist/index.js +897 -849
  7. package/dist/index.js.map +1 -1
  8. package/package.json +14 -5
  9. package/src/__tests__/c4-label-clearance-1290.test.ts +0 -112
  10. package/src/__tests__/c4-line-ends-1208.test.ts +0 -52
  11. package/src/__tests__/c4-mermaid-reference.test.ts +0 -124
  12. package/src/__tests__/c4-svg.test.ts +0 -337
  13. package/src/__tests__/c4-text-width-1210.test.ts +0 -43
  14. package/src/__tests__/class-component-order-1249.test.ts +0 -40
  15. package/src/__tests__/class-diamond-markers-1232.test.ts +0 -59
  16. package/src/__tests__/class-member-right-padding-1238.test.ts +0 -43
  17. package/src/__tests__/class-namespace-1196.test.ts +0 -165
  18. package/src/__tests__/class-namespace-sparse-elk-1196.test.ts +0 -96
  19. package/src/__tests__/class-visibility-markers-1237.test.ts +0 -27
  20. package/src/__tests__/edge-jog-straightening-1388.test.ts +0 -57
  21. package/src/__tests__/elk-adapter-utils.test.ts +0 -166
  22. package/src/__tests__/er-attribute-right-padding-1262.test.ts +0 -47
  23. package/src/__tests__/er-cardinality-markers-1233.test.ts +0 -35
  24. package/src/__tests__/er-default-direction-1250.test.ts +0 -69
  25. package/src/__tests__/er-edge-label-contrast-1244.test.ts +0 -58
  26. package/src/__tests__/fixtures/c4-mermaid-reference.json +0 -626
  27. package/src/__tests__/flowchart-parallel-edge-lanes-1308.test.ts +0 -168
  28. package/src/__tests__/pie-layout.test.ts +0 -215
  29. package/src/__tests__/pie-svg.test.ts +0 -194
  30. package/src/__tests__/render-mermaid-svg.test.ts +0 -64
  31. package/src/__tests__/sequence-label-clearance-1242.test.ts +0 -162
  32. package/src/__tests__/sequence-nested-activation-1241.test.ts +0 -37
  33. package/src/__tests__/svg-edge-to-own-cluster-1310.test.ts +0 -32
  34. package/src/__tests__/xychart-smooth-curve.test.ts +0 -42
  35. package/src/__tests__/xychart-x-label-overlap-1243.test.ts +0 -62
  36. package/src/c4/arial-widths.ts +0 -59
  37. package/src/c4/layout.ts +0 -680
  38. package/src/c4/metrics.ts +0 -261
  39. package/src/c4/renderer.ts +0 -366
  40. package/src/class/layout.ts +0 -525
  41. package/src/class/renderer.ts +0 -669
  42. package/src/edge-curves.ts +0 -205
  43. package/src/elk-instance.ts +0 -292
  44. package/src/er/layout.ts +0 -216
  45. package/src/er/renderer.ts +0 -516
  46. package/src/generated/mono-font-subset.ts +0 -32
  47. package/src/index.ts +0 -277
  48. package/src/layout-engine/back-edges.ts +0 -73
  49. package/src/layout-engine/compound-flat.ts +0 -916
  50. package/src/layout-engine/constants.ts +0 -19
  51. package/src/layout-engine/edge-bundling.ts +0 -458
  52. package/src/layout-engine/elk-adapter-utils.ts +0 -81
  53. package/src/layout-engine/elk-graph-builder.ts +0 -245
  54. package/src/layout-engine/from-elk.ts +0 -767
  55. package/src/layout-engine/inner-edges.ts +0 -329
  56. package/src/layout-engine/jog-straightening.ts +0 -52
  57. package/src/layout-engine/layer-alignment.ts +0 -174
  58. package/src/layout-engine/layout-hints.ts +0 -27
  59. package/src/layout-engine/to-elk.ts +0 -855
  60. package/src/layout-engine.ts +0 -115
  61. package/src/pie/layout.ts +0 -234
  62. package/src/pie/renderer.ts +0 -161
  63. package/src/registry.ts +0 -441
  64. package/src/renderer.ts +0 -1510
  65. package/src/resolve-colors.ts +0 -339
  66. package/src/sequence/layout.ts +0 -766
  67. package/src/sequence/renderer.ts +0 -687
  68. package/src/shape-clipping.ts +0 -197
  69. package/src/styles.ts +0 -145
  70. package/src/title-gaps.ts +0 -161
  71. package/src/xychart/layout.ts +0 -727
  72. 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
- }
@@ -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
- }