@zombie-mermaid/svg-renderer 4.2.2 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +34 -1
- package/src/__tests__/c4-label-clearance-1290.test.ts +0 -112
- package/src/__tests__/c4-line-ends-1208.test.ts +0 -52
- package/src/__tests__/c4-mermaid-reference.test.ts +0 -124
- package/src/__tests__/c4-svg.test.ts +0 -337
- package/src/__tests__/c4-text-width-1210.test.ts +0 -43
- package/src/__tests__/class-component-order-1249.test.ts +0 -40
- package/src/__tests__/class-diamond-markers-1232.test.ts +0 -59
- package/src/__tests__/class-member-right-padding-1238.test.ts +0 -43
- package/src/__tests__/class-namespace-1196.test.ts +0 -165
- package/src/__tests__/class-namespace-sparse-elk-1196.test.ts +0 -96
- package/src/__tests__/class-visibility-markers-1237.test.ts +0 -27
- package/src/__tests__/edge-jog-straightening-1388.test.ts +0 -57
- package/src/__tests__/elk-adapter-utils.test.ts +0 -166
- package/src/__tests__/er-attribute-right-padding-1262.test.ts +0 -47
- package/src/__tests__/er-cardinality-markers-1233.test.ts +0 -35
- package/src/__tests__/er-default-direction-1250.test.ts +0 -69
- package/src/__tests__/er-edge-label-contrast-1244.test.ts +0 -58
- package/src/__tests__/fixtures/c4-mermaid-reference.json +0 -626
- package/src/__tests__/flowchart-parallel-edge-lanes-1308.test.ts +0 -168
- package/src/__tests__/pie-layout.test.ts +0 -215
- package/src/__tests__/pie-svg.test.ts +0 -194
- package/src/__tests__/render-mermaid-svg.test.ts +0 -64
- package/src/__tests__/sequence-label-clearance-1242.test.ts +0 -162
- package/src/__tests__/sequence-nested-activation-1241.test.ts +0 -37
- package/src/__tests__/svg-edge-to-own-cluster-1310.test.ts +0 -32
- package/src/__tests__/xychart-smooth-curve.test.ts +0 -42
- package/src/__tests__/xychart-x-label-overlap-1243.test.ts +0 -62
|
@@ -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
|
-
})
|