@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,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
- })