@zombie-mermaid/core 4.1.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 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +125 -105
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/diagram-type.test.ts +15 -0
- package/src/diagram-type.ts +9 -1
- package/src/multiline-utils.ts +66 -2
- package/src/theme.ts +31 -15
package/package.json
CHANGED
|
@@ -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')
|
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/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
|
@@ -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 (
|