@jarenjs/mermaid 0.34.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.
Files changed (64) hide show
  1. package/README.md +282 -0
  2. package/dist/types/ast.d.ts +210 -0
  3. package/dist/types/component/index.d.ts +81 -0
  4. package/dist/types/errors.d.ts +27 -0
  5. package/dist/types/index.d.ts +97 -0
  6. package/dist/types/interactive.d.ts +36 -0
  7. package/dist/types/layout/flowchart.d.ts +12 -0
  8. package/dist/types/layout/sequence.d.ts +12 -0
  9. package/dist/types/layout/state.d.ts +19 -0
  10. package/dist/types/parser/class.d.ts +11 -0
  11. package/dist/types/parser/config.d.ts +61 -0
  12. package/dist/types/parser/er.d.ts +11 -0
  13. package/dist/types/parser/flowchart.d.ts +26 -0
  14. package/dist/types/parser/gantt.d.ts +11 -0
  15. package/dist/types/parser/index.d.ts +22 -0
  16. package/dist/types/parser/pie.d.ts +11 -0
  17. package/dist/types/parser/sequence.d.ts +16 -0
  18. package/dist/types/parser/state.d.ts +22 -0
  19. package/dist/types/plugin.d.ts +50 -0
  20. package/dist/types/render/error.d.ts +23 -0
  21. package/dist/types/render/flowchart.d.ts +18 -0
  22. package/dist/types/render/index.d.ts +23 -0
  23. package/dist/types/render/misc.d.ts +54 -0
  24. package/dist/types/render/sequence.d.ts +16 -0
  25. package/dist/types/styles.d.ts +81 -0
  26. package/dist/types/theme.d.ts +47 -0
  27. package/dist/types/to-mermaid.d.ts +20 -0
  28. package/dist/types/utils.d.ts +47 -0
  29. package/docs/MERMAID-FORMAT.md +242 -0
  30. package/package.json +84 -0
  31. package/schemas/jaren-mermaid-ast.schema.json +78 -0
  32. package/schemas/jaren-workflow.schema.json +28 -0
  33. package/src/ast.js +252 -0
  34. package/src/component/index.js +109 -0
  35. package/src/errors.js +35 -0
  36. package/src/index.js +155 -0
  37. package/src/interactive.js +244 -0
  38. package/src/layout/flowchart.js +352 -0
  39. package/src/layout/sequence.js +178 -0
  40. package/src/layout/state.js +65 -0
  41. package/src/parser/class.js +90 -0
  42. package/src/parser/config.js +215 -0
  43. package/src/parser/er.js +86 -0
  44. package/src/parser/flowchart.js +413 -0
  45. package/src/parser/gantt.js +49 -0
  46. package/src/parser/index.js +122 -0
  47. package/src/parser/pie.js +32 -0
  48. package/src/parser/sequence.js +156 -0
  49. package/src/parser/state.js +137 -0
  50. package/src/plugin.js +76 -0
  51. package/src/render/error.js +55 -0
  52. package/src/render/flowchart.js +249 -0
  53. package/src/render/index.js +93 -0
  54. package/src/render/misc.js +135 -0
  55. package/src/render/sequence.js +152 -0
  56. package/src/styles.js +181 -0
  57. package/src/theme.js +180 -0
  58. package/src/to-mermaid.js +317 -0
  59. package/src/utils.js +64 -0
  60. package/styles/mermaid.css +115 -0
  61. package/stylesheets/dag-to-flowchart.jslt.json +62 -0
  62. package/stylesheets/flowchart-to-dag.jslt.json +29 -0
  63. package/stylesheets/state-to-workflow.jslt.json +26 -0
  64. package/stylesheets/workflow-to-state.jslt.json +41 -0
@@ -0,0 +1,413 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Flowchart grammar → flowchart AST. Flowchart and sequence are
4
+ * the two fully-modeled diagram types; this is the first.
5
+ *
6
+ * Char-code recursive descent over one statement at a time. Every
7
+ * pattern regex is a module constant used with the sticky (`y`) flag so
8
+ * there is no per-call `RegExp` allocation; the vertex/edge chain is a
9
+ * hand-written scan. Produces the geometry-free flowchart AST from
10
+ * `ast.js` — nodes, edges, subgraphs, classDef/class/style — preserving
11
+ * shape, label and declaration order so `to-mermaid.js` is a fixed
12
+ * point.
13
+ */
14
+
15
+ import {
16
+ flowNode, flowEdge, flowSubgraph, flowClassDef, flowClass, flowStyle, flowchartAst,
17
+ } from '../ast.js';
18
+ import { fail } from '../errors.js';
19
+ import { countCharCode } from '@jarenjs/core/string';
20
+
21
+ /**
22
+ * Shape opener sequences, longest first, each with the closer sequences
23
+ * that terminate it and the resulting shape name.
24
+ * @type {[string, [string, string][]][]}
25
+ */
26
+ const SHAPE_OPENERS = [
27
+ ['(((', [[')))', 'doublecircle']]],
28
+ ['[[', [[']]', 'subroutine']]],
29
+ ['[(', [[')]', 'cylinder']]],
30
+ ['[/', [['/]', 'parallelogram'], ['\\]', 'trapezoid']]],
31
+ ['[\\', [['\\]', 'parallelogram_alt'], ['/]', 'trapezoid_alt']]],
32
+ ['([', [['])', 'stadium']]],
33
+ ['((', [['))', 'circle']]],
34
+ ['{{', [['}}', 'hexagon']]],
35
+ ['[', [[']', 'rect']]],
36
+ ['(', [[')', 'round']]],
37
+ ['{', [['}', 'diamond']]],
38
+ ['>', [[']', 'asymmetric']]],
39
+ ];
40
+
41
+ /** Sticky: an edge with an inline ` text ` label between two link runs. */
42
+ const RE_EDGE_LABELED = /(<?)([-.=]{2,})\s+(.+?)\s+([-.=]{2,})([>ox]?)/y;
43
+ /** Sticky: a plain link operator. */
44
+ const RE_EDGE_PLAIN = /(<?)([-.=]{2,})([>ox]?)/y;
45
+ /** Sticky: an identifier (vertex id, class name, …). Ids are
46
+ * alphanumeric/underscore (plus Unicode letters); `-`/`.` are excluded
47
+ * so they cannot swallow a following link operator. */
48
+ const RE_ID = /[A-Za-z0-9_À-￿][\wÀ-￿]*/y;
49
+
50
+ /**
51
+ * Parse a flowchart body (config already stripped) into the flowchart
52
+ * AST. `keyword`/`firstLine` give the header; `direction` overrides.
53
+ * @param {string[]} lines body lines
54
+ * @param {number} lineOffset absolute line number of `lines[0]`
55
+ * @param {string} direction detected direction
56
+ * @returns {object}
57
+ */
58
+ export function parseFlowchart(lines, lineOffset, direction) {
59
+ /** @type {Map<string, object>} */
60
+ const nodeMap = new Map();
61
+ const nodeOrder = [];
62
+ const edges = [];
63
+ const subgraphs = [];
64
+ const classDefs = [];
65
+ const classes = [];
66
+ const styles = [];
67
+ /** @type {{ id: string, label: string, direction: string|null, nodes: string[] }[]} */
68
+ const subgraphStack = [];
69
+
70
+ const ensureNode = (id, label, shape) => {
71
+ let node = nodeMap.get(id);
72
+ if (node === undefined) {
73
+ node = flowNode(id, label ?? id, shape ?? 'rect');
74
+ nodeMap.set(id, node);
75
+ nodeOrder.push(id);
76
+ }
77
+ else if (label !== null && label !== undefined) {
78
+ node.label = label;
79
+ node.shape = shape ?? node.shape;
80
+ }
81
+ if (subgraphStack.length > 0) {
82
+ const group = subgraphStack[subgraphStack.length - 1];
83
+ if (!group.nodes.includes(id)) group.nodes.push(id);
84
+ }
85
+ return node;
86
+ };
87
+
88
+ for (let li = 0; li < lines.length; li++) {
89
+ const line = lines[li].trim();
90
+ if (line === '') continue;
91
+ const lineNo = lineOffset + li + 1;
92
+
93
+ // subgraph … end
94
+ if (line === 'end') {
95
+ const group = subgraphStack.pop();
96
+ if (group === undefined) fail("unexpected 'end' with no open subgraph", lineNo);
97
+ // A finished subgraph goes to the list only when it is top-level;
98
+ // nested subgraphs still record membership via their id.
99
+ subgraphs.push(flowSubgraph(group.id, group.label, group.direction, group.nodes));
100
+ continue;
101
+ }
102
+ if (line.startsWith('subgraph')) {
103
+ const rest = line.slice('subgraph'.length).trim();
104
+ const sg = parseSubgraphHeader(rest);
105
+ subgraphStack.push({ id: sg.id, label: sg.label, direction: null, nodes: [] });
106
+ continue;
107
+ }
108
+ if (line.startsWith('direction ')) {
109
+ const dir = line.slice('direction '.length).trim();
110
+ if (subgraphStack.length > 0) subgraphStack[subgraphStack.length - 1].direction = dir;
111
+ continue;
112
+ }
113
+ if (line.startsWith('classDef ')) {
114
+ const rest = line.slice('classDef '.length).trim();
115
+ const sp = rest.indexOf(' ');
116
+ if (sp === -1) { classDefs.push(flowClassDef(rest, '')); continue; }
117
+ classDefs.push(flowClassDef(rest.slice(0, sp).trim(), rest.slice(sp + 1).trim()));
118
+ continue;
119
+ }
120
+ if (line.startsWith('class ')) {
121
+ const rest = line.slice('class '.length).trim();
122
+ const sp = rest.lastIndexOf(' ');
123
+ if (sp === -1) continue;
124
+ const nodeList = rest.slice(0, sp).split(',').map((s) => s.trim()).filter(Boolean);
125
+ const name = rest.slice(sp + 1).trim();
126
+ for (const n of nodeList) classes.push(flowClass(n, name));
127
+ continue;
128
+ }
129
+ if (line.startsWith('style ')) {
130
+ const rest = line.slice('style '.length).trim();
131
+ const sp = rest.indexOf(' ');
132
+ if (sp === -1) continue;
133
+ styles.push(flowStyle(rest.slice(0, sp).trim(), rest.slice(sp + 1).trim()));
134
+ continue;
135
+ }
136
+ if (line.startsWith('click ') || line.startsWith('linkStyle ')) {
137
+ continue; // parse-accept, not modeled in v1
138
+ }
139
+
140
+ // A node/edge chain: split on ';' first (multiple statements per line).
141
+ for (const stmt of splitStatements(line)) {
142
+ parseChain(stmt, lineNo, ensureNode, edges, classes);
143
+ }
144
+ }
145
+
146
+ // Close any dangling subgraphs (lenient).
147
+ while (subgraphStack.length > 0) {
148
+ const group = subgraphStack.pop();
149
+ subgraphs.push(flowSubgraph(group.id, group.label, group.direction, group.nodes));
150
+ }
151
+
152
+ const nodes = nodeOrder.map((id) => nodeMap.get(id));
153
+ return flowchartAst(direction, nodes, edges, subgraphs, classDefs, classes, styles);
154
+ }
155
+
156
+ /**
157
+ * Split a line on top-level `;` (rarely used, but valid).
158
+ *
159
+ * Only a `;` OUTSIDE a quoted label separates statements. A label is text —
160
+ * `"fetch is injected; streaming via SSE"` is one node, not two statements —
161
+ * and splitting inside it produced an unterminated-shape error on a document
162
+ * that is perfectly good Mermaid.
163
+ * @param {string} line
164
+ * @returns {string[]}
165
+ */
166
+ function splitStatements(line) {
167
+ if (line.indexOf(';') === -1) return [line];
168
+ const out = [];
169
+ let start = 0;
170
+ let quote = '';
171
+ for (let i = 0; i < line.length; i++) {
172
+ const ch = line[i];
173
+ if (quote !== '') {
174
+ if (ch === quote) quote = '';
175
+ continue;
176
+ }
177
+ if (ch === '"' || ch === '\'') { quote = ch; continue; }
178
+ if (ch !== ';') continue;
179
+ const part = line.slice(start, i).trim();
180
+ if (part !== '') out.push(part);
181
+ start = i + 1;
182
+ }
183
+ const tail = line.slice(start).trim();
184
+ if (tail !== '') out.push(tail);
185
+ return out;
186
+ }
187
+
188
+ /**
189
+ * Parse `subgraph` header: `id`, `id[title]`, `"title"`, or `id [title]`.
190
+ * @param {string} rest
191
+ * @returns {{ id: string, label: string }}
192
+ */
193
+ function parseSubgraphHeader(rest) {
194
+ if (rest === '') return { id: 'sub', label: '' };
195
+ // id[title] form
196
+ const br = rest.indexOf('[');
197
+ if (br !== -1 && rest.endsWith(']')) {
198
+ const id = rest.slice(0, br).trim();
199
+ const label = stripQuotes(rest.slice(br + 1, -1).trim());
200
+ return { id: id || label, label };
201
+ }
202
+ if (rest.startsWith('"') && rest.endsWith('"')) {
203
+ const label = stripQuotes(rest.slice(1, -1));
204
+ return { id: label, label };
205
+ }
206
+ return { id: rest, label: rest };
207
+ }
208
+
209
+ /**
210
+ * Parse one vertex/edge chain: `A[x] --> B & C -->|l| D`.
211
+ * @param {string} stmt
212
+ * @param {number} lineNo
213
+ * @param {(id: string, label: string|null, shape: string|null) => object} ensureNode
214
+ * @param {object[]} edges
215
+ * @param {object[]} classes
216
+ */
217
+ function parseChain(stmt, lineNo, ensureNode, edges, classes) {
218
+ const s = stmt;
219
+ let pos = skipSpace(s, 0);
220
+ let prevGroup = readVertexGroup(s, pos, lineNo, ensureNode, classes);
221
+ if (prevGroup === null) return; // empty
222
+ pos = prevGroup.pos;
223
+
224
+ // Standalone vertex (no edge): already ensured; done.
225
+ for (;;) {
226
+ pos = skipSpace(s, pos);
227
+ if (pos >= s.length) break;
228
+ const edge = readEdge(s, pos);
229
+ if (edge === null) {
230
+ // Unrecognized trailing content — be lenient, stop.
231
+ break;
232
+ }
233
+ pos = edge.pos;
234
+ pos = skipSpace(s, pos);
235
+ const nextGroup = readVertexGroup(s, pos, lineNo, ensureNode, classes);
236
+ if (nextGroup === null) fail('expected a node after a link', lineNo);
237
+ pos = nextGroup.pos;
238
+ for (const from of prevGroup.ids) {
239
+ for (const to of nextGroup.ids) {
240
+ edges.push(flowEdge(from, to, edge.stroke, edge.head, edge.tail, edge.length, edge.label));
241
+ }
242
+ }
243
+ prevGroup = nextGroup;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Read a `&`-separated vertex group, ensuring each node.
249
+ * @returns {{ ids: string[], pos: number } | null}
250
+ */
251
+ function readVertexGroup(s, pos, lineNo, ensureNode, classes) {
252
+ const ids = [];
253
+ for (;;) {
254
+ pos = skipSpace(s, pos);
255
+ const v = readVertex(s, pos, lineNo);
256
+ if (v === null) return ids.length === 0 ? null : { ids, pos };
257
+ ensureNode(v.id, v.label, v.shape);
258
+ if (v.className !== null) classes.push(flowClass(v.id, v.className));
259
+ ids.push(v.id);
260
+ pos = skipSpace(s, v.pos);
261
+ if (s.charCodeAt(pos) === 0x26 /* & */) { pos++; continue; }
262
+ return { ids, pos };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Read one vertex: id + optional shape/label + optional `:::class`.
268
+ * @returns {{ id: string, label: string|null, shape: string|null, className: string|null, pos: number } | null}
269
+ */
270
+ function readVertex(s, pos, lineNo) {
271
+ RE_ID.lastIndex = pos;
272
+ const m = RE_ID.exec(s);
273
+ if (m === null || m.index !== pos) return null;
274
+ const id = m[0];
275
+ let p = RE_ID.lastIndex;
276
+
277
+ let label = null;
278
+ let shape = null;
279
+ // Shape wrapper directly after the id (no space).
280
+ const opened = matchOpener(s, p);
281
+ if (opened !== null) {
282
+ const closed = scanToCloser(s, opened.contentStart, opened.closers);
283
+ if (closed === null) fail(`unterminated '${opened.opener}' shape for node '${id}'`, lineNo);
284
+ label = stripQuotes(s.slice(opened.contentStart, closed.at).trim());
285
+ shape = closed.shape;
286
+ p = closed.end;
287
+ }
288
+
289
+ // Inline class: A:::name
290
+ let className = null;
291
+ if (s.charCodeAt(p) === 0x3a && s.charCodeAt(p + 1) === 0x3a && s.charCodeAt(p + 2) === 0x3a) {
292
+ RE_ID.lastIndex = p + 3;
293
+ const cm = RE_ID.exec(s);
294
+ if (cm !== null && cm.index === p + 3) {
295
+ className = cm[0];
296
+ p = RE_ID.lastIndex;
297
+ }
298
+ }
299
+ return { id, label, shape, className, pos: p };
300
+ }
301
+
302
+ /**
303
+ * Match a shape opener at `pos` (longest first).
304
+ * @returns {{ opener: string, contentStart: number, closers: [string, string][] } | null}
305
+ */
306
+ function matchOpener(s, pos) {
307
+ for (let i = 0; i < SHAPE_OPENERS.length; i++) {
308
+ const [opener, closers] = SHAPE_OPENERS[i];
309
+ if (s.startsWith(opener, pos)) {
310
+ return { opener, contentStart: pos + opener.length, closers };
311
+ }
312
+ }
313
+ return null;
314
+ }
315
+
316
+ /**
317
+ * Scan for the earliest closer sequence from `from`.
318
+ * @param {string} s
319
+ * @param {number} from
320
+ * @param {[string, string][]} closers
321
+ * @returns {{ at: number, end: number, shape: string } | null}
322
+ */
323
+ function scanToCloser(s, from, closers) {
324
+ for (let i = from; i < s.length; i++) {
325
+ for (let c = 0; c < closers.length; c++) {
326
+ const [seq, shape] = closers[c];
327
+ if (s.startsWith(seq, i)) return { at: i, end: i + seq.length, shape };
328
+ }
329
+ }
330
+ return null;
331
+ }
332
+
333
+ /**
334
+ * Read a link operator, plain or labeled.
335
+ * @returns {{ stroke: string, head: string, tail: string, length: number, label: string|null, pos: number } | null}
336
+ */
337
+ function readEdge(s, pos) {
338
+ RE_EDGE_LABELED.lastIndex = pos;
339
+ let m = RE_EDGE_LABELED.exec(s);
340
+ let label = null;
341
+ if (m !== null && m.index === pos) {
342
+ label = m[3].trim();
343
+ const info = classifyLink(m[1], m[4], m[5]);
344
+ return { ...info, label, pos: RE_EDGE_LABELED.lastIndex };
345
+ }
346
+ RE_EDGE_PLAIN.lastIndex = pos;
347
+ m = RE_EDGE_PLAIN.exec(s);
348
+ if (m === null || m.index !== pos) return null;
349
+ const info = classifyLink(m[1], m[2], m[3]);
350
+ let p = RE_EDGE_PLAIN.lastIndex;
351
+ // Optional |label|
352
+ if (s.charCodeAt(p) === 0x7c /* | */) {
353
+ const close = s.indexOf('|', p + 1);
354
+ if (close !== -1) {
355
+ label = stripQuotes(s.slice(p + 1, close).trim());
356
+ p = close + 1;
357
+ }
358
+ }
359
+ return { ...info, label, pos: p };
360
+ }
361
+
362
+ /**
363
+ * Classify a link body into stroke/head/tail/length.
364
+ * @param {string} tailMark leading `<` or ''
365
+ * @param {string} body run of `-`/`.`/`=`
366
+ * @param {string} headMark trailing `>`/`o`/`x` or ''
367
+ */
368
+ function classifyLink(tailMark, body, headMark) {
369
+ const stroke = body.indexOf('=') !== -1 ? 'thick'
370
+ : body.indexOf('.') !== -1 ? 'dotted' : 'solid';
371
+ const head = headMark === '>' ? 'arrow' : headMark === 'o' ? 'circle'
372
+ : headMark === 'x' ? 'cross' : 'none';
373
+ const tail = tailMark === '<' ? 'arrow' : 'none';
374
+ let length;
375
+ if (stroke === 'thick') length = countCharCode(body, 0x3d);
376
+ else if (stroke === 'dotted') length = countCharCode(body, 0x2e);
377
+ else length = countCharCode(body, 0x2d);
378
+ if (length < 1) length = 1;
379
+ return { stroke, head, tail, length };
380
+ }
381
+
382
+ /**
383
+ * @param {string} s @param {number} pos @returns {number}
384
+ */
385
+ function skipSpace(s, pos) {
386
+ while (pos < s.length) {
387
+ const c = s.charCodeAt(pos);
388
+ if (c !== 0x20 && c !== 0x09) break;
389
+ pos++;
390
+ }
391
+ return pos;
392
+ }
393
+
394
+ /**
395
+ * Strip surrounding matching quotes from a label.
396
+ * @param {string} s @returns {string}
397
+ */
398
+ export function stripQuotes(s) {
399
+ let out = s;
400
+ if (out.length >= 2) {
401
+ const a = out.charCodeAt(0);
402
+ const b = out.charCodeAt(out.length - 1);
403
+ if ((a === 0x22 && b === 0x22) || (a === 0x27 && b === 0x27)) out = out.slice(1, -1);
404
+ }
405
+ // `<br/>` is Mermaid's line break inside a label. Normalizing it to a real
406
+ // newline here — the one place every label passes through — is what lets
407
+ // both the text measurement and the renderer treat it as one, instead of
408
+ // printing the tag and sizing the box for a single long line.
409
+ return RE_BR.test(out) ? out.replace(RE_BR, '\n') : out;
410
+ }
411
+
412
+ /** `<br>`, `<br/>`, `<br />` — the spellings Mermaid accepts. */
413
+ const RE_BR = /<br\s*\/?>/gi;
@@ -0,0 +1,49 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Gantt grammar → gantt AST: a schedule DAG. Header
4
+ * directives (`title`, `dateFormat`, `axisFormat`, `excludes`) go to
5
+ * `meta`; `section` groups tasks; task rows keep their raw metadata
6
+ * string (`:done, id, 2014-01-06, 3d`) verbatim, geometry-free.
7
+ */
8
+
9
+ const HEADER_KEYS = new Set(['title', 'dateFormat', 'axisFormat', 'excludes', 'todayMarker', 'tickInterval', 'weekday']);
10
+
11
+ /**
12
+ * @param {string[]} lines
13
+ * @returns {object}
14
+ */
15
+ export function parseGantt(lines) {
16
+ const meta = {};
17
+ const sections = [];
18
+ let current = { name: null, tasks: [] };
19
+ sections.push(current);
20
+
21
+ for (let li = 0; li < lines.length; li++) {
22
+ const line = lines[li].trim();
23
+ if (line === '' || line.startsWith('%%')) continue;
24
+ const sp = line.indexOf(' ');
25
+ const key = sp === -1 ? line : line.slice(0, sp);
26
+
27
+ if (HEADER_KEYS.has(key)) {
28
+ meta[key] = sp === -1 ? '' : line.slice(sp + 1).trim();
29
+ continue;
30
+ }
31
+ if (key === 'section') {
32
+ current = { name: line.slice('section'.length).trim(), tasks: [] };
33
+ sections.push(current);
34
+ continue;
35
+ }
36
+ // Task row: `Name : meta`
37
+ const colon = line.indexOf(':');
38
+ if (colon !== -1) {
39
+ const name = line.slice(0, colon).trim();
40
+ const info = line.slice(colon + 1).trim();
41
+ current.tasks.push({ name, info });
42
+ }
43
+ }
44
+
45
+ // Drop a leading empty default section if unused.
46
+ const trimmed = sections[0].name === null && sections[0].tasks.length === 0
47
+ ? sections.slice(1) : sections;
48
+ return { meta, sections: trimmed };
49
+ }
@@ -0,0 +1,122 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `parseMermaid`: detect the diagram type + config on the first
4
+ * non-config line, dispatch to the type parser, and assemble the shared
5
+ * `DiagramDocument` envelope.
6
+ *
7
+ * Flowchart and sequence are fully modeled. The remaining
8
+ * first-class types (class, ER, state, gantt, pie) plug in through
9
+ * `TYPE_PARSERS`; secondary types (mindmap, gitGraph, journey, timeline,
10
+ * quadrantChart) parse-accept into a geometry-free `rawAst` and are
11
+ * counted honestly in the coverage scorecard.
12
+ */
13
+
14
+ import { diagramDocument, rawAst } from '../ast.js';
15
+ import { hashContent, toLines, firstToken } from '../utils.js';
16
+ import { parseMermaidConfig } from './config.js';
17
+ import { parseFlowchart } from './flowchart.js';
18
+ import { parseSequence } from './sequence.js';
19
+ import { parseClass } from './class.js';
20
+ import { parseState } from './state.js';
21
+ import { parseEr } from './er.js';
22
+ import { parseGantt } from './gantt.js';
23
+ import { parsePie } from './pie.js';
24
+ import { fail } from '../errors.js';
25
+
26
+ /** Keyword → canonical diagram type. */
27
+ const KEYWORDS = {
28
+ flowchart: 'flowchart',
29
+ graph: 'flowchart',
30
+ sequenceDiagram: 'sequence',
31
+ classDiagram: 'class',
32
+ 'classDiagram-v2': 'class',
33
+ stateDiagram: 'state',
34
+ 'stateDiagram-v2': 'state',
35
+ erDiagram: 'er',
36
+ gantt: 'gantt',
37
+ pie: 'pie',
38
+ mindmap: 'mindmap',
39
+ gitGraph: 'gitGraph',
40
+ journey: 'journey',
41
+ timeline: 'timeline',
42
+ quadrantChart: 'quadrantChart',
43
+ requirementDiagram: 'requirement',
44
+ };
45
+
46
+ /** Fully/partly modeled type parsers. */
47
+ const TYPE_PARSERS = {
48
+ class: parseClass,
49
+ state: parseState,
50
+ er: parseEr,
51
+ gantt: parseGantt,
52
+ pie: parsePie,
53
+ };
54
+
55
+ /** Types that parse-accept into a placeholder in the coverage scorecard. */
56
+ export const SECONDARY_TYPES = new Set([
57
+ 'mindmap', 'gitGraph', 'journey', 'timeline', 'quadrantChart', 'requirement',
58
+ ]);
59
+
60
+ /**
61
+ * Parse Mermaid source into a `DiagramDocument`.
62
+ * @param {string} source
63
+ * @param {{ parseFrontmatter?: (text: string) => any }} [options]
64
+ * @returns {import('../ast.js').DiagramDocument}
65
+ */
66
+ export function parseMermaid(source, options = {}) {
67
+ const { config, title, body } = parseMermaidConfig(source, options);
68
+ const lines = toLines(body);
69
+
70
+ // First non-blank line carries the diagram keyword.
71
+ let start = 0;
72
+ while (start < lines.length && lines[start].trim() === '') start++;
73
+ if (start >= lines.length) fail('empty diagram', 1);
74
+
75
+ const header = lines[start].trim();
76
+ const keyword = firstToken(header);
77
+ const type = KEYWORDS[keyword];
78
+ if (type === undefined) {
79
+ fail(`unknown diagram type '${keyword}'`, start + 1);
80
+ }
81
+
82
+ let direction = null;
83
+ let ast;
84
+ const bodyLines = lines.slice(start + 1);
85
+ const bodyOffset = start + 1;
86
+
87
+ if (type === 'flowchart') {
88
+ direction = detectDirection(header, keyword);
89
+ ast = parseFlowchart(bodyLines, bodyOffset, direction);
90
+ }
91
+ else if (type === 'sequence') {
92
+ ast = parseSequence(bodyLines, bodyOffset);
93
+ }
94
+ else if (TYPE_PARSERS[type] !== undefined) {
95
+ ast = TYPE_PARSERS[type](bodyLines, bodyOffset, header);
96
+ }
97
+ else {
98
+ // Secondary: preserve the raw body so `toMermaid` round-trips and
99
+ // the scorecard is honest.
100
+ ast = rawAst(type, bodyLines.filter((l) => l.trim() !== ''));
101
+ }
102
+
103
+ return diagramDocument(type, config, ast, {
104
+ hash: hashContent(source),
105
+ direction,
106
+ title,
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Detect a flowchart direction from the header (`flowchart TD`).
112
+ * @param {string} header
113
+ * @param {string} keyword
114
+ * @returns {string}
115
+ */
116
+ function detectDirection(header, keyword) {
117
+ const rest = header.slice(keyword.length).trim();
118
+ // `graph LR` or `flowchart TD`; a trailing `:` occasionally appears.
119
+ const dir = rest.replace(/:$/, '').trim().toUpperCase();
120
+ const known = new Set(['TB', 'TD', 'BT', 'LR', 'RL']);
121
+ return known.has(dir) ? (dir === 'TD' ? 'TD' : dir) : 'TB';
122
+ }
@@ -0,0 +1,32 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Pie grammar → pie AST. `pie [showData]` then `"label" : value`
4
+ * rows, with an optional `title …`.
5
+ */
6
+
7
+ import { stripQuotes } from './flowchart.js';
8
+
9
+ /** `"label" : 42` (label may be unquoted). */
10
+ const RE_SLICE = /^(".*?"|[^:]+?)\s*:\s*([0-9]*\.?[0-9]+)\s*$/;
11
+
12
+ /**
13
+ * @param {string[]} lines
14
+ * @param {number} lineOffset
15
+ * @param {string} header
16
+ * @returns {object}
17
+ */
18
+ export function parsePie(lines, lineOffset, header) {
19
+ const showData = /\bshowData\b/.test(header);
20
+ let title = null;
21
+ const slices = [];
22
+ for (let li = 0; li < lines.length; li++) {
23
+ const line = lines[li].trim();
24
+ if (line === '') continue;
25
+ if (line.startsWith('title ')) { title = line.slice('title '.length).trim(); continue; }
26
+ const m = RE_SLICE.exec(line);
27
+ if (m !== null) {
28
+ slices.push({ label: stripQuotes(m[1].trim()), value: Number(m[2]) });
29
+ }
30
+ }
31
+ return { title, showData, slices };
32
+ }