@zombie-mermaid/mermaid-parser 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 +9 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +841 -248
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/flowchart-edge-label-space-before-pipe.test.ts +54 -0
- package/src/__tests__/pie-mermaid-spec.test.ts +308 -0
- package/src/__tests__/pie-parser.test.ts +452 -0
- package/src/flowchart-parser.ts +1327 -0
- package/src/index.ts +7 -2
- package/src/pie/parser.ts +393 -0
- package/src/pie/types.ts +109 -0
package/src/index.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// @zombie-mermaid/mermaid-parser — per-type diagram parsers
|
|
3
3
|
//
|
|
4
4
|
// Class, ER, sequence, and XY chart diagrams have no shared generic model
|
|
5
|
-
// the way flowcharts/state diagrams do (`MermaidGraph`, parsed by
|
|
6
|
-
// umbrella's
|
|
5
|
+
// the way flowcharts/state diagrams do (`MermaidGraph`, parsed by
|
|
6
|
+
// `flowchart-parser.ts` here, moved from the umbrella's `src/parser.ts`
|
|
7
|
+
// under #1318 so both renderers share one copy). Both `svg-renderer` and `ascii-renderer`
|
|
7
8
|
// import each type's parse function and types directly — confirmed by grep
|
|
8
9
|
// (zombie-mermaid#624, umbrella #620, `monorepo-conversion-scoping.md`
|
|
9
10
|
// finding 2) — so this package's public API is every per-type parse
|
|
@@ -53,4 +54,8 @@ export * from './xychart/parser.ts'
|
|
|
53
54
|
export * from './xychart/types.ts'
|
|
54
55
|
export * from './xychart/colors.ts'
|
|
55
56
|
|
|
57
|
+
export * from './pie/parser.ts'
|
|
58
|
+
export * from './pie/types.ts'
|
|
59
|
+
|
|
56
60
|
export * from './expanded-shapes.ts'
|
|
61
|
+
export * from './flowchart-parser.ts'
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import type { PieChart, PieSlice } from './types.ts'
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Pie chart parser
|
|
5
|
+
//
|
|
6
|
+
// Parses Mermaid `pie` syntax into a typed PieChart, matching what Mermaid
|
|
7
|
+
// itself accepts and rejects. Mermaid parses pie charts with a Langium
|
|
8
|
+
// grammar (mermaid-js/mermaid packages/parser/src/language/pie/pie.langium
|
|
9
|
+
// plus common/common.langium) and then validates slices in pieDb.ts. This
|
|
10
|
+
// file is a small hand-written lexer + recursive-descent parser over the same
|
|
11
|
+
// token patterns, so the accepted language is the same:
|
|
12
|
+
//
|
|
13
|
+
// pie [showData] [title <text> | accTitle: <text> | accDescr …]
|
|
14
|
+
// title <text> (own line; rest of line, unquoted)
|
|
15
|
+
// accTitle: <text>
|
|
16
|
+
// accDescr: <text>
|
|
17
|
+
// accDescr { <multi-line text> }
|
|
18
|
+
// "Label" : 42.5 (or 'Label'; label MUST be quoted)
|
|
19
|
+
//
|
|
20
|
+
// Mermaid's rules, each mirrored below:
|
|
21
|
+
// - `showData` is only valid directly after `pie`; a `showData` line on
|
|
22
|
+
// its own is a syntax error.
|
|
23
|
+
// - Numbers are `-?\d+\.\d+` or `-?(0|[1-9]\d*)` — no `+`, no exponent,
|
|
24
|
+
// no leading `.`, no trailing `.`.
|
|
25
|
+
// - Everything after a complete statement on the same line (other than a
|
|
26
|
+
// `%%` comment) is a syntax error.
|
|
27
|
+
// - A negative value is an error (pieDb.addSection), reported only once
|
|
28
|
+
// the whole chart has parsed, and before the duplicate-label check.
|
|
29
|
+
// - A zero value is accepted.
|
|
30
|
+
// - A repeated label keeps its first value; later ones are ignored.
|
|
31
|
+
// - A chart with no slices (a bare `pie`) is valid.
|
|
32
|
+
// - `title`/`accTitle`/`accDescr` may repeat; the last one wins, and an
|
|
33
|
+
// empty one counts as unset.
|
|
34
|
+
// - Keywords are case-sensitive (`Pie`, `showdata`, `Title` are errors).
|
|
35
|
+
//
|
|
36
|
+
// Why raw text and not `Statement[]` like the other mermaid-parser parsers:
|
|
37
|
+
// `splitStatements` splits on `;` and treats `'` as a quote when looking for
|
|
38
|
+
// `%%`, neither of which Mermaid's pie grammar does — `title A; B` is one
|
|
39
|
+
// title there, and a quoted label may even span lines. Lexing the source
|
|
40
|
+
// directly keeps those cases identical to Mermaid. The SVG registry's
|
|
41
|
+
// `parse(lines, text)` already hands every parser the raw text too.
|
|
42
|
+
//
|
|
43
|
+
// Earlier ports: lukilabs/beautiful-mermaid#151 (birenroy) — the starting
|
|
44
|
+
// point for the model and tests — and #150 (Daniele-rolli), whose parser
|
|
45
|
+
// rejected unknown lines instead of dropping them.
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
type TokenKind =
|
|
49
|
+
| 'NEWLINE'
|
|
50
|
+
| 'PIE'
|
|
51
|
+
| 'SHOW_DATA'
|
|
52
|
+
| 'COLON'
|
|
53
|
+
| 'NUMBER'
|
|
54
|
+
| 'STRING'
|
|
55
|
+
| 'TITLE'
|
|
56
|
+
| 'ACC_TITLE'
|
|
57
|
+
| 'ACC_DESCR'
|
|
58
|
+
|
|
59
|
+
interface Token {
|
|
60
|
+
kind: TokenKind
|
|
61
|
+
text: string
|
|
62
|
+
/** 1-based source line the token starts on. */
|
|
63
|
+
line: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Token patterns in Langium's lexing order: whitespace first, then
|
|
68
|
+
* keywords (longest first), then terminals. `null` kinds are hidden (skipped).
|
|
69
|
+
* Each is sticky so it only matches at the current position.
|
|
70
|
+
*
|
|
71
|
+
* `pie`/`showData` carry the `(?:(?=%%)|(?!\S))` suffix Mermaid's
|
|
72
|
+
* `AbstractMermaidTokenBuilder` adds: the keyword must be followed by
|
|
73
|
+
* whitespace, a comment, or the end of the text.
|
|
74
|
+
*/
|
|
75
|
+
const TOKEN_PATTERNS: ReadonlyArray<readonly [TokenKind | null, RegExp]> = [
|
|
76
|
+
[null, /[\t ]+/y],
|
|
77
|
+
['SHOW_DATA', /showData(?:(?=%%)|(?!\S))/y],
|
|
78
|
+
['PIE', /pie(?:(?=%%)|(?!\S))/y],
|
|
79
|
+
['COLON', /:/y],
|
|
80
|
+
['NUMBER', /-?[0-9]+\.[0-9]+(?!\.)|-?(?:0|[1-9][0-9]*)(?!\.)/y],
|
|
81
|
+
[
|
|
82
|
+
'ACC_DESCR',
|
|
83
|
+
/[\t ]*accDescr(?:[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)|\s*\{[^}]*\})/y,
|
|
84
|
+
],
|
|
85
|
+
['ACC_TITLE', /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/y],
|
|
86
|
+
['TITLE', /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/y],
|
|
87
|
+
['STRING', /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/y],
|
|
88
|
+
['NEWLINE', /\n/y],
|
|
89
|
+
[null, /[\t ]*%%[^\n\r]*/y],
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
/** Mermaid's `directiveRegex` (diagram-api/regexes.ts), verbatim. */
|
|
93
|
+
const DIRECTIVE_REGEX =
|
|
94
|
+
/%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi
|
|
95
|
+
|
|
96
|
+
/** Value extractors from Mermaid's common/matcher.ts. */
|
|
97
|
+
const ACC_DESCR_VALUE = /accDescr(?:[\t ]*:([^\n\r]*)|\s*\{([^}]*)\})/
|
|
98
|
+
const ACC_TITLE_VALUE = /accTitle[\t ]*:([^\n\r]*)/
|
|
99
|
+
const TITLE_VALUE = /title([\t ][^\n\r]*|)/
|
|
100
|
+
|
|
101
|
+
const SYNTAX_HELP =
|
|
102
|
+
'A pie chart is `pie [showData] [title <text>]` followed by lines of `"Label" : value` (label quoted with " or \'), `title <text>`, `accTitle: <text>`, or `accDescr: <text>`.'
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Mirror Mermaid's text preprocessing (preprocess.ts `cleanupText` and
|
|
106
|
+
* `removeDirectives`) that runs before the grammar sees the source:
|
|
107
|
+
* - CRLF / lone CR become LF;
|
|
108
|
+
* - double-quoted attributes inside HTML-like tags become single-quoted;
|
|
109
|
+
* - `%%{ … }%%` directives are removed. Unlike Mermaid, the newlines a
|
|
110
|
+
* multi-line directive spans are kept, so line numbers in errors still
|
|
111
|
+
* point at the caller's source.
|
|
112
|
+
*/
|
|
113
|
+
function preprocess(text: string): string {
|
|
114
|
+
return text
|
|
115
|
+
.replace(/\r\n?/g, '\n')
|
|
116
|
+
.replace(
|
|
117
|
+
/<(\w+)([^>]*)>/g,
|
|
118
|
+
(_match, tag: string, attributes: string) =>
|
|
119
|
+
'<' + tag + attributes.replace(/="([^"]*)"/g, "='$1'") + '>',
|
|
120
|
+
)
|
|
121
|
+
.replace(DIRECTIVE_REGEX, (directive) => directive.replace(/[^\n]/g, ''))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function countNewlines(text: string): number {
|
|
125
|
+
let count = 0
|
|
126
|
+
for (const ch of text) if (ch === '\n') count++
|
|
127
|
+
return count
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Build a `Line N: …` error quoting the offending source line. */
|
|
131
|
+
function lineError(
|
|
132
|
+
lines: readonly string[],
|
|
133
|
+
line: number,
|
|
134
|
+
message: string,
|
|
135
|
+
): Error {
|
|
136
|
+
/* v8 ignore next -- every caller passes a line inside `lines` */
|
|
137
|
+
const source = (lines[line - 1] ?? '').trim()
|
|
138
|
+
return new Error(`Line ${line}: ${message} in "${source}". ${SYNTAX_HELP}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** What the lexer expected at a position it couldn't tokenize. */
|
|
142
|
+
function lexHint(source: string, pos: number, previous?: Token): string {
|
|
143
|
+
const ch = source[pos]
|
|
144
|
+
if (ch === '"' || ch === "'") {
|
|
145
|
+
return `Unterminated quoted label (missing closing ${ch})`
|
|
146
|
+
}
|
|
147
|
+
if (previous?.kind === 'COLON') {
|
|
148
|
+
return 'Invalid slice value — expected a number like 42 or 42.5 after ":"'
|
|
149
|
+
}
|
|
150
|
+
const atLineStart =
|
|
151
|
+
previous === undefined ||
|
|
152
|
+
previous.kind === 'NEWLINE' ||
|
|
153
|
+
previous.kind === 'PIE' ||
|
|
154
|
+
previous.kind === 'SHOW_DATA'
|
|
155
|
+
if (atLineStart) {
|
|
156
|
+
return 'Unrecognized statement — slice labels must be quoted, e.g. "Dogs" : 42'
|
|
157
|
+
}
|
|
158
|
+
return `Unexpected text ${JSON.stringify(source.slice(pos).split('\n')[0])}`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function tokenize(source: string, lines: readonly string[]): Token[] {
|
|
162
|
+
const tokens: Token[] = []
|
|
163
|
+
let pos = 0
|
|
164
|
+
let line = 1
|
|
165
|
+
|
|
166
|
+
while (pos < source.length) {
|
|
167
|
+
let matched = false
|
|
168
|
+
for (const [kind, pattern] of TOKEN_PATTERNS) {
|
|
169
|
+
pattern.lastIndex = pos
|
|
170
|
+
const match = pattern.exec(source)
|
|
171
|
+
if (match === null || match[0].length === 0) continue
|
|
172
|
+
const text = match[0]
|
|
173
|
+
if (kind !== null) tokens.push({ kind, text, line })
|
|
174
|
+
line += countNewlines(text)
|
|
175
|
+
pos += text.length
|
|
176
|
+
matched = true
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
if (!matched) {
|
|
180
|
+
throw lineError(lines, line, lexHint(source, pos, tokens.at(-1)))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return tokens
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Langium's default `STRING` value conversion (ValueConverter.convertString):
|
|
189
|
+
* drop the surrounding quotes and resolve backslash escapes. Mermaid's pie
|
|
190
|
+
* value converter doesn't override it for slice labels.
|
|
191
|
+
*/
|
|
192
|
+
function convertString(input: string): string {
|
|
193
|
+
let result = ''
|
|
194
|
+
for (let i = 1; i < input.length - 1; i++) {
|
|
195
|
+
const ch = input.charAt(i)
|
|
196
|
+
if (ch !== '\\') {
|
|
197
|
+
result += ch
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
const escaped = input.charAt(++i)
|
|
201
|
+
switch (escaped) {
|
|
202
|
+
case 'b':
|
|
203
|
+
result += '\b'
|
|
204
|
+
break
|
|
205
|
+
case 'f':
|
|
206
|
+
result += '\f'
|
|
207
|
+
break
|
|
208
|
+
case 'n':
|
|
209
|
+
result += '\n'
|
|
210
|
+
break
|
|
211
|
+
case 'r':
|
|
212
|
+
result += '\r'
|
|
213
|
+
break
|
|
214
|
+
case 't':
|
|
215
|
+
result += '\t'
|
|
216
|
+
break
|
|
217
|
+
case 'v':
|
|
218
|
+
result += '\v'
|
|
219
|
+
break
|
|
220
|
+
case '0':
|
|
221
|
+
result += '\0'
|
|
222
|
+
break
|
|
223
|
+
default:
|
|
224
|
+
result += escaped
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Mermaid's `AbstractMermaidValueConverter.runCommonConverter` for the
|
|
232
|
+
* TITLE / ACC_TITLE / ACC_DESCR tokens.
|
|
233
|
+
*/
|
|
234
|
+
function convertTitleLike(kind: TokenKind, text: string): string {
|
|
235
|
+
const regex =
|
|
236
|
+
kind === 'ACC_DESCR'
|
|
237
|
+
? ACC_DESCR_VALUE
|
|
238
|
+
: kind === 'ACC_TITLE'
|
|
239
|
+
? ACC_TITLE_VALUE
|
|
240
|
+
: TITLE_VALUE
|
|
241
|
+
const match = regex.exec(text)
|
|
242
|
+
if (match?.[1] !== undefined) {
|
|
243
|
+
return match[1].trim().replace(/[\t ]{2,}/gm, ' ')
|
|
244
|
+
}
|
|
245
|
+
// Only the `accDescr { … }` form leaves group 1 unset, and it always sets
|
|
246
|
+
// group 2: every TITLE / ACC_TITLE / ACC_DESCR token matches its value
|
|
247
|
+
// regex. (Mermaid's converter returns undefined if neither group is set.)
|
|
248
|
+
/* v8 ignore next */
|
|
249
|
+
const braced = match?.[2] ?? ''
|
|
250
|
+
return braced
|
|
251
|
+
.replace(/^\s*/gm, '')
|
|
252
|
+
.replace(/\s+$/gm, '')
|
|
253
|
+
.replace(/[\t ]{2,}/gm, ' ')
|
|
254
|
+
.replace(/[\n\r]{2,}/gm, '\n')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const DESCRIBE: Record<TokenKind, string> = {
|
|
258
|
+
NEWLINE: 'end of line',
|
|
259
|
+
PIE: '"pie"',
|
|
260
|
+
SHOW_DATA: '"showData"',
|
|
261
|
+
COLON: '":"',
|
|
262
|
+
NUMBER: 'number',
|
|
263
|
+
STRING: 'quoted label',
|
|
264
|
+
TITLE: 'title',
|
|
265
|
+
ACC_TITLE: 'accTitle',
|
|
266
|
+
ACC_DESCR: 'accDescr',
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface RawSlice extends PieSlice {
|
|
270
|
+
line: number
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Parse Mermaid pie chart source text.
|
|
275
|
+
*
|
|
276
|
+
* Throws `Line N: …` errors (the shape the MCP diagnostics expect) for
|
|
277
|
+
* anything Mermaid's own parser would reject.
|
|
278
|
+
*/
|
|
279
|
+
export function parsePieChart(text: string): PieChart {
|
|
280
|
+
const source = preprocess(text)
|
|
281
|
+
const lines = source.split('\n')
|
|
282
|
+
const tokens = tokenize(source, lines)
|
|
283
|
+
let i = 0
|
|
284
|
+
|
|
285
|
+
const unexpected = (token: Token | undefined, expected: string): Error => {
|
|
286
|
+
if (token === undefined) {
|
|
287
|
+
return lineError(
|
|
288
|
+
lines,
|
|
289
|
+
lines.length,
|
|
290
|
+
`Unexpected end of input — expected ${expected}`,
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
let message = `Unexpected ${DESCRIBE[token.kind]} — expected ${expected}`
|
|
294
|
+
if (token.kind === 'SHOW_DATA') {
|
|
295
|
+
message +=
|
|
296
|
+
'; showData is only allowed directly after "pie" on the header line'
|
|
297
|
+
} else if (token.kind === 'PIE') {
|
|
298
|
+
message += '; the "pie" header may only appear once'
|
|
299
|
+
}
|
|
300
|
+
return lineError(lines, token.line, message)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Statements end at a newline or the end of the text (Mermaid's `EOL`). */
|
|
304
|
+
const expectEndOfStatement = (what: string): void => {
|
|
305
|
+
const next = tokens[i]
|
|
306
|
+
if (next !== undefined && next.kind !== 'NEWLINE') {
|
|
307
|
+
throw unexpected(next, `end of line after ${what}`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
while (tokens[i]?.kind === 'NEWLINE') i++
|
|
312
|
+
if (tokens[i]?.kind !== 'PIE') {
|
|
313
|
+
throw unexpected(tokens[i], 'the "pie" header')
|
|
314
|
+
}
|
|
315
|
+
i++
|
|
316
|
+
|
|
317
|
+
let showData = false
|
|
318
|
+
if (tokens[i]?.kind === 'SHOW_DATA') {
|
|
319
|
+
showData = true
|
|
320
|
+
i++
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let title = ''
|
|
324
|
+
let accTitle = ''
|
|
325
|
+
let accDescr = ''
|
|
326
|
+
const rawSlices: RawSlice[] = []
|
|
327
|
+
|
|
328
|
+
while (i < tokens.length) {
|
|
329
|
+
const token = tokens[i]!
|
|
330
|
+
switch (token.kind) {
|
|
331
|
+
case 'NEWLINE':
|
|
332
|
+
i++
|
|
333
|
+
break
|
|
334
|
+
case 'TITLE':
|
|
335
|
+
case 'ACC_TITLE':
|
|
336
|
+
case 'ACC_DESCR': {
|
|
337
|
+
const value = convertTitleLike(token.kind, token.text)
|
|
338
|
+
if (token.kind === 'TITLE') title = value
|
|
339
|
+
else if (token.kind === 'ACC_TITLE') accTitle = value
|
|
340
|
+
else accDescr = value
|
|
341
|
+
i++
|
|
342
|
+
expectEndOfStatement(DESCRIBE[token.kind])
|
|
343
|
+
break
|
|
344
|
+
}
|
|
345
|
+
case 'STRING': {
|
|
346
|
+
i++
|
|
347
|
+
if (tokens[i]?.kind !== 'COLON') {
|
|
348
|
+
throw unexpected(tokens[i], '":" after the slice label')
|
|
349
|
+
}
|
|
350
|
+
i++
|
|
351
|
+
const valueToken = tokens[i]
|
|
352
|
+
if (valueToken?.kind !== 'NUMBER') {
|
|
353
|
+
throw unexpected(valueToken, 'a number after ":"')
|
|
354
|
+
}
|
|
355
|
+
i++
|
|
356
|
+
expectEndOfStatement('the slice value')
|
|
357
|
+
rawSlices.push({
|
|
358
|
+
label: convertString(token.text),
|
|
359
|
+
value: Number(valueToken.text),
|
|
360
|
+
line: token.line,
|
|
361
|
+
})
|
|
362
|
+
break
|
|
363
|
+
}
|
|
364
|
+
default:
|
|
365
|
+
throw unexpected(
|
|
366
|
+
token,
|
|
367
|
+
'a quoted slice label, title, accTitle, or accDescr',
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Mermaid's pieDb.addSection, applied in source order once the whole
|
|
373
|
+
// chart has parsed: negative check first, then first-label-wins.
|
|
374
|
+
const slices: PieSlice[] = []
|
|
375
|
+
const seen = new Set<string>()
|
|
376
|
+
for (const { label, value, line } of rawSlices) {
|
|
377
|
+
if (value < 0) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
`Line ${line}: "${label}" has invalid value: ${value}. Negative values are not allowed in pie charts. All slice values must be >= 0.`,
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
if (seen.has(label)) continue
|
|
383
|
+
seen.add(label)
|
|
384
|
+
// `-0` passes Mermaid's `< 0` check; store it as plain 0.
|
|
385
|
+
slices.push({ label, value: value === 0 ? 0 : value })
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const chart: PieChart = { showData, slices }
|
|
389
|
+
if (title) chart.title = title
|
|
390
|
+
if (accTitle) chart.accTitle = accTitle
|
|
391
|
+
if (accDescr) chart.accDescr = accDescr
|
|
392
|
+
return chart
|
|
393
|
+
}
|
package/src/pie/types.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Pie chart types
|
|
3
|
+
//
|
|
4
|
+
// The parsed (logical) model of a Mermaid `pie` diagram, plus the positioned
|
|
5
|
+
// model the SVG renderer draws (`PositionedPieChart`, produced by
|
|
6
|
+
// `layoutPieChart` in `@zombie-mermaid/svg-renderer`). The ASCII renderer
|
|
7
|
+
// works from the parsed `PieChart` directly, like xychart's does.
|
|
8
|
+
//
|
|
9
|
+
// Shape adapted from lukilabs/beautiful-mermaid#151 (birenroy), extended with
|
|
10
|
+
// the accessibility fields Mermaid's own pie grammar accepts.
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
/** One slice of a pie chart, in source order. */
|
|
14
|
+
export interface PieSlice {
|
|
15
|
+
/** Slice label, with its surrounding quotes removed and escapes resolved. */
|
|
16
|
+
label: string
|
|
17
|
+
/** Slice value. Never negative; zero is allowed (Mermaid accepts it). */
|
|
18
|
+
value: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A parsed Mermaid pie chart. */
|
|
22
|
+
export interface PieChart {
|
|
23
|
+
/** `title …`, either on the `pie` header line or on its own line. */
|
|
24
|
+
title?: string
|
|
25
|
+
/** `accTitle: …` accessible title. */
|
|
26
|
+
accTitle?: string
|
|
27
|
+
/** `accDescr: …` or multi-line `accDescr { … }` accessible description. */
|
|
28
|
+
accDescr?: string
|
|
29
|
+
/** `pie showData` — show each slice's raw value next to its legend label. */
|
|
30
|
+
showData: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Slices in source order. A label that repeats keeps its first value and
|
|
33
|
+
* position; later duplicates are ignored, as in Mermaid. May be empty:
|
|
34
|
+
* Mermaid accepts a bare `pie` with no slices.
|
|
35
|
+
*/
|
|
36
|
+
slices: PieSlice[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Positioned pie chart — ready for SVG rendering
|
|
41
|
+
//
|
|
42
|
+
// Geometry follows Mermaid's own pieRenderer.ts: a 450px-tall frame, a pie of
|
|
43
|
+
// radius 185 centred at (225, 225) before any viewBox shift, slices drawn
|
|
44
|
+
// clockwise from 12 o'clock in source order, percentage labels at 0.75 of the
|
|
45
|
+
// radius, and a legend to the right. All coordinates here are absolute SVG
|
|
46
|
+
// user units with the viewBox already shifted to start at (0, 0).
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
/** One drawn slice. Slices under 1% of the total are never positioned. */
|
|
50
|
+
export interface PositionedPieSlice {
|
|
51
|
+
/** Slice label (as parsed). */
|
|
52
|
+
label: string
|
|
53
|
+
/** Slice value (as parsed). */
|
|
54
|
+
value: number
|
|
55
|
+
/**
|
|
56
|
+
* Palette slot: the slice's index among *all* slices in source order,
|
|
57
|
+
* modulo 12 — Mermaid's `pie1`..`pie12` colour scale is keyed by every
|
|
58
|
+
* label, including slices too small to draw, and wraps after 12.
|
|
59
|
+
*/
|
|
60
|
+
colorIndex: number
|
|
61
|
+
/** Start angle, radians clockwise from 12 o'clock. */
|
|
62
|
+
startAngle: number
|
|
63
|
+
/** End angle, radians clockwise from 12 o'clock. */
|
|
64
|
+
endAngle: number
|
|
65
|
+
/** SVG path data for the wedge (or full circle for a 100% slice). */
|
|
66
|
+
path: string
|
|
67
|
+
/** Percentage label, `((value / total) * 100).toFixed(0) + '%'` as in Mermaid. */
|
|
68
|
+
percentText: string
|
|
69
|
+
/** Anchor of the percentage label (text-anchor middle, alphabetic baseline). */
|
|
70
|
+
labelX: number
|
|
71
|
+
labelY: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One legend row: a colour swatch and its label. Every slice gets one. */
|
|
75
|
+
export interface PositionedPieLegendItem {
|
|
76
|
+
/** Row text: the label, or `label [value]` when `showData` is on. */
|
|
77
|
+
text: string
|
|
78
|
+
colorIndex: number
|
|
79
|
+
/** Top-left corner of the square swatch. */
|
|
80
|
+
x: number
|
|
81
|
+
y: number
|
|
82
|
+
/** Swatch side length. */
|
|
83
|
+
size: number
|
|
84
|
+
/** Text anchor (text-anchor start, alphabetic baseline). */
|
|
85
|
+
textX: number
|
|
86
|
+
textY: number
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface PositionedPieChart {
|
|
90
|
+
width: number
|
|
91
|
+
height: number
|
|
92
|
+
/** Pie centre. */
|
|
93
|
+
cx: number
|
|
94
|
+
cy: number
|
|
95
|
+
/** Slice radius. */
|
|
96
|
+
radius: number
|
|
97
|
+
/** Radius of the outline circle drawn around the pie. */
|
|
98
|
+
outerRadius: number
|
|
99
|
+
/** Title text and its anchor (text-anchor middle, alphabetic baseline). */
|
|
100
|
+
title?: { text: string; x: number; y: number }
|
|
101
|
+
/** Drawn slices, in source order. */
|
|
102
|
+
slices: PositionedPieSlice[]
|
|
103
|
+
/** Legend rows, one per parsed slice, in source order. */
|
|
104
|
+
legend: PositionedPieLegendItem[]
|
|
105
|
+
/** `accTitle`, carried through for the SVG accessible name. */
|
|
106
|
+
accTitle?: string
|
|
107
|
+
/** `accDescr`, carried through for the SVG `<desc>`. */
|
|
108
|
+
accDescr?: string
|
|
109
|
+
}
|