@zombie-mermaid/svg-renderer 4.2.1 → 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 +42 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +695 -652
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +34 -1
- package/src/layout-engine/from-elk.ts +2 -0
- package/src/layout-engine/jog-straightening.ts +52 -0
- 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__/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,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import type { ElkNode } from 'elkjs/lib/elk-api'
|
|
3
|
-
import { splitStatements } from '@zombie-mermaid/core'
|
|
4
|
-
import { parseClassDiagram } from '@zombie-mermaid/mermaid-parser'
|
|
5
|
-
|
|
6
|
-
// ELK's result type marks every coordinate optional. Strip them (and
|
|
7
|
-
// optionally drop a node) so the layout extractor's fallbacks run.
|
|
8
|
-
const mode = vi.hoisted(() => ({
|
|
9
|
-
strip: false,
|
|
10
|
-
dropNamespaces: false,
|
|
11
|
-
emptyNamespaces: false,
|
|
12
|
-
noChildren: false,
|
|
13
|
-
}))
|
|
14
|
-
|
|
15
|
-
vi.mock('../elk-instance.ts', async (importOriginal) => {
|
|
16
|
-
const real = await importOriginal<typeof import('../elk-instance.ts')>()
|
|
17
|
-
const bare = (n: ElkNode): ElkNode => {
|
|
18
|
-
return { id: n.id, edges: n.edges, children: n.children?.map(bare) }
|
|
19
|
-
}
|
|
20
|
-
return {
|
|
21
|
-
...real,
|
|
22
|
-
elkLayoutSync: (...args: Parameters<typeof real.elkLayoutSync>) => {
|
|
23
|
-
const result = real.elkLayoutSync(...args)
|
|
24
|
-
if (mode.noChildren) return { ...result, children: undefined }
|
|
25
|
-
if (mode.emptyNamespaces) {
|
|
26
|
-
return {
|
|
27
|
-
...result,
|
|
28
|
-
children: result.children?.map((c) =>
|
|
29
|
-
c.id.startsWith('namespace') ? { ...c, children: undefined } : c,
|
|
30
|
-
),
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (mode.dropNamespaces) {
|
|
34
|
-
return {
|
|
35
|
-
...result,
|
|
36
|
-
children: result.children?.filter(
|
|
37
|
-
(c) => !c.id.startsWith('namespace'),
|
|
38
|
-
),
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return mode.strip ? bare(result) : result
|
|
42
|
-
},
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
const { layoutClassDiagramSync } = await import('../class/layout.ts')
|
|
47
|
-
|
|
48
|
-
const SRC = `classDiagram
|
|
49
|
-
namespace Core {
|
|
50
|
-
class A
|
|
51
|
-
class B
|
|
52
|
-
}
|
|
53
|
-
A --> B`
|
|
54
|
-
|
|
55
|
-
const layout = () =>
|
|
56
|
-
layoutClassDiagramSync(parseClassDiagram(splitStatements(SRC)))
|
|
57
|
-
|
|
58
|
-
describe('class namespace layout against sparse ELK output (#1196)', () => {
|
|
59
|
-
it('falls back to the origin and measured sizes when ELK omits geometry', () => {
|
|
60
|
-
mode.strip = true
|
|
61
|
-
mode.dropNamespaces = false
|
|
62
|
-
const p = layout()
|
|
63
|
-
expect(p.namespaces).toEqual([
|
|
64
|
-
{ name: 'Core', classIds: ['A', 'B'], x: 0, y: 0, width: 0, height: 0 },
|
|
65
|
-
])
|
|
66
|
-
for (const c of p.classes) {
|
|
67
|
-
expect(c.x).toBe(0)
|
|
68
|
-
expect(c.width).toBeGreaterThan(0)
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('tolerates a namespace node without children', () => {
|
|
73
|
-
mode.strip = false
|
|
74
|
-
mode.dropNamespaces = false
|
|
75
|
-
mode.emptyNamespaces = true
|
|
76
|
-
expect(layout().namespaces).toHaveLength(1)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('tolerates a result without any children', () => {
|
|
80
|
-
mode.strip = false
|
|
81
|
-
mode.dropNamespaces = false
|
|
82
|
-
mode.emptyNamespaces = false
|
|
83
|
-
mode.noChildren = true
|
|
84
|
-
const p = layout()
|
|
85
|
-
expect(p.namespaces).toEqual([])
|
|
86
|
-
expect(p.classes).toEqual([])
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('skips a namespace ELK did not return', () => {
|
|
90
|
-
mode.strip = false
|
|
91
|
-
mode.noChildren = false
|
|
92
|
-
mode.emptyNamespaces = false
|
|
93
|
-
mode.dropNamespaces = true
|
|
94
|
-
expect(layout().namespaces).toEqual([])
|
|
95
|
-
})
|
|
96
|
-
})
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { renderMermaidSVG } from '../../../../src/index.ts'
|
|
3
|
-
|
|
4
|
-
// The marker used to share the faint colour and regular weight of the colon,
|
|
5
|
-
// so `~` read as `-` at 1x (#1237). It now gets the member-name colour and a
|
|
6
|
-
// bold weight, in its own tspan.
|
|
7
|
-
describe('class diagram visibility markers (#1237)', () => {
|
|
8
|
-
const svg = renderMermaidSVG(`classDiagram
|
|
9
|
-
class User {
|
|
10
|
-
+String a
|
|
11
|
-
-String b
|
|
12
|
-
#String c
|
|
13
|
-
~String d
|
|
14
|
-
}`)
|
|
15
|
-
|
|
16
|
-
for (const marker of ['+', '-', '#', '~']) {
|
|
17
|
-
it(`draws ${marker} in a bold, non-faint tspan`, () => {
|
|
18
|
-
expect(svg).toContain(
|
|
19
|
-
`<tspan fill="var(--_text-sec)" font-weight="700">${marker}</tspan>`,
|
|
20
|
-
)
|
|
21
|
-
})
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
it('keeps a faint spacer between marker and name', () => {
|
|
25
|
-
expect(svg).toContain('<tspan fill="var(--_text-faint)"> </tspan>')
|
|
26
|
-
})
|
|
27
|
-
})
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the shared ELK edge-geometry extraction helpers
|
|
3
|
-
* (`extractEdgePoints` / `extractEdgeLabelPosition`).
|
|
4
|
-
*
|
|
5
|
-
* `from-elk.ts`, `class/layout.ts`, and `er/layout.ts` all exercise this
|
|
6
|
-
* module indirectly through full-diagram integration tests, but those
|
|
7
|
-
* only ever hit the "happy path" (an edge with sections and, sometimes,
|
|
8
|
-
* a placed label). These tests cover the defensive branches directly —
|
|
9
|
-
* missing/empty sections, missing/empty labels, an unplaced label, and
|
|
10
|
-
* missing bend points/label dimensions — so every branch has direct
|
|
11
|
-
* coverage rather than relying on an integration test to happen to hit it.
|
|
12
|
-
*/
|
|
13
|
-
import { describe, it, expect } from 'vitest'
|
|
14
|
-
import type { ElkExtendedEdge } from 'elkjs'
|
|
15
|
-
import {
|
|
16
|
-
extractEdgePoints,
|
|
17
|
-
extractEdgeLabelPosition,
|
|
18
|
-
} from '@zombie-mermaid/svg-renderer'
|
|
19
|
-
|
|
20
|
-
function edge(partial: Partial<ElkExtendedEdge>): ElkExtendedEdge {
|
|
21
|
-
return { id: 'e0', sources: ['a'], targets: ['b'], ...partial }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('extractEdgePoints', () => {
|
|
25
|
-
it('returns an empty array when the edge has no sections property', () => {
|
|
26
|
-
expect(extractEdgePoints(edge({}))).toEqual([])
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('returns an empty array when sections is an empty array', () => {
|
|
30
|
-
expect(extractEdgePoints(edge({ sections: [] }))).toEqual([])
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('walks start -> end with no bend points', () => {
|
|
34
|
-
const e = edge({
|
|
35
|
-
sections: [
|
|
36
|
-
{
|
|
37
|
-
id: 's0',
|
|
38
|
-
startPoint: { x: 10, y: 20 },
|
|
39
|
-
endPoint: { x: 30, y: 40 },
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
})
|
|
43
|
-
expect(extractEdgePoints(e)).toEqual([
|
|
44
|
-
{ x: 10, y: 20 },
|
|
45
|
-
{ x: 30, y: 40 },
|
|
46
|
-
])
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
it('walks start -> bendPoints -> end when bend points are present', () => {
|
|
50
|
-
const e = edge({
|
|
51
|
-
sections: [
|
|
52
|
-
{
|
|
53
|
-
id: 's0',
|
|
54
|
-
startPoint: { x: 0, y: 0 },
|
|
55
|
-
endPoint: { x: 100, y: 0 },
|
|
56
|
-
bendPoints: [
|
|
57
|
-
{ x: 25, y: 50 },
|
|
58
|
-
{ x: 75, y: 50 },
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
})
|
|
63
|
-
expect(extractEdgePoints(e)).toEqual([
|
|
64
|
-
{ x: 0, y: 0 },
|
|
65
|
-
{ x: 25, y: 50 },
|
|
66
|
-
{ x: 75, y: 50 },
|
|
67
|
-
{ x: 100, y: 0 },
|
|
68
|
-
])
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('translates every point by offsetX/offsetY when provided', () => {
|
|
72
|
-
const e = edge({
|
|
73
|
-
sections: [
|
|
74
|
-
{
|
|
75
|
-
id: 's0',
|
|
76
|
-
startPoint: { x: 0, y: 0 },
|
|
77
|
-
endPoint: { x: 10, y: 10 },
|
|
78
|
-
bendPoints: [{ x: 5, y: 5 }],
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
})
|
|
82
|
-
expect(extractEdgePoints(e, 100, 200)).toEqual([
|
|
83
|
-
{ x: 100, y: 200 },
|
|
84
|
-
{ x: 105, y: 205 },
|
|
85
|
-
{ x: 110, y: 210 },
|
|
86
|
-
])
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('only reads the first section when multiple are present', () => {
|
|
90
|
-
const e = edge({
|
|
91
|
-
sections: [
|
|
92
|
-
{ id: 's0', startPoint: { x: 0, y: 0 }, endPoint: { x: 1, y: 1 } },
|
|
93
|
-
{ id: 's1', startPoint: { x: 9, y: 9 }, endPoint: { x: 8, y: 8 } },
|
|
94
|
-
],
|
|
95
|
-
})
|
|
96
|
-
expect(extractEdgePoints(e)).toEqual([
|
|
97
|
-
{ x: 0, y: 0 },
|
|
98
|
-
{ x: 1, y: 1 },
|
|
99
|
-
])
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('extractEdgeLabelPosition', () => {
|
|
104
|
-
it('returns undefined when the edge has no labels property', () => {
|
|
105
|
-
expect(extractEdgeLabelPosition(edge({}))).toBeUndefined()
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('returns undefined when labels is an empty array', () => {
|
|
109
|
-
expect(extractEdgeLabelPosition(edge({ labels: [] }))).toBeUndefined()
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('returns undefined when the label has no x/y (ELK left it unplaced)', () => {
|
|
113
|
-
expect(
|
|
114
|
-
extractEdgeLabelPosition(edge({ labels: [{ text: 'foo' }] })),
|
|
115
|
-
).toBeUndefined()
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('returns undefined when only x is set (y still unplaced)', () => {
|
|
119
|
-
expect(
|
|
120
|
-
extractEdgeLabelPosition(edge({ labels: [{ text: 'foo', x: 5 }] })),
|
|
121
|
-
).toBeUndefined()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('computes the label center, defaulting missing width/height to 0', () => {
|
|
125
|
-
expect(
|
|
126
|
-
extractEdgeLabelPosition(
|
|
127
|
-
edge({ labels: [{ text: 'foo', x: 10, y: 20 }] }),
|
|
128
|
-
),
|
|
129
|
-
).toEqual({ x: 10, y: 20 })
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('computes the label center from x/y/width/height', () => {
|
|
133
|
-
expect(
|
|
134
|
-
extractEdgeLabelPosition(
|
|
135
|
-
edge({
|
|
136
|
-
labels: [{ text: 'foo', x: 10, y: 20, width: 40, height: 10 }],
|
|
137
|
-
}),
|
|
138
|
-
),
|
|
139
|
-
).toEqual({ x: 30, y: 25 })
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('translates the computed center by offsetX/offsetY when provided', () => {
|
|
143
|
-
expect(
|
|
144
|
-
extractEdgeLabelPosition(
|
|
145
|
-
edge({
|
|
146
|
-
labels: [{ text: 'foo', x: 10, y: 20, width: 40, height: 10 }],
|
|
147
|
-
}),
|
|
148
|
-
100,
|
|
149
|
-
200,
|
|
150
|
-
),
|
|
151
|
-
).toEqual({ x: 130, y: 225 })
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('only reads the first label when multiple are present', () => {
|
|
155
|
-
expect(
|
|
156
|
-
extractEdgeLabelPosition(
|
|
157
|
-
edge({
|
|
158
|
-
labels: [
|
|
159
|
-
{ text: 'first', x: 0, y: 0 },
|
|
160
|
-
{ text: 'second', x: 100, y: 100 },
|
|
161
|
-
],
|
|
162
|
-
}),
|
|
163
|
-
),
|
|
164
|
-
).toEqual({ x: 0, y: 0 })
|
|
165
|
-
})
|
|
166
|
-
})
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { renderMermaidSVG } from '../../../../src/index.ts'
|
|
3
|
-
|
|
4
|
-
// Mono glyphs advance 0.6em (6.6px at the 11px attribute size). Chromium
|
|
5
|
-
// without subpixel glyph positioning (headless Linux) rounds each advance to a
|
|
6
|
-
// whole pixel, so the worst-case drawn width is chars * ceil(6.6) = chars * 7.
|
|
7
|
-
// A box sized from the unrounded estimate let the type run into the
|
|
8
|
-
// right-aligned name (#1262, same cause as #1238).
|
|
9
|
-
const WORST_CASE_ADVANCE = 7
|
|
10
|
-
// The row text puts two spaces between type and name, so at worst-case
|
|
11
|
-
// advances the gap must not shrink below two whole-pixel glyphs.
|
|
12
|
-
const MIN_GAP_BETWEEN_TYPE_AND_NAME = 2 * WORST_CASE_ADVANCE
|
|
13
|
-
|
|
14
|
-
const SRC = `erDiagram
|
|
15
|
-
CUSTOMER {
|
|
16
|
-
varchar(255) emailAddressPrimary
|
|
17
|
-
string name
|
|
18
|
-
}`
|
|
19
|
-
|
|
20
|
-
const attr = (tag: string, name: string) => {
|
|
21
|
-
const start = tag.indexOf(`${name}="`) + name.length + 2
|
|
22
|
-
return Number(tag.slice(start, tag.indexOf('"', start)))
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('ER attribute row padding (#1262)', () => {
|
|
26
|
-
const svg = renderMermaidSVG(SRC)
|
|
27
|
-
const texts = svg.split('<text')
|
|
28
|
-
const typeText = texts.find((t) => t.includes('varchar(255)')) as string
|
|
29
|
-
const nameText = texts.find((t) =>
|
|
30
|
-
t.includes('emailAddressPrimary'),
|
|
31
|
-
) as string
|
|
32
|
-
|
|
33
|
-
it('finds the type and name text', () => {
|
|
34
|
-
expect(typeText).toBeDefined()
|
|
35
|
-
expect(nameText).toBeDefined()
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
it('leaves a gap between type and name even with whole-pixel glyph advances', () => {
|
|
39
|
-
const typeRight =
|
|
40
|
-
attr(typeText, 'x') + 'varchar(255)'.length * WORST_CASE_ADVANCE
|
|
41
|
-
const nameLeft =
|
|
42
|
-
attr(nameText, 'x') - 'emailAddressPrimary'.length * WORST_CASE_ADVANCE
|
|
43
|
-
expect(nameLeft - typeRight).toBeGreaterThanOrEqual(
|
|
44
|
-
MIN_GAP_BETWEEN_TYPE_AND_NAME,
|
|
45
|
-
)
|
|
46
|
-
})
|
|
47
|
-
})
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #1233: one-or-many (`|{`) was drawn as a bare crow's foot. The visual suite's
|
|
3
|
-
* tolerance can't see a missing bar, so assert the drawn primitives directly.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect } from 'vitest'
|
|
6
|
-
import { renderMermaidSVG } from '@zombie-mermaid/svg-renderer'
|
|
7
|
-
|
|
8
|
-
function count(src: string): { lines: number; circles: number } {
|
|
9
|
-
const svg = renderMermaidSVG(src)
|
|
10
|
-
return {
|
|
11
|
-
lines: (svg.match(/<line /g) ?? []).length,
|
|
12
|
-
circles: (svg.match(/<circle /g) ?? []).length,
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Primitives drawn for the right-hand cardinality token, net of an `||` end. */
|
|
17
|
-
function rightEnd(token: string): { lines: number; circles: number } {
|
|
18
|
-
const base = count('erDiagram\n A ||--|| B : r')
|
|
19
|
-
const v = count(`erDiagram\n A ||--${token} B : r`)
|
|
20
|
-
return { lines: v.lines - base.lines, circles: v.circles - base.circles }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('ER cardinality markers (#1233)', () => {
|
|
24
|
-
it('draws one-or-many as foot (3 lines) + bar, vs 2 lines for exactly one', () => {
|
|
25
|
-
expect(rightEnd('|{')).toEqual({ lines: 2, circles: 0 })
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('draws zero-or-many as foot + circle, with no bar', () => {
|
|
29
|
-
expect(rightEnd('o{')).toEqual({ lines: 1, circles: 1 })
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('distinguishes one-or-many from zero-or-many', () => {
|
|
33
|
-
expect(rightEnd('|{')).not.toEqual(rightEnd('o{'))
|
|
34
|
-
})
|
|
35
|
-
})
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #1250: an ER diagram with no `direction` statement laid out left-to-right,
|
|
3
|
-
* while official Mermaid lays it out top-to-bottom. The relationship line
|
|
4
|
-
* between two entities is the layout axis: vertical when stacked, horizontal
|
|
5
|
-
* when side by side.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect } from 'vitest'
|
|
8
|
-
import { renderMermaidSVG } from '@zombie-mermaid/svg-renderer'
|
|
9
|
-
|
|
10
|
-
const SOURCE = 'ORDER ||--|{ LINE_ITEM : contains'
|
|
11
|
-
|
|
12
|
-
function endpoints(svg: string): { dx: number; dy: number } {
|
|
13
|
-
const points = svg.match(/class="er-relationship"[^>]*points="([^"]+)"/)?.[1]
|
|
14
|
-
expect(points).toBeDefined()
|
|
15
|
-
const pts = (points ?? '').split(' ').map((p) => p.split(',').map(Number))
|
|
16
|
-
const first = pts[0] ?? []
|
|
17
|
-
const last = pts[pts.length - 1] ?? []
|
|
18
|
-
return {
|
|
19
|
-
dx: Math.abs((last[0] ?? 0) - (first[0] ?? 0)),
|
|
20
|
-
dy: Math.abs((last[1] ?? 0) - (first[1] ?? 0)),
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('ER default layout direction (#1250)', () => {
|
|
25
|
-
it('stacks entities top-to-bottom when the source has no direction', () => {
|
|
26
|
-
const { dx, dy } = endpoints(renderMermaidSVG(`erDiagram\n ${SOURCE}`))
|
|
27
|
-
expect(dy).toBeGreaterThan(dx)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('still honors an explicit `direction LR` in the source', () => {
|
|
31
|
-
const { dx, dy } = endpoints(
|
|
32
|
-
renderMermaidSVG(`erDiagram\n direction LR\n ${SOURCE}`),
|
|
33
|
-
)
|
|
34
|
-
expect(dx).toBeGreaterThan(dy)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('still honors the direction render option', () => {
|
|
38
|
-
const { dx, dy } = endpoints(
|
|
39
|
-
renderMermaidSVG(`erDiagram\n ${SOURCE}`, { direction: 'LR' }),
|
|
40
|
-
)
|
|
41
|
-
expect(dx).toBeGreaterThan(dy)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('keeps declaration order within a layer and centers a shared child', () => {
|
|
45
|
-
const svg = renderMermaidSVG(`erDiagram
|
|
46
|
-
CUSTOMER ||--o{ ORDER : places
|
|
47
|
-
ORDER ||--|{ LINE_ITEM : contains
|
|
48
|
-
PRODUCT ||--o{ LINE_ITEM : includes`)
|
|
49
|
-
const box = (id: string): { left: number; right: number } => {
|
|
50
|
-
const start = svg.indexOf(`data-id="${id}"`)
|
|
51
|
-
expect(start).toBeGreaterThanOrEqual(0)
|
|
52
|
-
const m = svg
|
|
53
|
-
.slice(start)
|
|
54
|
-
.match(/>\s*<rect x="([\d.]+)"[^>]*width="([\d.]+)"/)
|
|
55
|
-
expect(m).not.toBeNull()
|
|
56
|
-
const x = Number(m?.[1])
|
|
57
|
-
return { left: x, right: x + Number(m?.[2]) }
|
|
58
|
-
}
|
|
59
|
-
const order = box('ORDER')
|
|
60
|
-
const product = box('PRODUCT')
|
|
61
|
-
const lineItem = box('LINE_ITEM')
|
|
62
|
-
// ORDER is declared before PRODUCT, so it sits to its left.
|
|
63
|
-
expect(order.left).toBeLessThan(product.left)
|
|
64
|
-
// LINE_ITEM sits between its two parents, not off to one side.
|
|
65
|
-
const mid = (lineItem.left + lineItem.right) / 2
|
|
66
|
-
expect(mid).toBeGreaterThan(order.left)
|
|
67
|
-
expect(mid).toBeLessThan(product.right)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #1244: ER edge labels were drawn in `--_text-muted` (fg mixed at 40% into
|
|
3
|
-
* bg), ~2.4:1 on the default white background. Read the mix the renderer
|
|
4
|
-
* actually emits and assert it clears WCAG AA for text (4.5:1) against the
|
|
5
|
-
* default theme.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, it, expect } from 'vitest'
|
|
8
|
-
import { DEFAULTS, MIX } from '@zombie-mermaid/core'
|
|
9
|
-
import { renderMermaidSVG } from '@zombie-mermaid/svg-renderer'
|
|
10
|
-
|
|
11
|
-
type Rgb = readonly [number, number, number]
|
|
12
|
-
|
|
13
|
-
function channels(hex: string): Rgb {
|
|
14
|
-
const c = (i: number) => parseInt(hex.slice(i, i + 2), 16)
|
|
15
|
-
return [c(1), c(3), c(5)]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function luminance([r, g, b]: Rgb): number {
|
|
19
|
-
const lin = (c: number) => {
|
|
20
|
-
const v = c / 255
|
|
21
|
-
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4
|
|
22
|
-
}
|
|
23
|
-
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Contrast of `color-mix(in srgb, fg pct%, bg)` against bg. */
|
|
27
|
-
function mixContrast(pct: number): number {
|
|
28
|
-
const [fr, fgc, fb] = channels(DEFAULTS.fg)
|
|
29
|
-
const [br, bgc, bb] = channels(DEFAULTS.bg)
|
|
30
|
-
const mix = (f: number, b: number) => (f * pct + b * (100 - pct)) / 100
|
|
31
|
-
const mixed: Rgb = [mix(fr, br), mix(fgc, bgc), mix(fb, bb)]
|
|
32
|
-
const l1 = luminance(mixed)
|
|
33
|
-
const l2 = luminance([br, bgc, bb])
|
|
34
|
-
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe('ER edge label contrast (#1244)', () => {
|
|
38
|
-
const svg = renderMermaidSVG('erDiagram\n ORDER ||--|{ LINE_ITEM : contains')
|
|
39
|
-
const label = svg.match(/<text[^>]*>contains<\/text>/)?.[0] ?? ''
|
|
40
|
-
const percent = Number(
|
|
41
|
-
label.match(
|
|
42
|
-
/fill="color-mix\(in srgb, var\(--fg\) (\d+)%, var\(--bg\)\)"/,
|
|
43
|
-
)?.[1],
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
it('mixes the relationship label color from --fg and --bg', () => {
|
|
47
|
-
expect(Number.isFinite(percent)).toBe(true)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('clears WCAG AA (4.5:1) with the default theme', () => {
|
|
51
|
-
expect(mixContrast(percent)).toBeGreaterThanOrEqual(4.5)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('is darker than the shared muted/secondary text tokens that failed AA', () => {
|
|
55
|
-
expect(mixContrast(MIX.textMuted)).toBeLessThan(4.5)
|
|
56
|
-
expect(mixContrast(MIX.textSec)).toBeLessThan(4.5)
|
|
57
|
-
})
|
|
58
|
-
})
|