@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,43 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { splitStatements } from '@zombie-mermaid/core'
3
- import { parseC4Diagram } from '@zombie-mermaid/mermaid-parser'
4
- import { layoutC4DiagramSync } from '../c4/layout.ts'
5
- import { C4, C4_TEXT_WIDTH, c4TextWidth, wrapToWidth } from '../c4/metrics.ts'
6
-
7
- // Mermaid 11.17.2 measures C4 text in the browser. The widths below are read
8
- // back from its SVG (see c4-mermaid-reference.test.ts for the full set), and
9
- // text near the wrap limit is where an estimate that is a few percent off
10
- // changes a shape's width (#1210).
11
- const shapes = (src: string) =>
12
- layoutC4DiagramSync(parseC4Diagram(splitStatements(src))).elements
13
-
14
- describe('C4 text widths (#1210)', () => {
15
- it('measures a bold name as Mermaid does', () => {
16
- // Mermaid made the shape for this name 227.6px wide: text plus 2 x 20.
17
- expect(
18
- c4TextWidth('Personal Banking Customer', C4.nameSize, 700),
19
- ).toBeCloseTo(227.6 - 2 * C4.shapePadding, 0)
20
- })
21
-
22
- it('wraps at the shape width less its padding, measured at regular weight', () => {
23
- expect(C4_TEXT_WIDTH).toBe(176)
24
- // 187.6px in bold, 175.9px in regular: it stays on one line, as in
25
- // Mermaid, even though the drawn name is wider than the limit.
26
- const name = 'Personal Banking Customer'
27
- expect(wrapToWidth(name, C4_TEXT_WIDTH, C4.nameSize)).toEqual([name])
28
- expect(c4TextWidth(name, C4.nameSize, 700)).toBeGreaterThan(C4_TEXT_WIDTH)
29
- })
30
-
31
- it('keeps a person with a three-line description at the standard width', () => {
32
- const [p] = shapes(
33
- 'C4Context\n Person(p, "Customer", "A description that is long enough to wrap into three lines at the standard width of a shape")',
34
- )
35
- expect(p!.descriptionLines).toHaveLength(3)
36
- // Mermaid: 216px. A wider estimate widened it to 222px.
37
- expect(p!.width).toBeCloseTo(216, 1)
38
- })
39
-
40
- it('falls back to a scaled estimate outside printable ASCII', () => {
41
- expect(c4TextWidth('中文', C4.descrSize, 400)).toBeGreaterThan(0)
42
- })
43
- })
@@ -1,40 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { splitStatements } from '@zombie-mermaid/core'
3
- import { parseClassDiagram } from '@zombie-mermaid/mermaid-parser'
4
- import { layoutClassDiagramSync } from '../class/layout.ts'
5
-
6
- const layout = (src: string) =>
7
- layoutClassDiagramSync(parseClassDiagram(splitStatements(src)))
8
-
9
- // Mermaid places disconnected components left to right in declaration order
10
- // (#1249). ELK's layered algorithm otherwise orders them by its own graph
11
- // traversal, so the layout has to keep the declared order.
12
- describe('class diagram disconnected components (#1249)', () => {
13
- it('places each component left to right in source order, in one row', () => {
14
- const p = layout(`classDiagram
15
- A <|-- B : inheritance
16
- C *-- D : composition
17
- E o-- F : aggregation
18
- G --> H : association
19
- I ..> J : dependency
20
- K ..|> L : realization`)
21
- const at = (id: string) => p.classes.find((c) => c.id === id)!
22
- const roots = ['A', 'C', 'E', 'G', 'I', 'K'].map((id) => at(id))
23
- const lefts = roots.map((c, i) => Math.min(c.x, at('BDFHJL'[i]!).x))
24
- for (let i = 1; i < lefts.length; i++) {
25
- expect(lefts[i]!).toBeGreaterThan(lefts[i - 1]!)
26
- }
27
- // Single row: every component starts at the same top.
28
- expect(new Set(roots.map((c) => c.y)).size).toBe(1)
29
- })
30
-
31
- it('orders components by first appearance, not by relationship order', () => {
32
- const p = layout(`classDiagram
33
- class Z
34
- class Y
35
- Z <|-- Q
36
- Y <|-- R`)
37
- const x = (id: string) => p.classes.find((c) => c.id === id)!.x
38
- expect(x('Z')).toBeLessThan(x('Y'))
39
- })
40
- })
@@ -1,59 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { renderMermaidSVG } from '../../../../src/index.ts'
3
-
4
- // String slicing rather than RegExp(variable): the repo's Semgrep gate blocks
5
- // non-literal regexes (detect-non-literal-regexp), even in tests.
6
- const marker = (svg: string, id: string) => {
7
- const start = svg.indexOf(`<marker id="${id}"`)
8
- const end = svg.indexOf('</marker>', start)
9
- return start === -1 || end === -1 ? '' : svg.slice(start, end)
10
- }
11
-
12
- const attrValue = (def: string, attr: string) => {
13
- const start = def.indexOf(`${attr}="`) + attr.length + 2
14
- return def.slice(start, def.indexOf('"', start))
15
- }
16
-
17
- const num = (def: string, attr: string) => Number(attrValue(def, attr))
18
-
19
- const relationship = (svg: string, type: string) =>
20
- svg.split('<polyline').find((p) => p.includes(`data-type="${type}"`)) ?? ''
21
-
22
- // The diamond's tip must sit on the line's endpoint. With orient
23
- // auto-start-reverse the marker's +x points away from the line at the start,
24
- // so refX has to be the tip's x; refX=0 pushes the whole diamond outward
25
- // under the class box (#1232). The polygon must also be inset by half its
26
- // stroke, or the marker viewport clips the diamond's points flat.
27
- describe('class diagram diamond markers (#1232)', () => {
28
- const cases = [
29
- ['composition', 'C *-- D', 'cls-composition', 'var(--_arrow)'],
30
- ['aggregation', 'E o-- F', 'cls-aggregation', 'var(--bg)'],
31
- ] as const
32
-
33
- for (const [type, line, id, fill] of cases) {
34
- it(`${type} references its marker at the owning end`, () => {
35
- const svg = renderMermaidSVG(`classDiagram\n ${line}`)
36
- expect(relationship(svg, type)).toContain(`marker-start="url(#${id})"`)
37
- expect(marker(svg, id)).toContain(`fill="${fill}"`)
38
- })
39
-
40
- it(`${type} anchors its tip on the endpoint without clipping the stroke`, () => {
41
- const def = marker(renderMermaidSVG(`classDiagram\n ${line}`), id)
42
- const points = attrValue(def, 'points')
43
- .split(',')
44
- .map((p) => p.trim().split(/\s+/).map(Number) as [number, number])
45
- const xs = points.map((p) => p[0])
46
- const ys = points.map((p) => p[1])
47
- const half = num(def, 'stroke-width') / 2
48
- expect(num(def, 'refX')).toBe(Math.max(...xs))
49
- expect(Math.min(...xs) - half).toBeGreaterThanOrEqual(0)
50
- expect(Math.min(...ys) - half).toBeGreaterThanOrEqual(0)
51
- expect(Math.max(...xs) + half).toBeLessThanOrEqual(
52
- num(def, 'markerWidth'),
53
- )
54
- expect(Math.max(...ys) + half).toBeLessThanOrEqual(
55
- num(def, 'markerHeight'),
56
- )
57
- })
58
- }
59
- })
@@ -1,43 +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 member size). Chromium without
5
- // subpixel glyph positioning (headless Linux) rounds each advance to a whole
6
- // pixel, so the worst-case drawn width is chars * ceil(6.6) = chars * 7. A box
7
- // sized from the unrounded estimate let a long member run into the right
8
- // border in the Linux baselines (#1238).
9
- const WORST_CASE_ADVANCE = 7
10
- const MIN_RIGHT_PADDING = 8
11
-
12
- const SRC = `classDiagram
13
- class Controller {
14
- -model Model
15
- +handleInput(event) void
16
- +updateModel(data) void
17
- }`
18
-
19
- const attr = (tag: string, name: string) => {
20
- const start = tag.indexOf(`${name}="`) + name.length + 2
21
- return Number(tag.slice(start, tag.indexOf('"', start)))
22
- }
23
-
24
- describe('class member right padding (#1238)', () => {
25
- const svg = renderMermaidSVG(SRC)
26
- const box =
27
- (svg.split('<g class="class-node"')[1] ?? '').split('<rect')[1] ?? ''
28
- const text = svg
29
- .split('<text')
30
- .find((t) => t.includes('handleInput(event)')) as string
31
- const drawnChars = '+ handleInput(event): void'.length
32
-
33
- it('finds the box and the member text', () => {
34
- expect(box).not.toBe('')
35
- expect(text).toBeDefined()
36
- })
37
-
38
- it('keeps the minimum right padding even with whole-pixel glyph advances', () => {
39
- const boxRight = attr(box, 'x') + attr(box, 'width')
40
- const textRight = attr(text, 'x') + drawnChars * WORST_CASE_ADVANCE
41
- expect(boxRight - textRight).toBeGreaterThanOrEqual(MIN_RIGHT_PADDING)
42
- })
43
- })
@@ -1,165 +0,0 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { splitStatements } from '@zombie-mermaid/core'
3
- import { parseClassDiagram } from '@zombie-mermaid/mermaid-parser'
4
- import { layoutClassDiagramSync } from '../class/layout.ts'
5
- import { renderMermaidSVG } from '../index.ts'
6
-
7
- const SRC = `classDiagram
8
- namespace Core {
9
- class A
10
- class B
11
- }
12
- namespace Extras {
13
- class C
14
- }
15
- class D
16
- A --> C
17
- A --> B
18
- D --> B`
19
-
20
- const layout = (src: string) =>
21
- layoutClassDiagramSync(parseClassDiagram(splitStatements(src)))
22
-
23
- describe('class diagram namespaces (#1196)', () => {
24
- const p = layout(SRC)
25
- const cls = (id: string) => p.classes.find((c) => c.id === id)!
26
- const ns = (name: string) => p.namespaces.find((n) => n.name === name)!
27
-
28
- it('lays out one frame per namespace', () => {
29
- expect(p.namespaces.map((n) => n.name)).toEqual(['Core', 'Extras'])
30
- })
31
-
32
- it('encloses each member class and no outsider', () => {
33
- const inside = (n: string, id: string) => {
34
- const f = ns(n)
35
- const c = cls(id)
36
- return (
37
- c.x >= f.x &&
38
- c.y >= f.y &&
39
- c.x + c.width <= f.x + f.width &&
40
- c.y + c.height <= f.y + f.height
41
- )
42
- }
43
- expect(inside('Core', 'A')).toBe(true)
44
- expect(inside('Core', 'B')).toBe(true)
45
- expect(inside('Extras', 'C')).toBe(true)
46
- expect(inside('Core', 'C')).toBe(false)
47
- expect(inside('Core', 'D')).toBe(false)
48
- expect(inside('Extras', 'D')).toBe(false)
49
- })
50
-
51
- it('keeps frames inside the canvas and apart from each other', () => {
52
- for (const f of p.namespaces) {
53
- expect(f.x).toBeGreaterThanOrEqual(0)
54
- expect(f.y).toBeGreaterThanOrEqual(0)
55
- expect(f.x + f.width).toBeLessThanOrEqual(p.width)
56
- expect(f.y + f.height).toBeLessThanOrEqual(p.height)
57
- }
58
- const a = ns('Core')
59
- const b = ns('Extras')
60
- const disjoint =
61
- a.x + a.width <= b.x ||
62
- b.x + b.width <= a.x ||
63
- a.y + a.height <= b.y ||
64
- b.y + b.height <= a.y
65
- expect(disjoint).toBe(true)
66
- })
67
-
68
- it('leaves room for the title above the classes', () => {
69
- expect(cls('A').y - ns('Core').y).toBeGreaterThanOrEqual(28)
70
- })
71
-
72
- it('routes edges to the class boxes, including edges inside a namespace', () => {
73
- const touches = (pt: { x: number; y: number }, id: string) => {
74
- const c = cls(id)
75
- const tol = 1.5
76
- return (
77
- pt.x >= c.x - tol &&
78
- pt.x <= c.x + c.width + tol &&
79
- pt.y >= c.y - tol &&
80
- pt.y <= c.y + c.height + tol
81
- )
82
- }
83
- for (const rel of p.relationships) {
84
- const first = rel.points[0]!
85
- const last = rel.points[rel.points.length - 1]!
86
- expect(touches(first, rel.from)).toBe(true)
87
- expect(touches(last, rel.to)).toBe(true)
88
- }
89
- })
90
-
91
- it('draws the namespace names and frames in SVG', () => {
92
- const svg = renderMermaidSVG(SRC)
93
- expect(svg).toContain('>Core</text>')
94
- expect(svg).toContain('>Extras</text>')
95
- expect(svg.match(/class="class-namespace"/g)).toHaveLength(2)
96
- // Frames sit behind the class boxes.
97
- expect(svg.indexOf('class-namespace')).toBeLessThan(
98
- svg.indexOf('class-node'),
99
- )
100
- })
101
-
102
- it('escapes the namespace name', () => {
103
- const svg = renderMermaidSVG(`classDiagram
104
- namespace a&b {
105
- class A
106
- }`)
107
- expect(svg).toContain('>a&amp;b</text>')
108
- })
109
-
110
- it('ignores a namespace with no classes and a class claimed twice', () => {
111
- const q = layout(`classDiagram
112
- namespace Empty {
113
- }
114
- namespace One {
115
- class A
116
- }
117
- namespace Two {
118
- class A
119
- class B
120
- }`)
121
- expect(q.namespaces.map((n) => [n.name, n.classIds])).toEqual([
122
- ['One', ['A']],
123
- ['Two', ['B']],
124
- ])
125
- })
126
-
127
- it('leaves diagrams without namespaces unframed', () => {
128
- expect(layout('classDiagram\nA --> B').namespaces).toEqual([])
129
- })
130
-
131
- it('returns no frames for an empty diagram', () => {
132
- expect(layout('classDiagram').namespaces).toEqual([])
133
- })
134
-
135
- it('keeps a note link attached to a class inside a namespace', () => {
136
- const q = layout(`classDiagram
137
- namespace Core {
138
- class A
139
- class B
140
- }
141
- A --> B
142
- note for A "hello"`)
143
- const a = q.classes.find((c) => c.id === 'A')!
144
- const note = q.notes[0]!
145
- expect(note.linkPoints).toBeDefined()
146
- const pts = note.linkPoints!
147
- const end = pts[pts.length - 1]!
148
- const tol = 1.5
149
- const touchesA =
150
- end.x >= a.x - tol &&
151
- end.x <= a.x + a.width + tol &&
152
- end.y >= a.y - tol &&
153
- end.y <= a.y + a.height + tol
154
- expect(touchesA).toBe(true)
155
- })
156
-
157
- it('lays out a note that has no link', () => {
158
- const q = layout(`classDiagram
159
- namespace Core {
160
- class A
161
- }
162
- note "free floating"`)
163
- expect(q.notes[0]!.linkPoints).toBeUndefined()
164
- })
165
- })
@@ -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,57 +0,0 @@
1
- /**
2
- * #1388: parallel edges between differently sized, centered nodes kinked by a
3
- * fraction of a pixel near the target and tilted the arrowhead.
4
- */
5
- import { describe, it, expect } from 'vitest'
6
- import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
7
- import { layoutGraphSync } from '@zombie-mermaid/svg-renderer'
8
- import { straightenTinyJogs } from '../layout-engine/jog-straightening.ts'
9
-
10
- describe('straightenTinyJogs', () => {
11
- it('collapses a sub-pixel vertical jog into one straight run', () => {
12
- const out = straightenTinyJogs([
13
- { x: 62.48, y: 76.9 },
14
- { x: 62.48, y: 112.9 },
15
- { x: 63.11, y: 112.9 },
16
- { x: 63.11, y: 124.9 },
17
- ])
18
- expect(out).toHaveLength(2)
19
- expect(out[0]!.x).toBeCloseTo(out[1]!.x, 6)
20
- expect(out[0]!.y).toBe(76.9)
21
- expect(out[1]!.y).toBe(124.9)
22
- })
23
-
24
- it('collapses a sub-pixel horizontal jog', () => {
25
- const out = straightenTinyJogs([
26
- { x: 0, y: 10 },
27
- { x: 20, y: 10 },
28
- { x: 20, y: 10.6 },
29
- { x: 40, y: 10.6 },
30
- ])
31
- expect(out).toHaveLength(2)
32
- expect(out[0]!.y).toBeCloseTo(out[1]!.y, 6)
33
- })
34
-
35
- it('keeps a real bend', () => {
36
- const pts = [
37
- { x: 0, y: 0 },
38
- { x: 0, y: 20 },
39
- { x: 30, y: 20 },
40
- { x: 30, y: 40 },
41
- ]
42
- expect(straightenTinyJogs(pts)).toEqual(pts)
43
- })
44
- })
45
-
46
- describe('parallel edges between different-width nodes (#1388)', () => {
47
- it('routes both edges as straight vertical lines', () => {
48
- const g = layoutGraphSync(
49
- parseMermaid('graph TD\n Top <--> Bot\n Top <-.- Bot'),
50
- )
51
- expect(g.edges).toHaveLength(2)
52
- for (const e of g.edges) {
53
- const xs = new Set(e.points.map((p) => p.x.toFixed(2)))
54
- expect(xs.size, JSON.stringify(e.points)).toBe(1)
55
- }
56
- })
57
- })