zombie-mermaid 4.1.0 → 4.2.1
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/README.md +24 -14
- package/package.json +6 -6
- package/src/__tests__/ascii-draw-bundles.test.ts +17 -6
- package/src/__tests__/ascii-nonmember-beside-frame-1283.test.ts +6 -1
- package/src/__tests__/ascii-reverse-direction-edges-629.test.ts +14 -4
- package/src/__tests__/ascii-stacked-frame-geometry-1285.test.ts +34 -20
- package/src/__tests__/cli-diagrams.test.ts +5 -0
- package/src/__tests__/cli-e2e.test.ts +1 -1
- package/src/__tests__/mermaid-entity-codes.test.ts +114 -0
- package/src/__tests__/parser.test.ts +8 -1
- package/src/__tests__/svg-accessible-name-conformance.test.ts +15 -0
- package/src/__tests__/svg-accessible-name.test.ts +26 -0
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ Ultra-fast, fully themeable, zero DOM dependencies. A maintained fork of [`beaut
|
|
|
17
17
|
[](https://www.npmjs.com/package/zombie-mermaid)
|
|
18
18
|
[](https://github.com/dfadler/zombie-mermaid/actions/workflows/ci.yml)
|
|
19
19
|
[](https://codecov.io/gh/dfadler/zombie-mermaid)
|
|
20
|
-
[](#bundle-size)
|
|
21
|
+
[](#bundle-size)
|
|
21
22
|
[](https://socket.dev/npm/package/zombie-mermaid)
|
|
22
23
|
[](LICENSE)
|
|
23
24
|
|
|
@@ -46,19 +47,28 @@ Diagrams are essential for AI-assisted programming. When you're working with an
|
|
|
46
47
|
|
|
47
48
|
### Bundle Size
|
|
48
49
|
|
|
49
|
-
The
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
The two size badges above track the gzipped size of each renderer, and update
|
|
51
|
+
automatically with every release:
|
|
52
|
+
|
|
53
|
+
- **ascii-renderer** — the ASCII/Unicode renderer plus `@zombie-mermaid/core`
|
|
54
|
+
and `@zombie-mermaid/mermaid-parser`. It has no third-party dependencies, so
|
|
55
|
+
this is what `zombie-mermaid/ascii` costs.
|
|
56
|
+
- **svg-renderer (incl. deps)** — the SVG renderer, core and parser, **plus its
|
|
57
|
+
third-party dependencies**. Nearly all of the weight is
|
|
58
|
+
[ELK.js](https://github.com/kieler/elkjs), the layout engine: about 466 KB
|
|
59
|
+
gzipped on its own, shipped as one pre-bundled file that can't be
|
|
60
|
+
tree-shaken. `entities` is counted tree-shaken, since the renderer uses one
|
|
61
|
+
function from it.
|
|
62
|
+
|
|
63
|
+
Both figures gzip a plain concatenation of the built files, so they approximate
|
|
64
|
+
what you'd ship, not what any particular bundler emits. The umbrella
|
|
65
|
+
`zombie-mermaid` entry re-exports both renderers; its all-in total is
|
|
66
|
+
[](#bundle-size), the worst case. With a
|
|
67
|
+
bundler, importing only `renderMermaidASCII` from the root drops the SVG
|
|
68
|
+
renderer and ELK.js (every package here is `"sideEffects": false`). Without a
|
|
69
|
+
bundler — Node, a CLI, a serverless function — the root entry loads ELK.js at
|
|
70
|
+
startup even for ASCII-only use, so import from `zombie-mermaid/ascii` instead
|
|
71
|
+
(see [ASCII Output](#ascii-output)).
|
|
62
72
|
|
|
63
73
|
## Features
|
|
64
74
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zombie-mermaid",
|
|
3
|
-
"version": "4.1
|
|
3
|
+
"version": "4.2.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Render Mermaid diagrams as beautiful SVGs or ASCII art. Ultra-fast, fully themeable, zero DOM dependencies. Maintained fork of beautiful-mermaid.",
|
|
6
6
|
"type": "module",
|
|
@@ -90,11 +90,11 @@
|
|
|
90
90
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
91
91
|
"elkjs": "^0.11.0",
|
|
92
92
|
"entities": "^7.0.1",
|
|
93
|
-
"@zombie-mermaid/ascii-renderer": "4.1
|
|
94
|
-
"@zombie-mermaid/core": "4.1
|
|
95
|
-
"@zombie-mermaid/mcp": "4.1
|
|
96
|
-
"@zombie-mermaid/mermaid-parser": "4.1
|
|
97
|
-
"@zombie-mermaid/svg-renderer": "4.1
|
|
93
|
+
"@zombie-mermaid/ascii-renderer": "4.2.1",
|
|
94
|
+
"@zombie-mermaid/core": "4.2.1",
|
|
95
|
+
"@zombie-mermaid/mcp": "4.2.1",
|
|
96
|
+
"@zombie-mermaid/mermaid-parser": "4.2.1",
|
|
97
|
+
"@zombie-mermaid/svg-renderer": "4.2.1"
|
|
98
98
|
},
|
|
99
99
|
"devDependencies": {
|
|
100
100
|
"@changesets/changelog-github": "^1.0.0",
|
|
@@ -63,6 +63,15 @@ const FAN_IN = `flowchart TD
|
|
|
63
63
|
A[One] --> C[Target]
|
|
64
64
|
B[Two] --> C`
|
|
65
65
|
|
|
66
|
+
// A plain fan-in centers its target between the sources, so the grid has no
|
|
67
|
+
// spare columns beside the junction. The hand-built trunk paths below step a
|
|
68
|
+
// column to either side of it; a source that feeds a second node keeps the
|
|
69
|
+
// target under the first source (#1339), leaving that room.
|
|
70
|
+
const FAN_IN_BESIDE = `flowchart TD
|
|
71
|
+
A[One] --> C[Target]
|
|
72
|
+
B[Two] --> C
|
|
73
|
+
B --> D[Other]`
|
|
74
|
+
|
|
66
75
|
const FAN_OUT = `flowchart TD
|
|
67
76
|
C[Source] --> A[One]
|
|
68
77
|
C --> B[Two]`
|
|
@@ -82,16 +91,18 @@ const MIXED_FAN_OUT = `flowchart TD
|
|
|
82
91
|
describe('draw-bundles: rendered fan-in/fan-out diagrams', () => {
|
|
83
92
|
it('draws a fan-in bundle with a T-junction and single arrowhead (unicode)', () => {
|
|
84
93
|
const out = renderMermaidASCII(FAN_IN, { useAscii: false })
|
|
85
|
-
|
|
94
|
+
// The two sources join in one trunk whose T-junction sits over the
|
|
95
|
+
// (centered) target.
|
|
96
|
+
expect(out).toContain('└───────┬──────┘')
|
|
86
97
|
expect(out).toContain('▼')
|
|
87
98
|
expect(out.match(/▼/g)).toHaveLength(1)
|
|
88
99
|
})
|
|
89
100
|
|
|
90
101
|
it('draws a fan-in bundle box-start and junction in ASCII charset', () => {
|
|
91
102
|
const out = renderMermaidASCII(FAN_IN, { useAscii: true })
|
|
92
|
-
expect(out).toContain('
|
|
103
|
+
expect(out).toContain('+-------+------+')
|
|
93
104
|
expect(out).toContain('+--+--+')
|
|
94
|
-
expect(out).toContain('
|
|
105
|
+
expect(out).toContain('+--------+')
|
|
95
106
|
expect(out).toContain('v')
|
|
96
107
|
})
|
|
97
108
|
|
|
@@ -276,7 +287,7 @@ describe('drawBundledEdgeSegment', () => {
|
|
|
276
287
|
|
|
277
288
|
describe('drawBundleSharedPath', () => {
|
|
278
289
|
it('returns unmodified canvases when sharedPath has fewer than 2 points', () => {
|
|
279
|
-
const graph = buildGraph(
|
|
290
|
+
const graph = buildGraph(FAN_IN_BESIDE)
|
|
280
291
|
const bundle = graph.bundles[0]!
|
|
281
292
|
const shortBundle = {
|
|
282
293
|
...bundle,
|
|
@@ -324,7 +335,7 @@ describe('drawBundleSharedPath', () => {
|
|
|
324
335
|
])(
|
|
325
336
|
'draws a %s in the shared trunk (unicode)',
|
|
326
337
|
(_label, makePath, expectedChar) => {
|
|
327
|
-
const graph = buildGraph(
|
|
338
|
+
const graph = buildGraph(FAN_IN_BESIDE)
|
|
328
339
|
const bundle = graph.bundles[0]!
|
|
329
340
|
const bent = {
|
|
330
341
|
...bundle,
|
|
@@ -336,7 +347,7 @@ describe('drawBundleSharedPath', () => {
|
|
|
336
347
|
)
|
|
337
348
|
|
|
338
349
|
it('draws a bend in the shared trunk as "+" in ASCII charset', () => {
|
|
339
|
-
const graph = buildGraph(
|
|
350
|
+
const graph = buildGraph(FAN_IN_BESIDE, true)
|
|
340
351
|
const bundle = graph.bundles[0]!
|
|
341
352
|
const junction = bundle.junctionPoint!
|
|
342
353
|
const bent = {
|
|
@@ -89,7 +89,12 @@ end`
|
|
|
89
89
|
|
|
90
90
|
it('does not move anything when Y has two parents', () => {
|
|
91
91
|
const g = layout(`graph TD${frame}\nX-->A\nW-->Y\nV-->Y\nY-->A`, 'TD')
|
|
92
|
-
|
|
92
|
+
// Longest-path layering ranks A below Y, so Y no longer lands in the
|
|
93
|
+
// frame's span and nothing is moved: both parents stay on the root row
|
|
94
|
+
// in their own columns.
|
|
95
|
+
expect(grid(g, 'W').y).toBe(grid(g, 'V').y)
|
|
96
|
+
expect(grid(g, 'W').x).not.toBe(grid(g, 'V').x)
|
|
97
|
+
expect(grid(g, 'Y').y).toBeGreaterThan(grid(g, 'W').y)
|
|
93
98
|
})
|
|
94
99
|
|
|
95
100
|
it('leaves a parent that is itself in a subgraph alone', () => {
|
|
@@ -220,21 +220,31 @@ describe('opposite-direction edges between one node pair (#629)', () => {
|
|
|
220
220
|
})
|
|
221
221
|
|
|
222
222
|
it('never folds a lane-assigned edge into a fan-in/fan-out bundle', () => {
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
223
|
+
// Longest-path layering ranks a reciprocal pair on different levels (as
|
|
224
|
+
// dagre does), so B and C can no longer sit side by side with a lane
|
|
225
|
+
// group between them. The invariant still matters where a pair and a
|
|
226
|
+
// fan-in bundle (B, C --> D) share the same edges' neighbourhood: no
|
|
227
|
+
// edge of the pair may end up inside a bundle, and none may carry a lane
|
|
228
|
+
// into one.
|
|
226
229
|
const graph = buildGraph(
|
|
227
230
|
`graph TD
|
|
228
231
|
A --> B
|
|
229
232
|
A --> C
|
|
233
|
+
B --> D
|
|
234
|
+
C --> D
|
|
230
235
|
B -- x --> C
|
|
231
236
|
C -- y --> B`,
|
|
232
237
|
'TD',
|
|
233
238
|
)
|
|
234
239
|
|
|
235
|
-
expect(edgeBetween(graph, 'B', 'C').parallelLane).toBeDefined()
|
|
236
240
|
const bundled = (graph.bundles ?? []).flatMap((b) => b.edges)
|
|
241
|
+
expect(bundled.length).toBeGreaterThan(0)
|
|
237
242
|
expect(bundled.filter((e) => e.parallelLane)).toEqual([])
|
|
243
|
+
const pair = new Set([
|
|
244
|
+
edgeBetween(graph, 'B', 'C'),
|
|
245
|
+
edgeBetween(graph, 'C', 'B'),
|
|
246
|
+
])
|
|
247
|
+
expect(bundled.filter((e) => pair.has(e))).toEqual([])
|
|
238
248
|
})
|
|
239
249
|
|
|
240
250
|
it('leaves a vertically stacked reciprocal pair on the #530 mechanism', () => {
|
|
@@ -34,7 +34,7 @@ describe('ASCII: stacked frames keep clear columns beside walls and titles (#128
|
|
|
34
34
|
expect(lines.some((l) => l.includes('││'))).toBe(false)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
it('keeps a clear column when the edge runs
|
|
37
|
+
it('keeps a clear column when the edge runs beside a frame it passes', () => {
|
|
38
38
|
const out = renderMermaidASCII(`graph TD
|
|
39
39
|
subgraph S1[One]
|
|
40
40
|
a
|
|
@@ -61,25 +61,39 @@ a --> c`)
|
|
|
61
61
|
'│ │',
|
|
62
62
|
'│ ┌───┐ │',
|
|
63
63
|
'│ │ │ │',
|
|
64
|
-
'│ │ a
|
|
65
|
-
'│ │ │ │ │
|
|
66
|
-
'│ └─┬─┘ │ │
|
|
67
|
-
'│ │ │ │
|
|
68
|
-
'└───┼───┘ │
|
|
69
|
-
' │ │
|
|
70
|
-
' │ │
|
|
71
|
-
'
|
|
72
|
-
'┌───┼─────┐ │ │
|
|
73
|
-
'│ │ Two │ │ │
|
|
74
|
-
'│ │ │ │ │
|
|
75
|
-
'│ ▼ │ │ ▼
|
|
76
|
-
'│ ┌───┐ │ │ ┌───┐
|
|
77
|
-
'│ │ │ │ │ │ │
|
|
78
|
-
'│ │ b
|
|
79
|
-
'│ │ │ │
|
|
80
|
-
'│
|
|
81
|
-
'│
|
|
82
|
-
'
|
|
64
|
+
'│ │ a ├─┼───────────┐',
|
|
65
|
+
'│ │ │ │ │',
|
|
66
|
+
'│ └─┬─┘ │ │',
|
|
67
|
+
'│ │ │ │',
|
|
68
|
+
'└───┼───┘ │',
|
|
69
|
+
' │ │',
|
|
70
|
+
' │ │',
|
|
71
|
+
' ├───────┐ │',
|
|
72
|
+
'┌───┼─────┐ │ │',
|
|
73
|
+
'│ │ Two │ │ │',
|
|
74
|
+
'│ │ │ │ │',
|
|
75
|
+
'│ ▼ │ │ ▼',
|
|
76
|
+
'│ ┌───┐ │ │ ┌───┐',
|
|
77
|
+
'│ │ │ │ │ │ │',
|
|
78
|
+
'│ │ b │ │ │ │ x │',
|
|
79
|
+
'│ │ │ │ │ │ │',
|
|
80
|
+
'│ └─┬─┘ │ │ └───┘',
|
|
81
|
+
'│ │ │ │',
|
|
82
|
+
'└───┼─────┘ │',
|
|
83
|
+
' │ │',
|
|
84
|
+
' │ │',
|
|
85
|
+
' ├───────┘',
|
|
86
|
+
'┌───┼───────┐',
|
|
87
|
+
'│ │ Three │',
|
|
88
|
+
'│ │ │',
|
|
89
|
+
'│ ▼ │',
|
|
90
|
+
'│ ┌───┐ │',
|
|
91
|
+
'│ │ │ │',
|
|
92
|
+
'│ │ c │ │',
|
|
93
|
+
'│ │ │ │',
|
|
94
|
+
'│ └───┘ │',
|
|
95
|
+
'│ │',
|
|
96
|
+
'└───────────┘',
|
|
83
97
|
].join('\n'),
|
|
84
98
|
)
|
|
85
99
|
})
|
|
@@ -48,6 +48,11 @@ const diagrams: Record<string, string> = {
|
|
|
48
48
|
x-axis [jan, feb, mar, apr]
|
|
49
49
|
y-axis "Revenue (k)" 0 --> 120
|
|
50
50
|
bar [50, 60, 75, 90]`,
|
|
51
|
+
|
|
52
|
+
pie: `pie showData title Pets
|
|
53
|
+
"Dogs" : 386
|
|
54
|
+
"Cats" : 85
|
|
55
|
+
"Rats" : 15`,
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
// ============================================================================
|
|
@@ -27,7 +27,7 @@ beforeAll(() => {
|
|
|
27
27
|
if (!existsSync(CLI)) {
|
|
28
28
|
throw new Error(`Expected ${CLI} to exist after 'pnpm run build'`)
|
|
29
29
|
}
|
|
30
|
-
},
|
|
30
|
+
}, 180_000) // build is ~15s alone but slows several-fold under full-suite worker contention
|
|
31
31
|
|
|
32
32
|
const SIMPLE_FLOWCHART = `graph LR
|
|
33
33
|
A[Start] --> B[Middle] --> C[End]`
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid entity codes (#quot; #lt; #35; #x5B;) in labels, and HTML entities
|
|
3
|
+
* in ASCII labels. Ported in spirit from lukilabs/beautiful-mermaid#158
|
|
4
|
+
* (thiccyoda), reimplemented per label with a code point guard.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest'
|
|
7
|
+
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
8
|
+
import {
|
|
9
|
+
decodeMermaidEntities,
|
|
10
|
+
decodeXmlEntitiesInLabel,
|
|
11
|
+
} from '@zombie-mermaid/core'
|
|
12
|
+
import { renderMermaidSVG, renderMermaidASCII } from '../index.ts'
|
|
13
|
+
|
|
14
|
+
describe('Mermaid entity codes', () => {
|
|
15
|
+
it('decodes named and numeric codes in labels', () => {
|
|
16
|
+
const g = parseMermaid(
|
|
17
|
+
'flowchart LR\n A["say #quot;hi#quot; #lt;x#gt; #35;9 #x5B;y#x5D;"]',
|
|
18
|
+
)
|
|
19
|
+
expect(g.nodes.get('A')!.label).toBe('say "hi" <x> #9 [y]')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('leaves unknown codes alone', () => {
|
|
23
|
+
const g = parseMermaid('flowchart LR\n A["#nope; ok"]')
|
|
24
|
+
expect(g.nodes.get('A')!.label).toBe('#nope; ok')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('decodes edge labels, so SVG output no longer prints the raw code', () => {
|
|
28
|
+
const svg = renderMermaidSVG('flowchart LR\n A -->|#quot;go#quot;| B')
|
|
29
|
+
expect(svg).not.toContain('#quot;')
|
|
30
|
+
expect(svg).toContain('"go"')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('does not throw on out-of-range or invalid code points', () => {
|
|
34
|
+
for (const bad of ['#99999999;', '#x110000;', '#0;', '#xD800;']) {
|
|
35
|
+
expect(decodeMermaidEntities(bad)).toBe(bad)
|
|
36
|
+
}
|
|
37
|
+
const g = parseMermaid('flowchart LR\n A["a #99999999; b"]')
|
|
38
|
+
expect(g.nodes.get('A')!.label).toBe('a #99999999; b')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('does not touch #rrggbb; colors in style lines', () => {
|
|
42
|
+
const g = parseMermaid(
|
|
43
|
+
'flowchart LR\n A --> B\n style A fill:#123;,stroke:#ff0000;',
|
|
44
|
+
)
|
|
45
|
+
// #123; is a valid numeric code, so decoding the whole source would turn it into "{"
|
|
46
|
+
expect(g.nodeStyles.get('A')).toEqual({ fill: '#123;', stroke: '#ff0000' })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('keeps decoded syntax characters as label text instead of re-parsing', () => {
|
|
50
|
+
const g = parseMermaid('flowchart LR\n A["x #x5D;--> B#x5B;"]')
|
|
51
|
+
expect(g.nodes.has('B')).toBe(false)
|
|
52
|
+
expect(g.nodes.get('A')!.label).toBe('x ]--> B[')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('ASCII entity decoding', () => {
|
|
57
|
+
it('decodes HTML entities in ASCII labels as SVG does', () => {
|
|
58
|
+
const out = renderMermaidASCII(
|
|
59
|
+
'flowchart LR\n A[""q" <y>"]',
|
|
60
|
+
{ colorMode: 'none' },
|
|
61
|
+
)
|
|
62
|
+
expect(out).toContain('"q" <y>')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('decodes Mermaid codes in ASCII labels', () => {
|
|
66
|
+
const out = renderMermaidASCII(
|
|
67
|
+
'flowchart LR\n A["#quot;q#quot; #lt;y#gt;"]',
|
|
68
|
+
{ colorMode: 'none' },
|
|
69
|
+
)
|
|
70
|
+
expect(out).toContain('"q" <y>')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('decodes once: &lt; stays a literal <', () => {
|
|
74
|
+
const out = renderMermaidASCII('flowchart LR\n A["&lt;"]', {
|
|
75
|
+
colorMode: 'none',
|
|
76
|
+
})
|
|
77
|
+
expect(out).toContain('<')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('decodes edge labels in ASCII output', () => {
|
|
81
|
+
const out = renderMermaidASCII('flowchart LR\n A -->|"go"| B', {
|
|
82
|
+
colorMode: 'none',
|
|
83
|
+
})
|
|
84
|
+
expect(out).toContain('"go"')
|
|
85
|
+
expect(out).not.toContain('"')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('decodes subgraph labels, including nested ones', () => {
|
|
89
|
+
const out = renderMermaidASCII(
|
|
90
|
+
'flowchart TD\n subgraph O["<outer>"]\n subgraph I[""inner""]\n A\n end\n end',
|
|
91
|
+
{ colorMode: 'none' },
|
|
92
|
+
)
|
|
93
|
+
expect(out).toContain('<outer>')
|
|
94
|
+
expect(out).toContain('"inner"')
|
|
95
|
+
expect(out).not.toContain('<')
|
|
96
|
+
expect(out).not.toContain('"')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('decodes numeric and hex HTML entities', () => {
|
|
100
|
+
expect(decodeXmlEntitiesInLabel('#9 [y] '')).toBe(
|
|
101
|
+
"#9 [y] '",
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('leaves invalid numeric HTML entities untouched', () => {
|
|
106
|
+
for (const bad of ['�', '�', '�', '�']) {
|
|
107
|
+
expect(decodeXmlEntitiesInLabel(bad)).toBe(bad)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('decodes an uppercase X hex marker in Mermaid codes', () => {
|
|
112
|
+
expect(decodeMermaidEntities('#X5B;y#X5d;')).toBe('[y]')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -93,9 +93,16 @@ describe('parseMermaid – graph header', () => {
|
|
|
93
93
|
).toThrow('Did you mean "sequenceDiagram"?')
|
|
94
94
|
})
|
|
95
95
|
|
|
96
|
+
it('suggests `pie` for a pie header in the wrong case', () => {
|
|
97
|
+
// Mermaid's `pie` keyword is case-sensitive, so `PieChart` is not a pie.
|
|
98
|
+
expect(() => parseMermaid('PieChart\n "A" : 1')).toThrow(
|
|
99
|
+
'Did you mean "pie"?',
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
96
103
|
it('lists all supported diagram headers for a totally unrecognized header', () => {
|
|
97
104
|
expect(() => parseMermaid('this is not mermaid at all')).toThrow(
|
|
98
|
-
/"stateDiagram-v2".*"sequenceDiagram".*"classDiagram".*"erDiagram".*"xychart-beta"/,
|
|
105
|
+
/"stateDiagram-v2".*"sequenceDiagram".*"classDiagram".*"erDiagram".*"xychart-beta".*"pie"/,
|
|
99
106
|
)
|
|
100
107
|
})
|
|
101
108
|
|
|
@@ -73,8 +73,17 @@ const SAMPLE_BY_TYPE: Record<DiagramType, string> = {
|
|
|
73
73
|
'architecture-beta\n service a[A]\n service b[B]\n a:R -- L:b',
|
|
74
74
|
|
|
75
75
|
c4: 'C4Context\n Person(u, "User")\n System(s, "System")\n Rel(u, s, "Uses")',
|
|
76
|
+
pie: 'pie title Pets\n "Dogs" : 386\n "Cats" : 85',
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Types that are detected and parsed but have no SVG renderer yet. Each is
|
|
81
|
+
* asserted to fail with an explicit "not implemented" error instead of the
|
|
82
|
+
* accessibility contract — and that assertion stops passing the moment the
|
|
83
|
+
* renderer lands, forcing the type out of this set and into the real check.
|
|
84
|
+
*/
|
|
85
|
+
const NOT_YET_RENDERED: ReadonlySet<DiagramType> = new Set<DiagramType>([])
|
|
86
|
+
|
|
78
87
|
/**
|
|
79
88
|
* `stateDiagram-v2` is a distinct syntax a real caller might render, but it
|
|
80
89
|
* routes through the same `'flowchart'` DiagramType and renderSvg() pipeline
|
|
@@ -211,6 +220,12 @@ describe('accessible-name conformance — every diagram type (#294)', () => {
|
|
|
211
220
|
|
|
212
221
|
for (const diagramType of Object.keys(SAMPLE_BY_TYPE) as DiagramType[]) {
|
|
213
222
|
const source = SAMPLE_BY_TYPE[diagramType]
|
|
223
|
+
if (NOT_YET_RENDERED.has(diagramType)) {
|
|
224
|
+
it(`${diagramType}: rendering is not implemented yet`, () => {
|
|
225
|
+
expect(() => renderMermaidSVG(source)).toThrow(/not implemented yet/)
|
|
226
|
+
})
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
214
229
|
for (const { name, options } of OPTION_SETS) {
|
|
215
230
|
it(`${diagramType}: ${name}`, () => {
|
|
216
231
|
const svg = renderMermaidSVG(source, options)
|
|
@@ -182,6 +182,32 @@ describe('svgOpenTag – hasInteractiveLinks (issue #239)', () => {
|
|
|
182
182
|
})
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
+
describe('svgOpenTag – description', () => {
|
|
186
|
+
const desc = (decorative?: boolean, links?: boolean) =>
|
|
187
|
+
svgOpenTag(400, 300, colors, false, 'T', decorative, links, true, 'D & <x>')
|
|
188
|
+
|
|
189
|
+
it('adds aria-describedby and a <desc> after the <title>', () => {
|
|
190
|
+
const tag = desc()
|
|
191
|
+
expect(tag).toContain(
|
|
192
|
+
'role="img" aria-labelledby="zm-title-1" aria-describedby="zm-desc-1"',
|
|
193
|
+
)
|
|
194
|
+
expect(tag).toMatch(
|
|
195
|
+
/<title id="zm-title-1">T<\/title>\n {2}<desc id="zm-desc-1">D & <x><\/desc>$/,
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('keeps the description with interactive links, drops it when decorative', () => {
|
|
200
|
+
expect(desc(true, true)).toContain('<desc id="zm-desc-1">')
|
|
201
|
+
expect(desc(true, false)).not.toMatch(/<desc|aria-describedby/)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('emits nothing new without a description', () => {
|
|
205
|
+
expect(svgOpenTag(400, 300, colors, false, 'T')).not.toMatch(
|
|
206
|
+
/<desc|aria-describedby/,
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
185
211
|
// ============================================================================
|
|
186
212
|
// renderMermaidSVG – wiring across every diagram type
|
|
187
213
|
// ============================================================================
|