@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
@@ -1,162 +0,0 @@
1
- /**
2
- * #1242: lifelines were drawn straight through message labels and block
3
- * titles, and a stick-figure actor's label sat on the first message arrow.
4
- * The visual suite's tolerance can't see either, so assert the geometry.
5
- */
6
- import { describe, it, expect } from 'vitest'
7
- import { parseSequenceDiagram } from '@zombie-mermaid/mermaid-parser'
8
- import { renderMermaidSVG } from '../../../../src/index.ts'
9
- import { layoutSequenceDiagram } from '../sequence/layout.ts'
10
-
11
- function lifelineSegments(
12
- svg: string,
13
- actor: string,
14
- ): Array<{ x: number; y1: number; y2: number }> {
15
- const out: Array<{ x: number; y1: number; y2: number }> = []
16
- const re =
17
- /<line class="lifeline" data-actor="([^"]*)" x1="([\d.]+)" y1="([\d.]+)" x2="[\d.]+" y2="([\d.]+)"/g
18
- for (const m of svg.matchAll(re)) {
19
- if (m[1] !== actor) continue
20
- out.push({ x: Number(m[2]), y1: Number(m[3]), y2: Number(m[4]) })
21
- }
22
- return out
23
- }
24
-
25
- /** Centre y of the `<text>` whose content is `label`. */
26
- function textY(svg: string, label: string): number {
27
- for (const m of svg.matchAll(
28
- /<text [^>]*y="([\d.]+)"[^>]*>([^<]*)<\/text>/g,
29
- )) {
30
- if (m[2] === label) return Number(m[1])
31
- }
32
- return NaN
33
- }
34
-
35
- /** True when no lifeline segment of `actor` covers height `y`. */
36
- function lifelineClearAt(svg: string, actor: string, y: number): boolean {
37
- return lifelineSegments(svg, actor).every((s) => y < s.y1 || y > s.y2)
38
- }
39
-
40
- describe('lifelines stay out of message labels (#1242)', () => {
41
- const svg = renderMermaidSVG(`sequenceDiagram
42
- participant A
43
- participant B
44
- participant C
45
- A->>B: first
46
- A->>C: label crossing the middle lifeline
47
- A->>B: next`)
48
-
49
- it('cuts the crossed lifeline around the label instead of running through it', () => {
50
- const labelY = textY(svg, 'label crossing the middle lifeline')
51
- expect(lifelineSegments(svg, 'B').length).toBeGreaterThan(1)
52
- expect(lifelineClearAt(svg, 'B', labelY)).toBe(true)
53
- })
54
-
55
- it('leaves a lifeline whole where no label crosses it', () => {
56
- expect(lifelineSegments(svg, 'A')).toHaveLength(1)
57
- })
58
-
59
- it('cuts lifelines around a block title tab', () => {
60
- const blockSvg = renderMermaidSVG(`sequenceDiagram
61
- participant A
62
- participant B
63
- A->>B: hi
64
- alt Multiple panels
65
- B->>A: yes
66
- end`)
67
- // The tab sits at the block's top-left; the lifeline under it is cut.
68
- const rect = blockSvg.match(
69
- /<g class="block"[^>]*>\s*<rect x="([\d.]+)" y="([\d.]+)"/,
70
- )
71
- const blockY = Number(rect?.[2])
72
- expect(lifelineSegments(blockSvg, 'A').length).toBeGreaterThan(1)
73
- expect(lifelineClearAt(blockSvg, 'A', blockY + 9)).toBe(true)
74
- })
75
-
76
- it('cuts a lifeline beside a self-message label', () => {
77
- const selfSvg = renderMermaidSVG(`sequenceDiagram
78
- participant A
79
- participant B
80
- A->>A: a long self message label crossing B
81
- A->>B: after`)
82
- const y = textY(selfSvg, 'a long self message label crossing B')
83
- expect(lifelineSegments(selfSvg, 'B').length).toBeGreaterThan(1)
84
- expect(lifelineClearAt(selfSvg, 'B', y)).toBe(true)
85
- })
86
-
87
- it('cuts lifelines around a bare divider label', () => {
88
- const divSvg = renderMermaidSVG(`sequenceDiagram
89
- participant A
90
- participant B
91
- alt first
92
- A->>B: x
93
- else other case
94
- B->>A: y
95
- end`)
96
- const y = textY(divSvg, '[other case]')
97
- expect(lifelineSegments(divSvg, 'A').length).toBeGreaterThan(1)
98
- expect(lifelineClearAt(divSvg, 'A', y)).toBe(true)
99
- })
100
-
101
- it('ignores unlabeled blocks and dividers', () => {
102
- const bareSvg = renderMermaidSVG(`sequenceDiagram
103
- participant A
104
- participant B
105
- loop
106
- B->>A: ping
107
- else
108
- A->>B: pong
109
- end`)
110
- expect(lifelineSegments(bareSvg, 'A').length).toBeGreaterThan(0)
111
- expect(lifelineSegments(bareSvg, 'B').length).toBeGreaterThan(0)
112
- })
113
-
114
- it('never drops a lifeline entirely, even under a label taller than it', () => {
115
- const lines = Array.from({ length: 8 }, (_, i) => `l${i}`).join('<br/>')
116
- const tall = renderMermaidSVG(`sequenceDiagram
117
- participant A
118
- participant B
119
- participant C
120
- A->>C: ${lines}`)
121
- // The label spans B's whole lifeline; B keeps one uncut line.
122
- expect(lifelineSegments(tall, 'B')).toHaveLength(1)
123
- })
124
- })
125
-
126
- describe('stick-figure actor label clears the first message (#1242)', () => {
127
- const src = `sequenceDiagram
128
- actor U as User
129
- participant S as System
130
- U->>S: Click button`
131
- const svg = renderMermaidSVG(src)
132
- const layout = layoutSequenceDiagram(
133
- parseSequenceDiagram(
134
- src
135
- .split('\n')
136
- .slice(1)
137
- .map((text, line) => ({ text: text.trim(), line })),
138
- ),
139
- )
140
-
141
- it('starts the actor lifeline below its label', () => {
142
- const [seg] = lifelineSegments(svg, 'U')
143
- expect(seg!.y1).toBeGreaterThan(textY(svg, 'User') + 6)
144
- })
145
-
146
- it('places the first message arrow clear of the label', () => {
147
- const first = layout.messages[0]!
148
- // label is 13px-ish text centred at textY; arrow must be well below it
149
- expect(first.y).toBeGreaterThan(textY(svg, 'User') + 16)
150
- })
151
-
152
- it('keeps the original compact header when no stick figure is used', () => {
153
- const plain = layoutSequenceDiagram(
154
- parseSequenceDiagram([
155
- { text: 'participant A', line: 0 },
156
- { text: 'participant B', line: 1 },
157
- { text: 'A->>B: hi', line: 2 },
158
- ]),
159
- )
160
- expect(plain.messages[0]!.y - (plain.actors[0]!.y + 40)).toBe(20)
161
- })
162
- })
@@ -1,37 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { parseSequenceDiagram } from '@zombie-mermaid/mermaid-parser'
3
- import { renderMermaidSVG } from '../../../../src/index.ts'
4
- import { layoutSequenceDiagram } from '../sequence/layout.ts'
5
-
6
- const SRC = [
7
- 'participant C as Client',
8
- 'participant S as Server',
9
- 'C->>+S: Request',
10
- 'S->>+S: Process',
11
- 'S->>-S: Done',
12
- 'S-->>-C: Response',
13
- ]
14
-
15
- describe('nested self-message activation (#1241)', () => {
16
- const layout = layoutSequenceDiagram(
17
- parseSequenceDiagram(SRC.map((text, line) => ({ text, line }))),
18
- )
19
- const server = layout.activations.filter((a) => a.actorId === 'S')
20
-
21
- it('yields two activations on the actor with depths 0 and 1', () => {
22
- expect(server.map((a) => a.depth)).toEqual([0, 1])
23
- })
24
-
25
- it('offsets the nested bar right by half a bar, inside the parent span', () => {
26
- const outer = server.find((a) => a.depth === 0)!
27
- const inner = server.find((a) => a.depth === 1)!
28
- expect(inner.x - outer.x).toBe(outer.width / 2)
29
- expect(inner.topY).toBeGreaterThan(outer.topY)
30
- expect(inner.bottomY).toBeLessThan(outer.bottomY)
31
- })
32
-
33
- it('draws both bars, outer first so the nested one is painted on top', () => {
34
- const svg = renderMermaidSVG(`sequenceDiagram\n ${SRC.join('\n ')}`)
35
- expect(svg.match(/class="activation"/g)).toHaveLength(2)
36
- })
37
- })
@@ -1,32 +0,0 @@
1
- /**
2
- * #1310 — real mermaid.js (11.17.2) draws no line for an edge between a node
3
- * and a subgraph that contains it (the edge path is zero-length). The SVG
4
- * renderer used to draw degenerate stubs for it; it now omits the edge, so the
5
- * output equals the same diagram without that edge.
6
- */
7
- import { describe, it, expect } from 'vitest'
8
- import { renderMermaidSVG } from '../../../../src/index.ts'
9
-
10
- const base = 'flowchart TD; subgraph Sub; B; end; A --> B'
11
-
12
- describe('SVG: an edge between a node and its own subgraph (#1310)', () => {
13
- it.each([
14
- ['member to subgraph', `${base}; B --> Sub`],
15
- ['subgraph to member', `${base}; Sub --> B`],
16
- [
17
- 'nested containment',
18
- 'flowchart TD; subgraph Outer; subgraph Inner; B; end; end; A --> B; B --> Outer; Inner --> B; Outer --> Inner',
19
- ],
20
- ])('renders %s identically to the diagram without the edge', (_, src) => {
21
- const without = src.includes('Outer')
22
- ? 'flowchart TD; subgraph Outer; subgraph Inner; B; end; end; A --> B'
23
- : base
24
- expect(renderMermaidSVG(src)).toBe(renderMermaidSVG(without))
25
- })
26
-
27
- it('still draws an edge from an outside node to the subgraph', () => {
28
- expect(renderMermaidSVG(`${base}; X --> Sub`)).not.toBe(
29
- renderMermaidSVG(base),
30
- )
31
- })
32
- })
@@ -1,42 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { smoothCurvePath } from '../xychart/renderer.ts'
3
-
4
- /** Every y coordinate in the path (Bezier control points bound the curve). */
5
- function pathYs(d: string): number[] {
6
- return [...d.matchAll(/(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)/g)].map((m) =>
7
- Number(m[2]),
8
- )
9
- }
10
-
11
- function expectWithinRange(values: number[]) {
12
- const points = values.map((y, i) => ({ x: i * 50, y }))
13
- const ys = pathYs(smoothCurvePath(points))
14
- const lo = Math.min(...values)
15
- const hi = Math.max(...values)
16
- for (const y of ys) {
17
- expect(y).toBeGreaterThanOrEqual(lo - 0.01)
18
- expect(y).toBeLessThanOrEqual(hi + 0.01)
19
- }
20
- }
21
-
22
- describe('smoothCurvePath', () => {
23
- it('stays within the data range on a sharp spike after a flat run', () => {
24
- expectWithinRange([5, 12, 45, 380, 420, 250, 35, 8])
25
- expectWithinRange([0, 0, 0, 100, 0, 0, 0])
26
- })
27
-
28
- it('stays within the data range on a flat run and alternating values', () => {
29
- expectWithinRange([10, 10, 10, 10, 10])
30
- expectWithinRange([0, 100, 0, 100, 0])
31
- })
32
-
33
- it('passes through every data point', () => {
34
- const d = smoothCurvePath([
35
- { x: 0, y: 10 },
36
- { x: 50, y: 80 },
37
- { x: 100, y: 20 },
38
- ])
39
- expect(d).toContain('50,80')
40
- expect(d.endsWith('100,20')).toBe(true)
41
- })
42
- })
@@ -1,62 +0,0 @@
1
- /**
2
- * #1243: with many categories the x-axis labels touched. The visual suite's
3
- * tolerance can't see labels moving a few pixels, so assert on the layout.
4
- */
5
- import { describe, it, expect } from 'vitest'
6
- import { parseXYChart } from '@zombie-mermaid/mermaid-parser'
7
- import { splitStatements } from '@zombie-mermaid/core'
8
- import { layoutXYChart } from '../xychart/layout.ts'
9
- import { estimateTextWidth } from '../styles.ts'
10
-
11
- function layout(src: string) {
12
- return layoutXYChart(parseXYChart(splitStatements(src)))
13
- }
14
-
15
- function shown(src: string) {
16
- return layout(src).xAxis.ticks.filter((t) => t.label !== '')
17
- }
18
-
19
- function minGap(ticks: ReturnType<typeof shown>): number {
20
- let gap = Infinity
21
- for (let i = 1; i < ticks.length; i++) {
22
- const a = ticks[i - 1]!
23
- const b = ticks[i]!
24
- const half =
25
- (estimateTextWidth(a.label, 14, 400) +
26
- estimateTextWidth(b.label, 14, 400)) /
27
- 2
28
- gap = Math.min(gap, b.labelX - a.labelX - half)
29
- }
30
- return gap
31
- }
32
-
33
- const QUARTERS = `xychart-beta
34
- x-axis [21Q1, 21Q2, 21Q3, 21Q4, 22Q1, 22Q2, 22Q3, 22Q4, 23Q1, 23Q2, 23Q3, 23Q4, 24Q1, 24Q2, 24Q3, 24Q4]
35
- bar [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]`
36
-
37
- describe('xychart x-axis label spacing (#1243)', () => {
38
- it('drops labels so adjacent shown labels do not touch', () => {
39
- const ticks = shown(QUARTERS)
40
- expect(ticks.length).toBeLessThan(16)
41
- expect(ticks.length).toBeGreaterThan(1)
42
- expect(minGap(ticks)).toBeGreaterThan(0)
43
- })
44
-
45
- it('keeps every label when they all fit', () => {
46
- const src = `xychart-beta
47
- x-axis [Q1, Q2, Q3, Q4]
48
- bar [1, 2, 3, 4]`
49
- expect(shown(src).map((t) => t.label)).toEqual(['Q1', 'Q2', 'Q3', 'Q4'])
50
- })
51
-
52
- it('does not thin a roomy axis because of one long label', () => {
53
- const src = `xychart-beta
54
- x-axis [A, B, "A Rather Long Category", D, E]
55
- bar [1, 2, 3, 4, 5]`
56
- expect(shown(src)).toHaveLength(5)
57
- })
58
-
59
- it('keeps the tick marks of dropped labels', () => {
60
- expect(layout(QUARTERS).xAxis.ticks).toHaveLength(16)
61
- })
62
- })
@@ -1,59 +0,0 @@
1
- // Advance widths, in 1/2048 em, of Arial (the metrics Liberation Sans and
2
- // Helvetica share) for the printable ASCII range, regular and bold. Mermaid's
3
- // C4 renderer measures its text in the browser, and the 32 diagrams recorded
4
- // in `__tests__/fixtures/c4-mermaid-reference.json` are laid out to within a
5
- // fraction of a pixel by summing these advances, which a scaled copy of this
6
- // package's own Inter-based estimate could not do (see `c4TextWidth`).
7
- const UNITS_PER_EM = 2048
8
-
9
- const REGULAR =
10
- // Indexed from U+0020: ` !"#$%&'()*+,-./`
11
- [
12
- 569, 569, 727, 1139, 1139, 1821, 1366, 391, 682, 682, 797, 1196, 569, 682,
13
- 569, 569,
14
- // 0-9
15
- 1139, 1139, 1139, 1139, 1139, 1139, 1139, 1139, 1139, 1139,
16
- // :;<=>?@
17
- 569, 569, 1196, 1196, 1196, 1139, 2079,
18
- // A-Z
19
- 1366, 1366, 1479, 1479, 1366, 1251, 1593, 1479, 569, 1024, 1366, 1139, 1706,
20
- 1479, 1593, 1366, 1593, 1479, 1366, 1251, 1479, 1366, 1933, 1366, 1366,
21
- 1251,
22
- // [\]^_`
23
- 569, 569, 569, 961, 1139, 682,
24
- // a-z
25
- 1139, 1139, 1024, 1139, 1139, 569, 1139, 1139, 455, 455, 1024, 455, 1706,
26
- 1139, 1139, 1139, 1139, 682, 1024, 569, 1139, 1024, 1479, 1024, 1024, 1024,
27
- // {|}~
28
- 684, 532, 684, 1196,
29
- ]
30
-
31
- const BOLD = [
32
- // ` !"#$%&'()*+,-./`
33
- 569, 682, 971, 1139, 1139, 1821, 1479, 487, 682, 682, 797, 1196, 569, 682,
34
- 569, 569,
35
- // 0-9
36
- 1139, 1139, 1139, 1139, 1139, 1139, 1139, 1139, 1139, 1139,
37
- // :;<=>?@
38
- 682, 682, 1196, 1196, 1196, 1251, 1992,
39
- // A-Z
40
- 1479, 1479, 1479, 1479, 1366, 1251, 1593, 1479, 569, 1139, 1479, 1251, 1706,
41
- 1479, 1593, 1366, 1593, 1479, 1366, 1251, 1479, 1366, 1933, 1366, 1366, 1251,
42
- // [\]^_`
43
- 682, 569, 682, 1196, 1139, 682,
44
- // a-z
45
- 1139, 1251, 1139, 1251, 1139, 682, 1251, 1251, 569, 569, 1139, 569, 1821,
46
- 1251, 1251, 1251, 1251, 797, 1139, 682, 1251, 1139, 1593, 1139, 1139, 1024,
47
- // {|}~
48
- 801, 573, 801, 1196,
49
- ]
50
-
51
- /**
52
- * Width in em of `ch`, or `undefined` outside printable ASCII (the caller
53
- * falls back to its own estimate).
54
- */
55
- export function arialAdvance(ch: string, bold: boolean): number | undefined {
56
- const code = ch.codePointAt(0)!
57
- if (code < 0x20 || code > 0x7e) return undefined
58
- return (bold ? BOLD : REGULAR)[code - 0x20]! / UNITS_PER_EM
59
- }