@zombie-mermaid/mermaid-parser 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 +9 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +340 -179
- 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 +7 -4
- package/src/index.ts +3 -0
- package/src/pie/parser.ts +393 -0
- package/src/pie/types.ts +109 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parsePieChart } from '../index.ts'
|
|
3
|
+
|
|
4
|
+
// The first block is lukilabs/beautiful-mermaid#151's pie-parser.test.ts
|
|
5
|
+
// (birenroy), adapted. Where #151 accepted something Mermaid rejects, or
|
|
6
|
+
// read it differently, the case now asserts Mermaid's behavior and says so.
|
|
7
|
+
// The strictness cases after it cover Mermaid rules #151 didn't enforce,
|
|
8
|
+
// several of which #150's (Daniele-rolli) parser already did.
|
|
9
|
+
|
|
10
|
+
describe('upstream #151: parsePieChart', () => {
|
|
11
|
+
it('parses basic pie chart with double-quoted labels', () => {
|
|
12
|
+
const chart = parsePieChart(`pie
|
|
13
|
+
"Dogs" : 386
|
|
14
|
+
"Cats" : 85
|
|
15
|
+
"Rats" : 15`)
|
|
16
|
+
expect(chart).toEqual({
|
|
17
|
+
showData: false,
|
|
18
|
+
slices: [
|
|
19
|
+
{ label: 'Dogs', value: 386 },
|
|
20
|
+
{ label: 'Cats', value: 85 },
|
|
21
|
+
{ label: 'Rats', value: 15 },
|
|
22
|
+
],
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('parses title in header line: pie title <title>', () => {
|
|
27
|
+
const chart = parsePieChart(`pie title Pets adopted by volunteers
|
|
28
|
+
"Dogs" : 386
|
|
29
|
+
"Cats" : 85`)
|
|
30
|
+
expect(chart.title).toBe('Pets adopted by volunteers')
|
|
31
|
+
expect(chart.showData).toBe(false)
|
|
32
|
+
expect(chart.slices).toHaveLength(2)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// #151 stripped the quotes. Mermaid's title is the raw rest of the line,
|
|
36
|
+
// so the quotes are part of it.
|
|
37
|
+
it('keeps quotes in a quoted title, as Mermaid does', () => {
|
|
38
|
+
const chart = parsePieChart(`pie title "Pets adopted by volunteers"
|
|
39
|
+
"Dogs" : 386`)
|
|
40
|
+
expect(chart.title).toBe('"Pets adopted by volunteers"')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('parses standalone title directive', () => {
|
|
44
|
+
const chart = parsePieChart(`pie
|
|
45
|
+
title Key Elements in Product X
|
|
46
|
+
"Calcium" : 42.96
|
|
47
|
+
"Potassium" : 50.05`)
|
|
48
|
+
expect(chart.title).toBe('Key Elements in Product X')
|
|
49
|
+
expect(chart.slices).toHaveLength(2)
|
|
50
|
+
expect(chart.slices[0]!.value).toBe(42.96)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('parses showData on the header line', () => {
|
|
54
|
+
const chart = parsePieChart(`pie showData title Key elements
|
|
55
|
+
"Calcium" : 42.96
|
|
56
|
+
"Potassium" : 50.05`)
|
|
57
|
+
expect(chart.showData).toBe(true)
|
|
58
|
+
expect(chart.title).toBe('Key elements')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// #151 accepted a standalone `showData` line. Mermaid's grammar only
|
|
62
|
+
// allows it directly after `pie`.
|
|
63
|
+
it('rejects showData on its own line', () => {
|
|
64
|
+
expect(() =>
|
|
65
|
+
parsePieChart(`pie
|
|
66
|
+
showData
|
|
67
|
+
title Elements
|
|
68
|
+
"A" : 10`),
|
|
69
|
+
).toThrow(/Line 2: .*showData is only allowed directly after "pie"/)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('parses single-quoted labels', () => {
|
|
73
|
+
const chart = parsePieChart(`pie
|
|
74
|
+
'Calcium' : 42.96
|
|
75
|
+
"Magnesium" : 10.01`)
|
|
76
|
+
expect(chart.slices).toEqual([
|
|
77
|
+
{ label: 'Calcium', value: 42.96 },
|
|
78
|
+
{ label: 'Magnesium', value: 10.01 },
|
|
79
|
+
])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// #151 accepted unquoted labels. Mermaid requires quotes.
|
|
83
|
+
it('rejects unquoted slice labels', () => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
parsePieChart(`pie
|
|
86
|
+
Potassium : 50.05`),
|
|
87
|
+
).toThrow(/Line 2: .*slice labels must be quoted/)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('parses quoted labels containing colons', () => {
|
|
91
|
+
const chart = parsePieChart(`pie
|
|
92
|
+
"Ratio 1:2" : 30
|
|
93
|
+
"Ratio 2:3" : 70`)
|
|
94
|
+
expect(chart.slices).toEqual([
|
|
95
|
+
{ label: 'Ratio 1:2', value: 30 },
|
|
96
|
+
{ label: 'Ratio 2:3', value: 70 },
|
|
97
|
+
])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('ignores comments and empty lines', () => {
|
|
101
|
+
const chart = parsePieChart(`pie
|
|
102
|
+
%% This is a comment
|
|
103
|
+
"Slice A" : 10
|
|
104
|
+
|
|
105
|
+
%% Another comment
|
|
106
|
+
"Slice B" : 20`)
|
|
107
|
+
expect(chart.slices).toHaveLength(2)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('does not read "showdata" inside a title as the showData flag', () => {
|
|
111
|
+
const chart = parsePieChart(`pie title How to showdata properly
|
|
112
|
+
"A" : 10`)
|
|
113
|
+
expect(chart.showData).toBe(false)
|
|
114
|
+
expect(chart.title).toBe('How to showdata properly')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// #151 accepted `title: …`. In Mermaid's grammar `title` must be followed
|
|
118
|
+
// by whitespace, so the colon is a syntax error.
|
|
119
|
+
it('rejects title: with a colon', () => {
|
|
120
|
+
expect(() =>
|
|
121
|
+
parsePieChart(`pie
|
|
122
|
+
title: My Chart
|
|
123
|
+
"A" : 10`),
|
|
124
|
+
).toThrow(/Line 2: /)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// #151 dropped accTitle/accDescr. They are now kept.
|
|
128
|
+
it('keeps accTitle and accDescr alongside the slices', () => {
|
|
129
|
+
const chart = parsePieChart(`pie title Accessibility Chart
|
|
130
|
+
accTitle: 2024 Chart
|
|
131
|
+
accDescr: An accessibility description
|
|
132
|
+
"A" : 10
|
|
133
|
+
"B" : 20`)
|
|
134
|
+
expect(chart).toEqual({
|
|
135
|
+
title: 'Accessibility Chart',
|
|
136
|
+
accTitle: '2024 Chart',
|
|
137
|
+
accDescr: 'An accessibility description',
|
|
138
|
+
showData: false,
|
|
139
|
+
slices: [
|
|
140
|
+
{ label: 'A', value: 10 },
|
|
141
|
+
{ label: 'B', value: 20 },
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('Mermaid docs examples', () => {
|
|
148
|
+
it('parses the basic example', () => {
|
|
149
|
+
const chart = parsePieChart(`pie title Pets adopted by volunteers
|
|
150
|
+
"Dogs" : 386
|
|
151
|
+
"Cats" : 85
|
|
152
|
+
"Rats" : 15`)
|
|
153
|
+
expect(chart.title).toBe('Pets adopted by volunteers')
|
|
154
|
+
expect(chart.slices.map((s) => s.label)).toEqual(['Dogs', 'Cats', 'Rats'])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('parses the showData example body', () => {
|
|
158
|
+
const chart = parsePieChart(`pie showData
|
|
159
|
+
title Key elements in Product X
|
|
160
|
+
"Calcium" : 42.96
|
|
161
|
+
"Potassium" : 50.05
|
|
162
|
+
"Magnesium" : 10.01
|
|
163
|
+
"Iron" : 5`)
|
|
164
|
+
expect(chart.showData).toBe(true)
|
|
165
|
+
expect(chart.title).toBe('Key elements in Product X')
|
|
166
|
+
expect(chart.slices.map((s) => s.value)).toEqual([42.96, 50.05, 10.01, 5])
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('values', () => {
|
|
171
|
+
it("rejects a negative value with Mermaid's message and the line number", () => {
|
|
172
|
+
expect(() => parsePieChart('pie\n"A" : 1\n"B" : -2')).toThrow(
|
|
173
|
+
'Line 3: "B" has invalid value: -2. Negative values are not allowed in pie charts. All slice values must be >= 0.',
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('rejects a negative value even when its label repeats an earlier one', () => {
|
|
178
|
+
expect(() => parsePieChart('pie\n"A" : 1\n"A" : -2')).toThrow(
|
|
179
|
+
/"A" has invalid value: -2/,
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('reports a syntax error before a negative value earlier in the chart', () => {
|
|
184
|
+
expect(() => parsePieChart('pie\n"A" : -1\nnonsense')).toThrow(/^Line 3: /)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('accepts zero', () => {
|
|
188
|
+
expect(parsePieChart('pie\n"A" : 0\n"B" : 1').slices).toEqual([
|
|
189
|
+
{ label: 'A', value: 0 },
|
|
190
|
+
{ label: 'B', value: 1 },
|
|
191
|
+
])
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('accepts -0 (Mermaid only rejects values < 0) and stores it as 0', () => {
|
|
195
|
+
const [slice] = parsePieChart('pie\n"A" : -0').slices
|
|
196
|
+
expect(Object.is(slice!.value, 0)).toBe(true)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('accepts any number of decimal places and leading zeros before a decimal point', () => {
|
|
200
|
+
expect(parsePieChart('pie\n"A" : 1.23456\n"B" : 00.5').slices).toEqual([
|
|
201
|
+
{ label: 'A', value: 1.23456 },
|
|
202
|
+
{ label: 'B', value: 0.5 },
|
|
203
|
+
])
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it.each([
|
|
207
|
+
['a plus sign', '+5'],
|
|
208
|
+
['a plus sign on a decimal', '+5.5'],
|
|
209
|
+
['a leading dot', '.5'],
|
|
210
|
+
['a trailing dot', '5.'],
|
|
211
|
+
['an exponent', '1e3'],
|
|
212
|
+
['a leading zero on an integer', '05'],
|
|
213
|
+
['two dots', '1.2.3'],
|
|
214
|
+
['a unit suffix', '10px'],
|
|
215
|
+
['a word', 'ten'],
|
|
216
|
+
['a space after the minus', '- 5'],
|
|
217
|
+
])('rejects a value with %s (%s)', (_name, value) => {
|
|
218
|
+
expect(() => parsePieChart(`pie\n"A" : ${value}`)).toThrow(/^Line 2: /)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('rejects a slice with no value', () => {
|
|
222
|
+
expect(() => parsePieChart('pie\n"A" :')).toThrow(/expected a number/)
|
|
223
|
+
expect(() => parsePieChart('pie\n"A" : \n"B" : 1')).toThrow(
|
|
224
|
+
/Line 2: .*expected a number/,
|
|
225
|
+
)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('rejects a slice with no colon', () => {
|
|
229
|
+
expect(() => parsePieChart('pie\n"A" 10')).toThrow(
|
|
230
|
+
/Line 2: .*expected ":" after the slice label/,
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('labels', () => {
|
|
236
|
+
it('keeps the first value of a repeated label, in its original position', () => {
|
|
237
|
+
expect(parsePieChart('pie\n"A" : 1\n"B" : 2\n"A" : 3').slices).toEqual([
|
|
238
|
+
{ label: 'A', value: 1 },
|
|
239
|
+
{ label: 'B', value: 2 },
|
|
240
|
+
])
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('keeps slices in source order rather than sorting them', () => {
|
|
244
|
+
expect(
|
|
245
|
+
parsePieChart('pie\n"small" : 1\n"big" : 100\n"mid" : 10').slices.map(
|
|
246
|
+
(s) => s.label,
|
|
247
|
+
),
|
|
248
|
+
).toEqual(['small', 'big', 'mid'])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('resolves backslash escapes the way Langium does', () => {
|
|
252
|
+
expect(
|
|
253
|
+
parsePieChart(String.raw`pie
|
|
254
|
+
"say \"hi\"" : 1
|
|
255
|
+
'it\'s' : 2
|
|
256
|
+
"tab\there" : 3`).slices.map((s) => s.label),
|
|
257
|
+
).toEqual(['say "hi"', "it's", 'tab\there'])
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
// Langium's convertEscapeCharacter maps exactly these seven letters; any
|
|
261
|
+
// other escaped character stands for itself. Checked against
|
|
262
|
+
// @mermaid-js/parser 2.0.1, which turns
|
|
263
|
+
// "b\bf\fn\nr\rt\tv\v0\0q\"z\z" into "b\bf\fn\nr\rt\tv\u000b0\u0000q\"zz".
|
|
264
|
+
it.each([
|
|
265
|
+
['\\b', '\b'],
|
|
266
|
+
['\\f', '\f'],
|
|
267
|
+
['\\n', '\n'],
|
|
268
|
+
['\\r', '\r'],
|
|
269
|
+
['\\t', '\t'],
|
|
270
|
+
['\\v', '\v'],
|
|
271
|
+
['\\0', '\0'],
|
|
272
|
+
['\\z', 'z'],
|
|
273
|
+
['\\\\', '\\'],
|
|
274
|
+
])('resolves the %s escape in a label', (escape, resolved) => {
|
|
275
|
+
expect(parsePieChart(`pie\n"a${escape}b" : 1`).slices).toEqual([
|
|
276
|
+
{ label: `a${resolved}b`, value: 1 },
|
|
277
|
+
])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('keeps the other quote character literally', () => {
|
|
281
|
+
expect(
|
|
282
|
+
parsePieChart(`pie\n"it's" : 1\n'say "hi"' : 2`).slices.map(
|
|
283
|
+
(s) => s.label,
|
|
284
|
+
),
|
|
285
|
+
).toEqual(["it's", 'say "hi"'])
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('keeps surrounding spaces and allows an empty label', () => {
|
|
289
|
+
expect(
|
|
290
|
+
parsePieChart('pie\n" padded " : 1\n"" : 2').slices.map((s) => s.label),
|
|
291
|
+
).toEqual([' padded ', ''])
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('treats ; and %% inside a label as text', () => {
|
|
295
|
+
expect(
|
|
296
|
+
parsePieChart('pie\n"a; b" : 1\n"50%% off" : 2').slices.map(
|
|
297
|
+
(s) => s.label,
|
|
298
|
+
),
|
|
299
|
+
).toEqual(['a; b', '50%% off'])
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('accepts a quoted label that spans lines, as the grammar does', () => {
|
|
303
|
+
const chart = parsePieChart('pie\n"two\nlines" : 1\n"B" : 2')
|
|
304
|
+
expect(chart.slices[0]!.label).toBe('two\nlines')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('rejects an unterminated quoted label', () => {
|
|
308
|
+
expect(() => parsePieChart('pie\n"A : 1')).toThrow(
|
|
309
|
+
/Line 2: Unterminated quoted label/,
|
|
310
|
+
)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('statements and structure', () => {
|
|
315
|
+
it('accepts a bare pie with no slices (Mermaid renders an empty chart)', () => {
|
|
316
|
+
expect(parsePieChart('pie')).toEqual({ showData: false, slices: [] })
|
|
317
|
+
expect(parsePieChart('pie title Nothing yet')).toEqual({
|
|
318
|
+
title: 'Nothing yet',
|
|
319
|
+
showData: false,
|
|
320
|
+
slices: [],
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('accepts a slice on the header line, as the grammar does', () => {
|
|
325
|
+
expect(parsePieChart('pie "A" : 1\n"B" : 2').slices).toHaveLength(2)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('rejects two statements on one line', () => {
|
|
329
|
+
expect(() => parsePieChart('pie\n"A" : 1 "B" : 2')).toThrow(
|
|
330
|
+
/Line 2: .*expected end of line after the slice value/,
|
|
331
|
+
)
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('does not treat ; as a statement separator', () => {
|
|
335
|
+
expect(() => parsePieChart('pie\n"A" : 1; "B" : 2')).toThrow(/Line 2: /)
|
|
336
|
+
expect(() => parsePieChart('pie;"A" : 1')).toThrow(/Line 1: /)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('keeps ; in a title, which runs to the end of the line', () => {
|
|
340
|
+
expect(parsePieChart('pie title A; B').title).toBe('A; B')
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('rejects unknown statements', () => {
|
|
344
|
+
expect(() => parsePieChart('pie\n"A" : 1\nnonsense here')).toThrow(
|
|
345
|
+
/Line 3: Unrecognized statement.*"nonsense here"/,
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('rejects unknown header modifiers', () => {
|
|
350
|
+
expect(() => parsePieChart('pie banana\n"A" : 1')).toThrow(/Line 1: /)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('rejects a second pie header', () => {
|
|
354
|
+
expect(() => parsePieChart('pie\npie')).toThrow(
|
|
355
|
+
/Line 2: .*may only appear once/,
|
|
356
|
+
)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('rejects input that does not start with pie', () => {
|
|
360
|
+
expect(() => parsePieChart('"A" : 1')).toThrow(/expected the "pie" header/)
|
|
361
|
+
expect(() => parsePieChart('')).toThrow(/expected the "pie" header/)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('allows blank lines and comments before the header', () => {
|
|
365
|
+
expect(parsePieChart('\n%% note\n \npie\n"A" : 1').slices).toHaveLength(1)
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('matches keywords case-sensitively', () => {
|
|
369
|
+
expect(() => parsePieChart('Pie\n"A" : 1')).toThrow()
|
|
370
|
+
expect(() => parsePieChart('pie showdata\n"A" : 1')).toThrow()
|
|
371
|
+
expect(() => parsePieChart('pie\nTitle X')).toThrow()
|
|
372
|
+
expect(() => parsePieChart('pie\naccdescr: X')).toThrow()
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('requires whitespace after the pie and showData keywords', () => {
|
|
376
|
+
expect(() => parsePieChart('pieshowData')).toThrow()
|
|
377
|
+
expect(() => parsePieChart('pie showDatatitle X')).toThrow()
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('allows a comment directly after a keyword', () => {
|
|
381
|
+
expect(parsePieChart('pie%% c\n"A" : 1').slices).toHaveLength(1)
|
|
382
|
+
expect(parsePieChart('pie showData%% c\n"A" : 1').showData).toBe(true)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('accepts CRLF line endings', () => {
|
|
386
|
+
expect(
|
|
387
|
+
parsePieChart('pie title T\r\n"A" : 1\r\n"B" : 2\r\n').slices,
|
|
388
|
+
).toHaveLength(2)
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('skips %%{init}%% directives, including multi-line ones', () => {
|
|
392
|
+
const chart = parsePieChart(
|
|
393
|
+
'%%{init: {"pie": {"textPosition": 0.5}}}%%\npie\n%%{\n init: {}\n}%%\n"A" : 1',
|
|
394
|
+
)
|
|
395
|
+
expect(chart.slices).toEqual([{ label: 'A', value: 1 }])
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('reports physical line numbers across a multi-line directive', () => {
|
|
399
|
+
expect(() => parsePieChart('%%{\ninit: {}\n}%%\npie\nbad')).toThrow(
|
|
400
|
+
/^Line 5: /,
|
|
401
|
+
)
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
describe('title and accessibility', () => {
|
|
406
|
+
it('strips a trailing %% comment from title/accTitle/accDescr', () => {
|
|
407
|
+
const chart = parsePieChart(`pie
|
|
408
|
+
title My title %% note
|
|
409
|
+
accTitle: Acc %% note
|
|
410
|
+
accDescr: Desc %% note`)
|
|
411
|
+
expect(chart.title).toBe('My title')
|
|
412
|
+
expect(chart.accTitle).toBe('Acc')
|
|
413
|
+
expect(chart.accDescr).toBe('Desc')
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('strips a comment even after an apostrophe in the title', () => {
|
|
417
|
+
expect(parsePieChart("pie title Bob's pets %% note").title).toBe(
|
|
418
|
+
"Bob's pets",
|
|
419
|
+
)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('collapses runs of spaces and tabs', () => {
|
|
423
|
+
expect(parsePieChart('pie title a \t b').title).toBe('a b')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('lets the last title win, and treats an empty title as unset', () => {
|
|
427
|
+
expect(parsePieChart('pie title A\ntitle B').title).toBe('B')
|
|
428
|
+
expect(parsePieChart('pie title A\ntitle').title).toBeUndefined()
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('treats empty accTitle/accDescr as unset', () => {
|
|
432
|
+
const chart = parsePieChart('pie\naccTitle:\naccDescr {}')
|
|
433
|
+
expect(chart.accTitle).toBeUndefined()
|
|
434
|
+
expect(chart.accDescr).toBeUndefined()
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('accepts accTitle/accDescr with space before the colon', () => {
|
|
438
|
+
const chart = parsePieChart('pie\naccTitle : T\naccDescr : D')
|
|
439
|
+
expect(chart.accTitle).toBe('T')
|
|
440
|
+
expect(chart.accDescr).toBe('D')
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('rejects accTitle without a colon', () => {
|
|
444
|
+
expect(() => parsePieChart('pie\naccTitle T')).toThrow(/Line 2: /)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('rejects text after a multi-line accDescr block on the same line', () => {
|
|
448
|
+
expect(() => parsePieChart('pie\naccDescr {\n d\n} "A" : 1')).toThrow(
|
|
449
|
+
/Line 4: .*expected end of line after accDescr/,
|
|
450
|
+
)
|
|
451
|
+
})
|
|
452
|
+
})
|
package/src/flowchart-parser.ts
CHANGED
|
@@ -65,7 +65,7 @@ export { toDirection } from '@zombie-mermaid/core'
|
|
|
65
65
|
* way sequence/class/ER/xychart were before issue #541.
|
|
66
66
|
*/
|
|
67
67
|
const SUPPORTED_HEADERS =
|
|
68
|
-
'"graph <dir>"/"flowchart <dir>" (dir: TD, TB, LR, BT, RL), "stateDiagram-v2", "sequenceDiagram", "classDiagram", "erDiagram", "xychart-beta", "C4Context"/"C4Container"/"C4Component"/"C4Dynamic"/"C4Deployment", "architecture-beta"'
|
|
68
|
+
'"graph <dir>"/"flowchart <dir>" (dir: TD, TB, LR, BT, RL), "stateDiagram-v2", "sequenceDiagram", "classDiagram", "erDiagram", "xychart-beta", "C4Context"/"C4Container"/"C4Component"/"C4Dynamic"/"C4Deployment", "architecture-beta", "pie"'
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Best-guess canonical header for a header line that looks like an attempt
|
|
@@ -76,8 +76,8 @@ const SUPPORTED_HEADERS =
|
|
|
76
76
|
* any of those — in which case the generic "supported headers" list in the
|
|
77
77
|
* thrown error is the best we can do.
|
|
78
78
|
*
|
|
79
|
-
* This only recognizes the
|
|
80
|
-
* (sequenceDiagram/classDiagram/erDiagram/xychart-beta/stateDiagram-v2);
|
|
79
|
+
* This only recognizes the *other* headers
|
|
80
|
+
* (sequenceDiagram/classDiagram/erDiagram/xychart-beta/stateDiagram-v2/pie);
|
|
81
81
|
* "graph"/"flowchart" typos are handled by the direction-specific message
|
|
82
82
|
* below instead, since those already matched the diagram-type keyword and
|
|
83
83
|
* just have a bad or missing direction token.
|
|
@@ -89,6 +89,8 @@ function suggestedHeaderFor(header: string): string | undefined {
|
|
|
89
89
|
if (/^er/.test(lower)) return 'erDiagram'
|
|
90
90
|
if (/^xychart/.test(lower)) return 'xychart-beta'
|
|
91
91
|
if (/^state/.test(lower)) return 'stateDiagram-v2'
|
|
92
|
+
// Mermaid's `pie` keyword is case-sensitive, so `PieChart` lands here.
|
|
93
|
+
if (/^pie/.test(lower)) return 'pie'
|
|
92
94
|
return undefined
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -691,7 +693,8 @@ function ensureStateNode(
|
|
|
691
693
|
*
|
|
692
694
|
* Optional label: -->|label text|
|
|
693
695
|
*/
|
|
694
|
-
const ARROW_REGEX =
|
|
696
|
+
const ARROW_REGEX =
|
|
697
|
+
/^(<|o|x)?(-{2,}|={2,}|-\.+-|~{3,})(>|o|x)?(?:\s*\|([^|]*)\|)?/
|
|
695
698
|
|
|
696
699
|
/**
|
|
697
700
|
* Link bodies that are only a link when a start or end marker accompanies
|
package/src/index.ts
CHANGED
|
@@ -54,5 +54,8 @@ export * from './xychart/parser.ts'
|
|
|
54
54
|
export * from './xychart/types.ts'
|
|
55
55
|
export * from './xychart/colors.ts'
|
|
56
56
|
|
|
57
|
+
export * from './pie/parser.ts'
|
|
58
|
+
export * from './pie/types.ts'
|
|
59
|
+
|
|
57
60
|
export * from './expanded-shapes.ts'
|
|
58
61
|
export * from './flowchart-parser.ts'
|