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 CHANGED
@@ -17,7 +17,8 @@ Ultra-fast, fully themeable, zero DOM dependencies. A maintained fork of [`beaut
17
17
  [![npm version](https://img.shields.io/npm/v/zombie-mermaid.svg)](https://www.npmjs.com/package/zombie-mermaid)
18
18
  [![CI](https://github.com/dfadler/zombie-mermaid/actions/workflows/ci.yml/badge.svg)](https://github.com/dfadler/zombie-mermaid/actions/workflows/ci.yml)
19
19
  [![codecov](https://codecov.io/gh/dfadler/zombie-mermaid/branch/main/graph/badge.svg)](https://codecov.io/gh/dfadler/zombie-mermaid)
20
- [![Bundle Size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/dfadler/zombie-mermaid/main/badges/bundle-size.json)](#bundle-size)
20
+ [![ASCII renderer size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/dfadler/zombie-mermaid/main/badges/bundle-size-ascii-renderer.json)](#bundle-size)
21
+ [![SVG renderer size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/dfadler/zombie-mermaid/main/badges/bundle-size-svg-renderer.json)](#bundle-size)
21
22
  [![Socket Security](https://badge.socket.dev/npm/package/zombie-mermaid)](https://socket.dev/npm/package/zombie-mermaid)
22
23
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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 **Bundle Size** badge above tracks the gzipped size of `zombie-mermaid`'s
50
- main entry point (`dist/index.js`) together with the `@zombie-mermaid/*`
51
- workspace packages it imports, **plus the SVG renderer's third-party
52
- dependencies** ([ELK.js](https://github.com/kieler/elkjs) and `entities`,
53
- which make up most of the SVG renderer's real weight), and updates
54
- automatically with every release — the same badge/CI-tracked pattern already
55
- backing the coverage and CI status badges. The standalone renderer packages
56
- under [Packages](#packages) get the same measurement for their own dependency
57
- closure: the SVG badge is labelled "incl. deps" because it counts ELK.js and
58
- `entities`, while the ASCII renderer has no third-party dependencies to count. Need ASCII rendering only? `import { renderMermaidASCII }
59
- from 'zombie-mermaid/ascii'` skips [ELK.js](https://github.com/kieler/elkjs),
60
- the layout engine the SVG renderer depends on, for a substantially smaller
61
- bundle.
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
+ [![All-in size](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/dfadler/zombie-mermaid/main/badges/bundle-size.json)](#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.0",
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.0",
94
- "@zombie-mermaid/core": "4.1.0",
95
- "@zombie-mermaid/mcp": "4.1.0",
96
- "@zombie-mermaid/mermaid-parser": "4.1.0",
97
- "@zombie-mermaid/svg-renderer": "4.1.0"
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
- expect(out).toContain('├────────────┘')
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(FAN_IN)
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(FAN_IN)
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(FAN_IN, true)
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
- expect(grid(g, 'W').x).not.toBe(grid(g, 'Y').x)
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
- // Side-by-side siblings B and C with an edge each way between them, in
224
- // a TD graph (the only direction analyzeEdgeBundles bundles at all), so
225
- // both a lane group and bundle groups exist over the same edges.
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 left of a frame it passes', () => {
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 │ │ │ │ │ │ Three │',
74
- '│ │ │ │ │ │ │ │ │',
75
- '│ ▼ │ │ ▼ │ │ ▼ │',
76
- '│ ┌───┐ │ │ ┌───┐ │ │ ┌───┐ │',
77
- '│ │ │ │ │ │ │ │ │ │ │ │',
78
- '│ │ b ├───┼─┘ │ x │ └─┼►│ c │ │',
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
- }, 60_000)
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('&quot;go&quot;')
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["&quot;q&quot; &lt;y&gt;"]',
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: &amp;lt; stays a literal &lt;', () => {
74
+ const out = renderMermaidASCII('flowchart LR\n A["&amp;lt;"]', {
75
+ colorMode: 'none',
76
+ })
77
+ expect(out).toContain('&lt;')
78
+ })
79
+
80
+ it('decodes edge labels in ASCII output', () => {
81
+ const out = renderMermaidASCII('flowchart LR\n A -->|&quot;go&quot;| B', {
82
+ colorMode: 'none',
83
+ })
84
+ expect(out).toContain('"go"')
85
+ expect(out).not.toContain('&quot;')
86
+ })
87
+
88
+ it('decodes subgraph labels, including nested ones', () => {
89
+ const out = renderMermaidASCII(
90
+ 'flowchart TD\n subgraph O["&lt;outer&gt;"]\n subgraph I["&quot;inner&quot;"]\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('&lt;')
96
+ expect(out).not.toContain('&quot;')
97
+ })
98
+
99
+ it('decodes numeric and hex HTML entities', () => {
100
+ expect(decodeXmlEntitiesInLabel('&#35;9 &#x5B;y&#X5d; &apos;')).toBe(
101
+ "#9 [y] '",
102
+ )
103
+ })
104
+
105
+ it('leaves invalid numeric HTML entities untouched', () => {
106
+ for (const bad of ['&#99999999;', '&#x110000;', '&#0;', '&#xD800;']) {
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 &amp; &lt;x&gt;<\/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
  // ============================================================================