@zombie-mermaid/core 4.0.0 → 4.2.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.
- package/dist/index.cjs +7 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -17
- package/dist/index.d.ts +62 -17
- package/dist/index.js +215 -160
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/cluster-edges.test.ts +47 -0
- package/src/__tests__/diagram-type.test.ts +15 -0
- package/src/__tests__/theme.test.ts +18 -15
- package/src/cluster-edges.ts +78 -0
- package/src/diagram-type.ts +9 -1
- package/src/index.ts +1 -0
- package/src/multiline-utils.ts +66 -2
- package/src/theme.ts +53 -37
- package/src/generated/mono-font-subset.ts +0 -32
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
|
|
3
|
+
import { withoutOwnClusterEdges } from '../cluster-edges.ts'
|
|
4
|
+
|
|
5
|
+
const pairs = (src: string) =>
|
|
6
|
+
withoutOwnClusterEdges(parseMermaid(src)).edges.map(
|
|
7
|
+
(e) => `${e.source}>${e.target}`,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
describe('withoutOwnClusterEdges (#1310)', () => {
|
|
11
|
+
it('drops an edge from a member node to its subgraph', () => {
|
|
12
|
+
expect(
|
|
13
|
+
pairs('flowchart TD; subgraph Sub; B; end; A --> B; B --> Sub'),
|
|
14
|
+
).toEqual(['A>B'])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('drops an edge from a subgraph to one of its members', () => {
|
|
18
|
+
expect(
|
|
19
|
+
pairs('flowchart TD; subgraph Sub; B; end; A --> B; Sub --> B'),
|
|
20
|
+
).toEqual(['A>B'])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('drops edges between a nested member, its enclosing subgraphs, and nested subgraphs', () => {
|
|
24
|
+
expect(
|
|
25
|
+
pairs(
|
|
26
|
+
'flowchart TD; subgraph Outer; subgraph Inner; B; end; end; A --> B; B --> Outer; Inner --> B; Outer --> Inner',
|
|
27
|
+
),
|
|
28
|
+
).toEqual(['A>B'])
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('keeps an edge from an outside node to a subgraph', () => {
|
|
32
|
+
expect(
|
|
33
|
+
pairs('flowchart TD; subgraph Sub; B; end; X --> Sub; A --> B'),
|
|
34
|
+
).toEqual(['X>Sub', 'A>B'])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('keeps an edge between two subgraphs', () => {
|
|
38
|
+
expect(
|
|
39
|
+
pairs('flowchart TD; subgraph P; A; end; subgraph Q; B; end; P --> Q'),
|
|
40
|
+
).toEqual(['P>Q'])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns the same graph object when nothing is dropped', () => {
|
|
44
|
+
const g = parseMermaid('flowchart TD; subgraph Sub; B; end; X --> Sub')
|
|
45
|
+
expect(withoutOwnClusterEdges(g)).toBe(g)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -59,6 +59,21 @@ describe('detectDiagramType', () => {
|
|
|
59
59
|
expect(detectDiagramType('xychart-foo\nx-axis [a, b]')).toBe('flowchart')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
+
it('detects pie charts with or without trailing header content', () => {
|
|
63
|
+
expect(detectDiagramType('pie\n"A" : 1')).toBe('pie')
|
|
64
|
+
expect(detectDiagramType(' pie ')).toBe('pie')
|
|
65
|
+
expect(detectDiagramType('pie showData\n"A" : 1')).toBe('pie')
|
|
66
|
+
expect(detectDiagramType('pie title Pets\n"A" : 1')).toBe('pie')
|
|
67
|
+
expect(detectDiagramType('pie\ttitle Pets')).toBe('pie')
|
|
68
|
+
expect(detectDiagramType('%% comment\npie\n"A" : 1')).toBe('pie')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('matches the pie header case-sensitively and as a whole word, like Mermaid', () => {
|
|
72
|
+
expect(detectDiagramType('Pie\n"A" : 1')).toBe('flowchart')
|
|
73
|
+
expect(detectDiagramType('PIE\n"A" : 1')).toBe('flowchart')
|
|
74
|
+
expect(detectDiagramType('pieChart\n"A" : 1')).toBe('flowchart')
|
|
75
|
+
})
|
|
76
|
+
|
|
62
77
|
it('falls back to flowchart for unrecognized input', () => {
|
|
63
78
|
expect(detectDiagramType('')).toBe('flowchart')
|
|
64
79
|
expect(detectDiagramType('not a real header')).toBe('flowchart')
|
|
@@ -131,30 +131,33 @@ describe('buildStyleBlock – font handling', () => {
|
|
|
131
131
|
})
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
-
describe('buildStyleBlock – mono font (#1061)', () => {
|
|
134
|
+
describe('buildStyleBlock – mono font embed (#1061, #1319)', () => {
|
|
135
|
+
// core ships no font data; the caller supplies the embed (svg-renderer's
|
|
136
|
+
// buildSvgStyleBlock supplies the real subset — see src/__tests__/styles.test.ts).
|
|
137
|
+
const embed = {
|
|
138
|
+
family: 'Test Mono',
|
|
139
|
+
faceCss:
|
|
140
|
+
"@font-face { font-family: 'Test Mono'; src: url(data:font/woff2;base64,AAAA) format('woff2'); }",
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
it('never reaches out to Google Fonts for the .mono rule', () => {
|
|
136
|
-
const block = buildStyleBlock('Inter',
|
|
137
|
-
|
|
138
|
-
// this regression test), but nothing in the block should ever request
|
|
139
|
-
// JetBrains Mono from a third-party CDN.
|
|
140
|
-
expect(block).not.toContain('fonts.googleapis.com/css2?family=JetBrains')
|
|
144
|
+
const block = buildStyleBlock('Inter', embed)
|
|
145
|
+
expect(block).not.toContain('fonts.googleapis.com/css2?family=Test')
|
|
141
146
|
})
|
|
142
147
|
|
|
143
|
-
it('
|
|
144
|
-
const block = buildStyleBlock('Inter',
|
|
145
|
-
expect(block).toContain(
|
|
146
|
-
expect(block).toContain('data:font/woff2;base64,')
|
|
147
|
-
expect(block).toContain("font-family: 'JetBrains Mono NL';")
|
|
148
|
+
it('inlines the supplied @font-face verbatim', () => {
|
|
149
|
+
const block = buildStyleBlock('Inter', embed)
|
|
150
|
+
expect(block).toContain(` ${embed.faceCss}`)
|
|
148
151
|
})
|
|
149
152
|
|
|
150
|
-
it("leads the .mono rule's font stack with the
|
|
151
|
-
const block = buildStyleBlock('Inter',
|
|
153
|
+
it("leads the .mono rule's font stack with the supplied family name", () => {
|
|
154
|
+
const block = buildStyleBlock('Inter', embed)
|
|
152
155
|
expect(block).toContain(
|
|
153
|
-
" .mono { font-family: '
|
|
156
|
+
" .mono { font-family: 'Test Mono', 'SF Mono', 'Fira Code', ui-monospace, monospace; }",
|
|
154
157
|
)
|
|
155
158
|
})
|
|
156
159
|
|
|
157
|
-
it('emits neither the @font-face embed nor the .mono rule when
|
|
160
|
+
it('emits neither the @font-face embed nor the .mono rule when mono is false', () => {
|
|
158
161
|
const block = buildStyleBlock('Inter', false)
|
|
159
162
|
expect(block).not.toContain('@font-face')
|
|
160
163
|
expect(block).not.toContain('.mono')
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Edges between a node and a subgraph that contains it (#1310)
|
|
3
|
+
//
|
|
4
|
+
// Real mermaid.js (11.17.2, checked headlessly) accepts `B --> Sub` with B
|
|
5
|
+
// inside Sub, and `Sub --> B`, without erroring, but its layout produces a
|
|
6
|
+
// zero-length path (`M x,yZ`) for the edge: no line joins the two, only a
|
|
7
|
+
// stray arrowhead marker is left floating beside the node. Both renderers
|
|
8
|
+
// here treat such an edge as undrawable and omit it; this module is the one
|
|
9
|
+
// place that decides which edges those are, so the SVG and ASCII renderers
|
|
10
|
+
// cannot disagree.
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
import type { MermaidGraph, MermaidSubgraph } from './types.ts'
|
|
14
|
+
|
|
15
|
+
/** Collect `id` and every descendant subgraph into `byId`. */
|
|
16
|
+
function indexSubgraph(
|
|
17
|
+
sg: MermaidSubgraph,
|
|
18
|
+
byId: Map<string, MermaidSubgraph>,
|
|
19
|
+
): void {
|
|
20
|
+
byId.set(sg.id, sg)
|
|
21
|
+
for (const child of sg.children) indexSubgraph(child, byId)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* True when one endpoint of `edge` is a subgraph id and the other is a node or
|
|
26
|
+
* subgraph nested inside it, at any depth.
|
|
27
|
+
*/
|
|
28
|
+
export function isEdgeWithinOwnCluster(
|
|
29
|
+
edge: { source: string; target: string },
|
|
30
|
+
subgraphById: Map<string, MermaidSubgraph>,
|
|
31
|
+
): boolean {
|
|
32
|
+
const contains = (clusterId: string, innerId: string): boolean => {
|
|
33
|
+
const sg = subgraphById.get(clusterId)
|
|
34
|
+
if (!sg) return false
|
|
35
|
+
const inside = new Set<string>()
|
|
36
|
+
collectInsideIds(sg, inside)
|
|
37
|
+
return inside.has(innerId)
|
|
38
|
+
}
|
|
39
|
+
return (
|
|
40
|
+
contains(edge.target, edge.source) || contains(edge.source, edge.target)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Every node id and descendant subgraph id nested inside `sg`. */
|
|
45
|
+
function collectInsideIds(sg: MermaidSubgraph, out: Set<string>): void {
|
|
46
|
+
for (const id of sg.nodeIds) out.add(id)
|
|
47
|
+
for (const child of sg.children) {
|
|
48
|
+
out.add(child.id)
|
|
49
|
+
collectInsideIds(child, out)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `graph` without its edges between a node and a subgraph that contains it.
|
|
55
|
+
* The parser also registers a subgraph id used as an edge endpoint as a
|
|
56
|
+
* placeholder entry in `graph.nodes`; one no remaining edge refers to is
|
|
57
|
+
* removed too, so the result equals the graph parsed without those edges.
|
|
58
|
+
* Returns `graph` itself (same reference) when it has none, and never mutates
|
|
59
|
+
* its input.
|
|
60
|
+
*/
|
|
61
|
+
export function withoutOwnClusterEdges(graph: MermaidGraph): MermaidGraph {
|
|
62
|
+
if (graph.subgraphs.length === 0) return graph
|
|
63
|
+
const byId = new Map<string, MermaidSubgraph>()
|
|
64
|
+
for (const sg of graph.subgraphs) indexSubgraph(sg, byId)
|
|
65
|
+
const edges = graph.edges.filter((e) => !isEdgeWithinOwnCluster(e, byId))
|
|
66
|
+
if (edges.length === graph.edges.length) return graph
|
|
67
|
+
|
|
68
|
+
const referenced = new Set<string>()
|
|
69
|
+
for (const e of edges) {
|
|
70
|
+
referenced.add(e.source)
|
|
71
|
+
referenced.add(e.target)
|
|
72
|
+
}
|
|
73
|
+
const nodes = new Map(graph.nodes)
|
|
74
|
+
for (const id of byId.keys()) {
|
|
75
|
+
if (!referenced.has(id)) nodes.delete(id)
|
|
76
|
+
}
|
|
77
|
+
return { ...graph, nodes, edges }
|
|
78
|
+
}
|
package/src/diagram-type.ts
CHANGED
|
@@ -25,6 +25,7 @@ export const DIAGRAM_TYPES = [
|
|
|
25
25
|
'architecture',
|
|
26
26
|
|
|
27
27
|
'c4',
|
|
28
|
+
'pie',
|
|
28
29
|
] as const
|
|
29
30
|
|
|
30
31
|
/** The diagram types this library can detect and route to a renderer. */
|
|
@@ -40,7 +41,14 @@ export type DiagramType = (typeof DIAGRAM_TYPES)[number]
|
|
|
40
41
|
* routing and parsing can never disagree about where the header ends.
|
|
41
42
|
*/
|
|
42
43
|
export function detectDiagramType(text: string): DiagramType {
|
|
43
|
-
const
|
|
44
|
+
const header = splitStatements(text)[0]?.text ?? ''
|
|
45
|
+
const firstLine = header.toLowerCase()
|
|
46
|
+
|
|
47
|
+
// Case-sensitive, unlike the other headers: Mermaid's pie detector is
|
|
48
|
+
// `/^\s*pie/` and its grammar keyword `pie` must be followed by
|
|
49
|
+
// whitespace, a comment, or the end of input, so `Pie` and `pie:` are not
|
|
50
|
+
// pie charts there either.
|
|
51
|
+
if (/^pie(?:\s|$)/.test(header)) return 'pie'
|
|
44
52
|
|
|
45
53
|
if (/^xychart(?:-beta)?(?:\s|$)/.test(firstLine)) return 'xychart'
|
|
46
54
|
if (/^architecture(?:-beta)?\s*$/.test(firstLine)) return 'architecture'
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// ============================================================================
|
|
22
22
|
|
|
23
23
|
export * from './click-directive.ts'
|
|
24
|
+
export * from './cluster-edges.ts'
|
|
24
25
|
export * from './color-utils.ts'
|
|
25
26
|
export * from './diagram-type.ts'
|
|
26
27
|
export * from './direction.ts'
|
package/src/multiline-utils.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function normalizeBrTags(label: string): string {
|
|
|
30
30
|
? unquoted.slice(1, -1)
|
|
31
31
|
: unquoted
|
|
32
32
|
|
|
33
|
-
return (
|
|
33
|
+
return decodeMermaidEntities(
|
|
34
34
|
unfenced
|
|
35
35
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
36
36
|
.replace(/\\n/g, '\n')
|
|
@@ -38,7 +38,71 @@ export function normalizeBrTags(label: string): string {
|
|
|
38
38
|
// Markdown formatting → HTML tags (order matters: ** before *)
|
|
39
39
|
.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
|
|
40
40
|
.replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, '<i>$1</i>')
|
|
41
|
-
.replace(/~~(.+?)~~/g, '<s>$1</s>')
|
|
41
|
+
.replace(/~~(.+?)~~/g, '<s>$1</s>'),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const MAX_CODEPOINT = 0x10ffff
|
|
46
|
+
|
|
47
|
+
const NAMED_ENTITIES: Record<string, string> = {
|
|
48
|
+
quot: '"',
|
|
49
|
+
amp: '&',
|
|
50
|
+
lt: '<',
|
|
51
|
+
gt: '>',
|
|
52
|
+
apos: "'",
|
|
53
|
+
nbsp: '\u00a0',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Code point -> character, or undefined when it is not a usable scalar value. */
|
|
57
|
+
function codePointToChar(cp: number): string | undefined {
|
|
58
|
+
const usable =
|
|
59
|
+
Number.isInteger(cp) &&
|
|
60
|
+
cp > 0 &&
|
|
61
|
+
cp <= MAX_CODEPOINT &&
|
|
62
|
+
!(cp >= 0xd800 && cp <= 0xdfff)
|
|
63
|
+
return usable ? String.fromCodePoint(cp) : undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Decode Mermaid's entity codes (`#quot;`, `#lt;`, `#35;`, `#x5B;`), the way
|
|
68
|
+
* Mermaid writes characters that would otherwise end a label or break its
|
|
69
|
+
* syntax. Call it on a single already-parsed label, never on diagram source:
|
|
70
|
+
* decoding up front would let the characters it produces (`<`, `"`, `[`)
|
|
71
|
+
* change how the line parses, and would hit `#fff;` colors in style lines.
|
|
72
|
+
* Unknown names and out-of-range numbers are left untouched.
|
|
73
|
+
*/
|
|
74
|
+
export function decodeMermaidEntities(text: string): string {
|
|
75
|
+
return text.replace(
|
|
76
|
+
/#([xX][0-9a-fA-F]+|[0-9]+|[a-zA-Z]+);/g,
|
|
77
|
+
(match, code: string) => {
|
|
78
|
+
if (/^x/i.test(code)) {
|
|
79
|
+
return codePointToChar(parseInt(code.slice(1), 16)) ?? match
|
|
80
|
+
}
|
|
81
|
+
if (/^[0-9]+$/.test(code)) {
|
|
82
|
+
return codePointToChar(parseInt(code, 10)) ?? match
|
|
83
|
+
}
|
|
84
|
+
return NAMED_ENTITIES[code.toLowerCase()] ?? match
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decode HTML/XML entities (`"`, `<`, `#`, `[`) in one label,
|
|
91
|
+
* in a single pass so `&lt;` becomes `<` rather than `<`. Used by the
|
|
92
|
+
* ASCII renderer, which (unlike SVG) has no source-level decode step.
|
|
93
|
+
*/
|
|
94
|
+
export function decodeXmlEntitiesInLabel(text: string): string {
|
|
95
|
+
return text.replace(
|
|
96
|
+
/&(#[xX][0-9a-fA-F]+|#[0-9]+|quot|amp|lt|gt|apos);/g,
|
|
97
|
+
(match, code: string) => {
|
|
98
|
+
if (/^#x/i.test(code)) {
|
|
99
|
+
return codePointToChar(parseInt(code.slice(2), 16)) ?? match
|
|
100
|
+
}
|
|
101
|
+
if (code.startsWith('#')) {
|
|
102
|
+
return codePointToChar(parseInt(code.slice(1), 10)) ?? match
|
|
103
|
+
}
|
|
104
|
+
return NAMED_ENTITIES[code] ?? match
|
|
105
|
+
},
|
|
42
106
|
)
|
|
43
107
|
}
|
|
44
108
|
|
package/src/theme.ts
CHANGED
|
@@ -14,10 +14,6 @@
|
|
|
14
14
|
import { escapeXml, escapeAttr } from './multiline-utils.ts'
|
|
15
15
|
import { parseHexColor } from './color-utils.ts'
|
|
16
16
|
import { f } from './render-utils.ts'
|
|
17
|
-
import {
|
|
18
|
-
MONO_FONT_FAMILY as SVG_MONO_FONT_FAMILY,
|
|
19
|
-
MONO_FONT_FACE_CSS as SVG_MONO_FONT_FACE_CSS,
|
|
20
|
-
} from './generated/mono-font-subset.ts'
|
|
21
17
|
|
|
22
18
|
// ============================================================================
|
|
23
19
|
// Types
|
|
@@ -39,6 +35,17 @@ export interface SvgEmitOptions {
|
|
|
39
35
|
styleAttribute?: boolean
|
|
40
36
|
}
|
|
41
37
|
|
|
38
|
+
/**
|
|
39
|
+
* A self-hosted monospace font a caller embeds in the SVG `<style>` block
|
|
40
|
+
* for the `.mono` rule. See `buildStyleBlock`.
|
|
41
|
+
*/
|
|
42
|
+
export interface MonoFontEmbed {
|
|
43
|
+
/** Font family name, listed first in the `.mono` rule's font stack. */
|
|
44
|
+
family: string
|
|
45
|
+
/** The complete `@font-face { ... }` rule (typically a base64 data URI). */
|
|
46
|
+
faceCss: string
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
/**
|
|
43
50
|
* Diagram color configuration.
|
|
44
51
|
*
|
|
@@ -418,24 +425,17 @@ export function styleOpenTag(nonce?: string): string {
|
|
|
418
425
|
* Any other value is treated as a literal font name: sanitized and quoted
|
|
419
426
|
* exactly as before.
|
|
420
427
|
*
|
|
421
|
-
* `
|
|
422
|
-
* attribute types) never reaches out to Google Fonts at all
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* `woff2` data URI, generated by scripts/build-mono-font-subset.ts into
|
|
429
|
-
* ./generated/mono-font-subset.ts — directly in the SVG's own `<style>`
|
|
430
|
-
* block. Unlike `font` above, this isn't caller-configurable: it's the same
|
|
431
|
-
* embed for every consumer, on by default, with no network request either
|
|
432
|
-
* way — a strict improvement over the dead-or-alive CDN fetch it replaces,
|
|
433
|
-
* and the only way a *standalone* SVG (no surrounding host page providing
|
|
434
|
-
* its own copy of the font) renders `.mono` text correctly out of the box.
|
|
428
|
+
* `mono`'s `.mono` rule (class-diagram method signatures, ER-diagram
|
|
429
|
+
* attribute types) never reaches out to Google Fonts at all. The caller
|
|
430
|
+
* supplies a self-hosted {@link MonoFontEmbed} (a base64 `woff2` `@font-face`
|
|
431
|
+
* plus its family name), which is inlined directly in the SVG's own `<style>`
|
|
432
|
+
* block (#1061). The font data itself lives in `@zombie-mermaid/svg-renderer`
|
|
433
|
+
* (see its `buildSvgStyleBlock`), not here, so ASCII-only consumers of
|
|
434
|
+
* `core` never carry it (#1319). Pass `false` to omit the `.mono` rule.
|
|
435
435
|
*/
|
|
436
436
|
export function buildStyleBlock(
|
|
437
437
|
font: string,
|
|
438
|
-
|
|
438
|
+
mono: MonoFontEmbed | false,
|
|
439
439
|
nonce?: string,
|
|
440
440
|
): string {
|
|
441
441
|
const isVarReference = isSafeCssVarReference(font)
|
|
@@ -475,11 +475,11 @@ export function buildStyleBlock(
|
|
|
475
475
|
// Self-hosted, embedded @font-face for `.mono` text — see the doc
|
|
476
476
|
// comment above for why this never goes through the CDN-import/skip
|
|
477
477
|
// logic `font` above does.
|
|
478
|
-
...(
|
|
478
|
+
...(mono ? [` ${mono.faceCss}`] : []),
|
|
479
479
|
` text { font-family: ${isVarReference ? safeFont : `'${safeFont}'`}, system-ui, sans-serif; }`,
|
|
480
|
-
...(
|
|
480
|
+
...(mono
|
|
481
481
|
? [
|
|
482
|
-
` .mono { font-family: '${
|
|
482
|
+
` .mono { font-family: '${mono.family}', 'SF Mono', 'Fira Code', ui-monospace, monospace; }`,
|
|
483
483
|
]
|
|
484
484
|
: []),
|
|
485
485
|
` svg {${derivedVars}`,
|
|
@@ -560,6 +560,14 @@ function nextTitleId(): string {
|
|
|
560
560
|
return `zm-title-${titleIdCounter}`
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
/** Same scheme as `nextTitleId`, for the root `<desc>` (`description`). */
|
|
564
|
+
let descIdCounter = 0
|
|
565
|
+
|
|
566
|
+
function nextDescId(): string {
|
|
567
|
+
descIdCounter += 1
|
|
568
|
+
return `zm-desc-${descIdCounter}`
|
|
569
|
+
}
|
|
570
|
+
|
|
563
571
|
/**
|
|
564
572
|
* Test-only: reset the title-id counter so id assertions in tests don't
|
|
565
573
|
* depend on how many titled diagrams earlier tests in the same file rendered.
|
|
@@ -567,6 +575,7 @@ function nextTitleId(): string {
|
|
|
567
575
|
*/
|
|
568
576
|
export function __resetSvgTitleIdCounterForTests(): void {
|
|
569
577
|
titleIdCounter = 0
|
|
578
|
+
descIdCounter = 0
|
|
570
579
|
}
|
|
571
580
|
|
|
572
581
|
/**
|
|
@@ -669,6 +678,11 @@ export function themeStyleDeclarations(
|
|
|
669
678
|
* See #239 — forces no root `role` so the link
|
|
670
679
|
* stays reachable, regardless of `decorative`.
|
|
671
680
|
* @param styleAttribute - Emit the root `style="…"` attribute. Default true.
|
|
681
|
+
* @param description - Accessible description: a `<desc id="zm-desc-N">`
|
|
682
|
+
* after the title plus `aria-describedby` on the root.
|
|
683
|
+
* Follows the same rules as `title` (dropped when
|
|
684
|
+
* decorative without interactive links). Used for
|
|
685
|
+
* Mermaid's `accDescr`.
|
|
672
686
|
*/
|
|
673
687
|
export function svgOpenTag(
|
|
674
688
|
width: number,
|
|
@@ -679,6 +693,7 @@ export function svgOpenTag(
|
|
|
679
693
|
decorative?: boolean,
|
|
680
694
|
hasInteractiveLinks?: boolean,
|
|
681
695
|
styleAttribute: boolean = true,
|
|
696
|
+
description?: string,
|
|
682
697
|
): string {
|
|
683
698
|
const styleAttr = styleAttribute
|
|
684
699
|
? ` style="${themeStyleDeclarations(colors, transparent)}"`
|
|
@@ -686,23 +701,24 @@ export function svgOpenTag(
|
|
|
686
701
|
|
|
687
702
|
let a11yAttrs = ''
|
|
688
703
|
let titleEl = ''
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
// <a href> descendant from assistive tech while leaving it Tab-reachable.
|
|
692
|
-
if (title) {
|
|
693
|
-
const id = nextTitleId()
|
|
694
|
-
a11yAttrs = ` aria-labelledby="${id}"`
|
|
695
|
-
titleEl = `\n <title id="${id}">${escapeXml(title)}</title>`
|
|
696
|
-
}
|
|
697
|
-
} else if (decorative) {
|
|
704
|
+
const named = hasInteractiveLinks || !decorative
|
|
705
|
+
if (!hasInteractiveLinks && decorative) {
|
|
698
706
|
a11yAttrs = ' aria-hidden="true"'
|
|
699
|
-
} else {
|
|
707
|
+
} else if (!hasInteractiveLinks) {
|
|
708
|
+
// No root role with interactive links: role="img" or aria-hidden would
|
|
709
|
+
// hide a real, focusable <a href> descendant from assistive tech while
|
|
710
|
+
// leaving it Tab-reachable.
|
|
700
711
|
a11yAttrs = ' role="img"'
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
712
|
+
}
|
|
713
|
+
if (named && title) {
|
|
714
|
+
const id = nextTitleId()
|
|
715
|
+
a11yAttrs += ` aria-labelledby="${id}"`
|
|
716
|
+
titleEl = `\n <title id="${id}">${escapeXml(title)}</title>`
|
|
717
|
+
}
|
|
718
|
+
if (named && description) {
|
|
719
|
+
const id = nextDescId()
|
|
720
|
+
a11yAttrs += ` aria-describedby="${id}"`
|
|
721
|
+
titleEl += `\n <desc id="${id}">${escapeXml(description)}</desc>`
|
|
706
722
|
}
|
|
707
723
|
|
|
708
724
|
return (
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GENERATED FILE — do not edit by hand.
|
|
3
|
-
*
|
|
4
|
-
* Regenerate with `pnpm run build:mono-font`
|
|
5
|
-
* (scripts/build-mono-font-subset.ts), which subsets
|
|
6
|
-
* third_party/fonts/jetbrains-mono-nl/JetBrainsMonoNL-Regular.ttf down to
|
|
7
|
-
* Basic Latin + Latin-1 Supplement — the self-hosted, embeddable
|
|
8
|
-
* `@font-face` packages/core/src/theme.ts's `buildStyleBlock()` inlines
|
|
9
|
-
* into an SVG's own `<style>` for its `.mono` rule (class-diagram method
|
|
10
|
-
* signatures, ER-diagram attribute types), replacing a Google Fonts CDN
|
|
11
|
-
* `@import` this library's SVG output previously depended on by default
|
|
12
|
-
* (#1061). See that script's header comment for why this subset is
|
|
13
|
-
* deliberately narrower than the site's own (demo/components/generated/
|
|
14
|
-
* mono-font-subset.ts).
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** The self-hosted family name every consumer's font stack should list
|
|
18
|
-
* first — matches this subset's own `name` table entry. */
|
|
19
|
-
export const MONO_FONT_FAMILY = 'JetBrains Mono NL'
|
|
20
|
-
|
|
21
|
-
/** The `@font-face` rule embedding the subset as a base64 `woff2` data
|
|
22
|
-
* URI — no separate network request, no page-depth-relative path to get
|
|
23
|
-
* wrong (see the script header comment for why data-URI over a static
|
|
24
|
-
* asset file). */
|
|
25
|
-
export const MONO_FONT_FACE_CSS = `@font-face {
|
|
26
|
-
font-family: 'JetBrains Mono NL';
|
|
27
|
-
font-style: normal;
|
|
28
|
-
font-weight: 400;
|
|
29
|
-
font-display: swap;
|
|
30
|
-
src: url(data:font/woff2;base64,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) format('woff2');
|
|
31
|
-
unicode-range: U+0020-007E, U+00A0-00FF;
|
|
32
|
-
}`
|