@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
package/src/theme.js ADDED
@@ -0,0 +1,180 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Theme tokens. `createTheme` resolves a
4
+ * theme name (or overrides) into a flat token object of concrete colors
5
+ * *and* a matching set of `--mm-*` CSS custom properties.
6
+ *
7
+ * Both are used at once, deliberately: the render pass writes the
8
+ * concrete colors as SVG presentation attributes (so `toSvgString()` is
9
+ * a valid, self-colored standalone SVG with no CSS), and also stamps the
10
+ * `--mm-*` variables inline on the root `<svg>` plus a `class` on every
11
+ * shape, which `styles/mermaid.css` maps back to `var(--mm-*)`. Because
12
+ * the stamp is an inline style it beats every stylesheet rule — so the
13
+ * stamp itself is the re-theming hook: the `'host'` theme stamps each
14
+ * linked variable as `var(--<host-token>, <concrete>)` (see `HOST_VARS`),
15
+ * making diagrams follow a host's light/dark tokens live, with no
16
+ * re-render — memoized vnodes stay valid across a theme flip.
17
+ *
18
+ * Only the token tables live here; the resolution mechanics are shared
19
+ * (`@jarenjs/view/helpers` `resolveTheme`).
20
+ *
21
+ * The error-box tokens are `err*`, not `error*`, and must stay that way:
22
+ * every root stamps every token, `mm-error` is the class marking a root as
23
+ * an error box, and a `--mm-error-*` stamp would put that marker on every
24
+ * healthy diagram — so anything testing the marker by substring would read
25
+ * a working render as a failure.
26
+ */
27
+
28
+ import { resolveTheme } from '@jarenjs/view/helpers';
29
+
30
+ /** @type {Record<string, Record<string, string>>} */
31
+ const THEMES = {
32
+ default: {
33
+ background: 'transparent',
34
+ nodeFill: '#dbeafe',
35
+ nodeStroke: '#2563eb',
36
+ nodeText: '#1f2020',
37
+ lineColor: '#333333',
38
+ edgeLabelText: '#333333',
39
+ edgeLabelBg: '#ffffff',
40
+ clusterFill: '#f1f5f9',
41
+ clusterStroke: '#94a3b8',
42
+ actorFill: '#dbeafe',
43
+ actorStroke: '#2563eb',
44
+ actorText: '#1f2020',
45
+ lifeline: '#999999',
46
+ activationFill: '#f4f4f4',
47
+ activationStroke: '#666666',
48
+ noteFill: '#fef3c7',
49
+ noteStroke: '#d97706',
50
+ noteText: '#1f2020',
51
+ errFill: '#fdf2f2',
52
+ errStroke: '#e74c3c',
53
+ errTitle: '#c0392b',
54
+ errText: '#7b241c',
55
+ errSource: '#555555',
56
+ fontFamily: '"trebuchet ms", verdana, arial, sans-serif',
57
+ },
58
+ dark: {
59
+ background: 'transparent',
60
+ nodeFill: '#1f2020',
61
+ nodeStroke: '#81B1DB',
62
+ nodeText: '#f4f4f4',
63
+ lineColor: '#cccccc',
64
+ edgeLabelText: '#e0e0e0',
65
+ edgeLabelBg: '#1f2020',
66
+ clusterFill: '#2b2b3a',
67
+ clusterStroke: '#6f6f9e',
68
+ actorFill: '#252526',
69
+ actorStroke: '#81B1DB',
70
+ actorText: '#f4f4f4',
71
+ lifeline: '#8a8a8a',
72
+ activationFill: '#31313a',
73
+ activationStroke: '#8a8a8a',
74
+ noteFill: '#3b3b26',
75
+ noteStroke: '#aaaa33',
76
+ noteText: '#f4f4f4',
77
+ errFill: '#2b1b1b',
78
+ errStroke: '#e74c3c',
79
+ errTitle: '#f5827a',
80
+ errText: '#f0c9c5',
81
+ errSource: '#8a8a8a',
82
+ fontFamily: '"trebuchet ms", verdana, arial, sans-serif',
83
+ },
84
+ neutral: {
85
+ background: 'transparent',
86
+ nodeFill: '#eee',
87
+ nodeStroke: '#999',
88
+ nodeText: '#111',
89
+ lineColor: '#666',
90
+ edgeLabelText: '#333',
91
+ edgeLabelBg: '#fff',
92
+ clusterFill: '#f4f4f4',
93
+ clusterStroke: '#bbb',
94
+ actorFill: '#eee',
95
+ actorStroke: '#999',
96
+ actorText: '#111',
97
+ lifeline: '#999',
98
+ activationFill: '#f4f4f4',
99
+ activationStroke: '#666',
100
+ noteFill: '#f3f3d9',
101
+ noteStroke: '#b7b76d',
102
+ noteText: '#111',
103
+ errFill: '#fdf2f2',
104
+ errStroke: '#e74c3c',
105
+ errTitle: '#c0392b',
106
+ errText: '#7b241c',
107
+ errSource: '#555555',
108
+ fontFamily: '"trebuchet ms", verdana, arial, sans-serif',
109
+ },
110
+ forest: {
111
+ background: 'transparent',
112
+ nodeFill: '#cde498',
113
+ nodeStroke: '#13540c',
114
+ nodeText: '#13540c',
115
+ lineColor: '#13540c',
116
+ edgeLabelText: '#13540c',
117
+ edgeLabelBg: '#e8f5e0',
118
+ clusterFill: '#cdffb2',
119
+ clusterStroke: '#6eaa49',
120
+ actorFill: '#cde498',
121
+ actorStroke: '#13540c',
122
+ actorText: '#13540c',
123
+ lifeline: '#6eaa49',
124
+ activationFill: '#e8f5e0',
125
+ activationStroke: '#13540c',
126
+ noteFill: '#fff5ad',
127
+ noteStroke: '#aaaa33',
128
+ noteText: '#13540c',
129
+ errFill: '#fdf2f2',
130
+ errStroke: '#e74c3c',
131
+ errTitle: '#c0392b',
132
+ errText: '#7b241c',
133
+ errSource: '#555555',
134
+ fontFamily: '"trebuchet ms", verdana, arial, sans-serif',
135
+ },
136
+ };
137
+
138
+ /**
139
+ * Host custom-property links for the `'host'` theme: token key → the host
140
+ * token it should follow (the site token vocabulary, docs/DESIGN.md §2). The
141
+ * default theme's concrete colors remain as `var()` fallbacks, so the
142
+ * same SVG is standalone-valid outside any host.
143
+ * @type {Record<string, string>}
144
+ */
145
+ export const HOST_VARS = {
146
+ nodeFill: '--accent-soft',
147
+ nodeStroke: '--accent',
148
+ nodeText: '--fg',
149
+ lineColor: '--fg',
150
+ edgeLabelText: '--fg',
151
+ edgeLabelBg: '--bg',
152
+ clusterFill: '--surface',
153
+ clusterStroke: '--border',
154
+ actorFill: '--accent-soft',
155
+ actorStroke: '--accent',
156
+ actorText: '--fg',
157
+ lifeline: '--muted',
158
+ activationFill: '--surface',
159
+ activationStroke: '--muted',
160
+ noteFill: '--warn-soft',
161
+ noteStroke: '--warn',
162
+ noteText: '--fg',
163
+ errFill: '--fail-soft',
164
+ errStroke: '--fail',
165
+ errTitle: '--fail',
166
+ errText: '--fg',
167
+ errSource: '--muted',
168
+ };
169
+
170
+ /**
171
+ * Resolve a theme. The name `'host'` resolves the default tokens linked
172
+ * to the host token vocabulary via {@link HOST_VARS}.
173
+ * @param {string | Record<string, any>} [nameOrOverrides]
174
+ * @returns {{ name: string, tokens: Record<string, string>, cssVars: Record<string, string> }}
175
+ */
176
+ export function createTheme(nameOrOverrides = 'default') {
177
+ return resolveTheme(THEMES, 'mm', nameOrOverrides, HOST_VARS);
178
+ }
179
+
180
+ export { THEMES };
@@ -0,0 +1,317 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Canonical Mermaid printer: AST → text (MERMAID-FORMAT §5).
4
+ * The reverse arrow of `parseMermaid`, mirroring
5
+ * `@jarenjs/md`'s `to-md.js`: one specialized closure per diagram type,
6
+ * a **round-trip fixed point** — `parseMermaid(toMermaid(doc))`
7
+ * deep-equals `doc.ast` for the fully-modeled types.
8
+ *
9
+ * The printer is *canonical*, not verbatim: it does not preserve source
10
+ * whitespace or comments (the stored Markdown fence `value` gives
11
+ * verbatim round-trip while a node is untransformed). Its job is
12
+ * to re-emit an edited AST as editable Mermaid text.
13
+ */
14
+
15
+ //#region public ----------------------------------------------------
16
+
17
+ /** @type {Record<string, (ast: any) => string>} */
18
+ const PRINTERS = {
19
+ flowchart: printFlowchart,
20
+ sequence: printSequence,
21
+ class: printClass,
22
+ state: printState,
23
+ er: printEr,
24
+ gantt: printGantt,
25
+ pie: printPie,
26
+ };
27
+
28
+ /**
29
+ * Print a `DiagramDocument` (or a bare AST with a `diagram` field) to
30
+ * canonical Mermaid text. Config re-emits as a `%%{init}%%` header when
31
+ * present.
32
+ * @param {any} docOrAst
33
+ * @returns {string}
34
+ */
35
+ export function toMermaid(docOrAst) {
36
+ const doc = docOrAst && typeof docOrAst === 'object' && docOrAst.$mermaid !== undefined
37
+ ? docOrAst
38
+ : null;
39
+ const diagram = doc ? doc.diagram : docOrAst?.diagram;
40
+ const ast = doc ? doc.ast : docOrAst;
41
+ const config = doc ? doc.config : undefined;
42
+ const title = doc ? doc.meta?.title : undefined;
43
+
44
+ let head = '';
45
+ if (title != null && title !== '') {
46
+ head += '---\ntitle: ' + title + '\n---\n';
47
+ }
48
+ if (config && typeof config === 'object' && Object.keys(config).length > 0) {
49
+ head += '%%{init: ' + JSON.stringify(config) + '}%%\n';
50
+ }
51
+
52
+ const printer = PRINTERS[diagram];
53
+ if (printer !== undefined) return head + printer(ast);
54
+ // Secondary/raw diagram: keyword + preserved body lines.
55
+ if (ast && Array.isArray(ast.lines)) {
56
+ return head + ast.diagram + '\n' + ast.lines.join('\n') + '\n';
57
+ }
58
+ return head + String(diagram ?? '') + '\n';
59
+ }
60
+
61
+ //#endregion
62
+ //#region flowchart -------------------------------------------------
63
+
64
+ /** Labels containing shape-closer characters must be quoted. */
65
+ const RE_NEEDS_QUOTE = /[[\](){}<>|"]/;
66
+
67
+ /**
68
+ * @param {any} ast
69
+ * @returns {string}
70
+ */
71
+ function printFlowchart(ast) {
72
+ const out = ['flowchart ' + (ast.direction || 'TB')];
73
+
74
+ // 1. Every node declared once, in AST order — this fixes node order
75
+ // on the round trip. Default nodes emit a bare id line.
76
+ for (const node of ast.nodes) {
77
+ out.push(nodeDecl(node));
78
+ }
79
+
80
+ // 2. Subgraphs: reference members by bare id (membership only).
81
+ for (const sg of ast.subgraphs) {
82
+ out.push(sg.label && sg.label !== ''
83
+ ? `subgraph ${sg.id} [${sg.label.replace(/\n/g, '<br/>')}]`
84
+ : `subgraph ${sg.id}`);
85
+ if (sg.direction) out.push(' direction ' + sg.direction);
86
+ for (const id of sg.nodes) out.push(' ' + id);
87
+ out.push('end');
88
+ }
89
+
90
+ // 3. Edges in AST order.
91
+ for (const e of ast.edges) {
92
+ const op = edgeOp(e);
93
+ const label = e.label != null ? `|${e.label.replace(/\n/g, '<br/>')}|` : '';
94
+ out.push(`${e.from} ${op}${label} ${e.to}`);
95
+ }
96
+
97
+ // 4. classDef / class / style, each in AST order.
98
+ for (const cd of ast.classDefs) out.push(`classDef ${cd.name} ${cd.styles}`);
99
+ for (const c of ast.classes) out.push(`class ${c.node} ${c.name}`);
100
+ for (const s of ast.styles) out.push(`style ${s.node} ${s.styles}`);
101
+
102
+ return out.join('\n') + '\n';
103
+ }
104
+
105
+ /**
106
+ * @param {any} node
107
+ * @returns {string}
108
+ */
109
+ function nodeDecl(node) {
110
+ if (node.shape === 'rect' && node.label === node.id) return node.id;
111
+ return node.id + wrapShape(node.shape, node.label);
112
+ }
113
+
114
+ /**
115
+ * @param {string} shape
116
+ * @param {string} label
117
+ * @returns {string}
118
+ */
119
+ function wrapShape(shape, label) {
120
+ // The parser turns `<br/>` into a newline; printing that newline raw would
121
+ // end the statement. Emitting the tag back is what makes a transformed
122
+ // diagram round-trip to source that still parses.
123
+ const text = label.replace(/\n/g, '<br/>');
124
+ const l = RE_NEEDS_QUOTE.test(text) ? '"' + text.replace(/"/g, '') + '"' : text;
125
+ switch (shape) {
126
+ case 'round': return `(${l})`;
127
+ case 'stadium': return `([${l}])`;
128
+ case 'subroutine': return `[[${l}]]`;
129
+ case 'cylinder': return `[(${l})]`;
130
+ case 'circle': return `((${l}))`;
131
+ case 'doublecircle': return `(((${l})))`;
132
+ case 'diamond': return `{${l}}`;
133
+ case 'hexagon': return `{{${l}}}`;
134
+ case 'parallelogram': return `[/${l}/]`;
135
+ case 'parallelogram_alt': return `[\\${l}\\]`;
136
+ case 'trapezoid': return `[/${l}\\]`;
137
+ case 'trapezoid_alt': return `[\\${l}/]`;
138
+ case 'asymmetric': return `>${l}]`;
139
+ default: return `[${l}]`;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Reconstruct the canonical link operator (the exact inverse of
145
+ * `classifyLink` in the parser).
146
+ * @param {any} e
147
+ * @returns {string}
148
+ */
149
+ function edgeOp(e) {
150
+ const tail = e.tail === 'arrow' ? '<' : e.tail === 'circle' ? 'o' : e.tail === 'cross' ? 'x' : '';
151
+ const head = e.head === 'arrow' ? '>' : e.head === 'circle' ? 'o' : e.head === 'cross' ? 'x' : '';
152
+ const n = Math.max(1, e.length | 0);
153
+ let mid;
154
+ if (e.stroke === 'thick') mid = '='.repeat(n);
155
+ else if (e.stroke === 'dotted') mid = '-' + '.'.repeat(n) + '-';
156
+ else mid = '-'.repeat(Math.max(2, n));
157
+ return tail + mid + head;
158
+ }
159
+
160
+ //#endregion
161
+ //#region sequence --------------------------------------------------
162
+
163
+ /**
164
+ * @param {any} ast
165
+ * @returns {string}
166
+ */
167
+ function printSequence(ast) {
168
+ const out = ['sequenceDiagram'];
169
+ if (ast.autonumber) out.push('autonumber');
170
+ for (const p of ast.participants) {
171
+ const decl = p.kind === 'actor' ? 'actor' : 'participant';
172
+ out.push(p.label === p.id ? `${decl} ${p.id}` : `${decl} ${p.id} as ${p.label}`);
173
+ }
174
+ printSeqStatements(ast.statements, out, 0);
175
+ return out.join('\n') + '\n';
176
+ }
177
+
178
+ /**
179
+ * @param {any[]} statements
180
+ * @param {string[]} out
181
+ * @param {number} depth
182
+ */
183
+ function printSeqStatements(statements, out, depth) {
184
+ const pad = ' '.repeat(depth);
185
+ for (const stmt of statements) {
186
+ if (stmt.kind === 'message') {
187
+ const arrow = arrowToken(stmt.line, stmt.head);
188
+ const act = stmt.activation === 'activate' ? '+' : stmt.activation === 'deactivate' ? '-' : '';
189
+ out.push(`${pad}${stmt.from}${arrow}${act}${stmt.to}: ${stmt.text}`);
190
+ }
191
+ else if (stmt.kind === 'note') {
192
+ out.push(`${pad}note ${stmt.placement} ${stmt.actors.join(',')}: ${stmt.text}`);
193
+ }
194
+ else if (stmt.kind === 'activate' || stmt.kind === 'deactivate') {
195
+ out.push(`${pad}${stmt.kind} ${stmt.actor}`);
196
+ }
197
+ else if (stmt.kind === 'block') {
198
+ const branchWord = stmt.blockType === 'par' ? 'and'
199
+ : stmt.blockType === 'critical' ? 'option' : 'else';
200
+ const b0 = stmt.branches[0];
201
+ out.push(`${pad}${stmt.blockType}${b0.label ? ' ' + b0.label : ''}`);
202
+ printSeqStatements(b0.statements, out, depth + 1);
203
+ for (let i = 1; i < stmt.branches.length; i++) {
204
+ const b = stmt.branches[i];
205
+ out.push(`${pad}${branchWord}${b.label ? ' ' + b.label : ''}`);
206
+ printSeqStatements(b.statements, out, depth + 1);
207
+ }
208
+ out.push(`${pad}end`);
209
+ }
210
+ }
211
+ }
212
+
213
+ /**
214
+ * The exact inverse of `classifyArrow`.
215
+ * @param {'solid'|'dotted'} line
216
+ * @param {'arrow'|'open'|'cross'|'point'} head
217
+ * @returns {string}
218
+ */
219
+ function arrowToken(line, head) {
220
+ const dash = line === 'dotted' ? '--' : '-';
221
+ const tip = head === 'arrow' ? '>>' : head === 'cross' ? 'x' : head === 'point' ? ')' : '>';
222
+ return dash + tip;
223
+ }
224
+
225
+ //#endregion
226
+ //#region class / state / er / gantt / pie (best effort) ------------
227
+
228
+ /**
229
+ * @param {any} ast
230
+ * @returns {string}
231
+ */
232
+ function printClass(ast) {
233
+ const out = ['classDiagram'];
234
+ for (const cls of ast.classes) {
235
+ if (cls.members.length === 0) {
236
+ out.push(`class ${cls.name}`);
237
+ }
238
+ else {
239
+ out.push(`class ${cls.name} {`);
240
+ for (const m of cls.members) out.push(` ${m.visibility ?? ''}${m.text}`);
241
+ out.push('}');
242
+ }
243
+ }
244
+ for (const r of ast.relations) {
245
+ out.push(`${r.from} ${r.type} ${r.to}${r.label ? ' : ' + r.label : ''}`);
246
+ }
247
+ return out.join('\n') + '\n';
248
+ }
249
+
250
+ /**
251
+ * @param {any} ast
252
+ * @returns {string}
253
+ */
254
+ function printState(ast) {
255
+ const out = ['stateDiagram-v2'];
256
+ // an id-only state no transition mentions must still be DECLARED, or
257
+ // the canonical round trip silently drops it (`state x` is the
258
+ // parser's own spelling for exactly that)
259
+ const mentioned = new Set();
260
+ for (const t of ast.transitions) {
261
+ mentioned.add(t.from);
262
+ mentioned.add(t.to);
263
+ }
264
+ for (const s of ast.states) {
265
+ if (s.label !== s.id) out.push(`${s.id} : ${s.label}`);
266
+ else if (!mentioned.has(s.id)) out.push(`state ${s.id}`);
267
+ }
268
+ for (const t of ast.transitions) {
269
+ out.push(`${t.from} --> ${t.to}${t.label ? ' : ' + t.label : ''}`);
270
+ }
271
+ return out.join('\n') + '\n';
272
+ }
273
+
274
+ /**
275
+ * @param {any} ast
276
+ * @returns {string}
277
+ */
278
+ function printEr(ast) {
279
+ const out = ['erDiagram'];
280
+ for (const e of ast.entities) {
281
+ if (e.attributes.length === 0) { out.push(e.name); continue; }
282
+ out.push(`${e.name} {`);
283
+ for (const a of e.attributes) out.push(` ${a.type} ${a.name}${a.keys.length ? ' ' + a.keys.join(' ') : ''}`);
284
+ out.push('}');
285
+ }
286
+ for (const r of ast.relationships) {
287
+ out.push(`${r.left} ${r.leftCard}${r.identifying ? '--' : '..'}${r.rightCard} ${r.right} : ${r.label}`);
288
+ }
289
+ return out.join('\n') + '\n';
290
+ }
291
+
292
+ /**
293
+ * @param {any} ast
294
+ * @returns {string}
295
+ */
296
+ function printGantt(ast) {
297
+ const out = ['gantt'];
298
+ for (const key of Object.keys(ast.meta)) out.push(`${key} ${ast.meta[key]}`.trim());
299
+ for (const section of ast.sections) {
300
+ if (section.name) out.push(`section ${section.name}`);
301
+ for (const t of section.tasks) out.push(`${t.name} : ${t.info}`);
302
+ }
303
+ return out.join('\n') + '\n';
304
+ }
305
+
306
+ /**
307
+ * @param {any} ast
308
+ * @returns {string}
309
+ */
310
+ function printPie(ast) {
311
+ const out = ['pie' + (ast.showData ? ' showData' : '')];
312
+ if (ast.title) out.push('title ' + ast.title);
313
+ for (const s of ast.slices) out.push(`"${s.label}" : ${s.value}`);
314
+ return out.join('\n') + '\n';
315
+ }
316
+
317
+ //#endregion
package/src/utils.js ADDED
@@ -0,0 +1,64 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Small shared helpers for the mermaid engine.
4
+ *
5
+ * `hashContent` and `coord` are re-exported rather than re-implemented, so
6
+ * this package agrees with the rest of the suite by construction:
7
+ * `hashContent` is the one content fingerprint (the same source produces
8
+ * the same vnode `key`, `meta.hash` and memo key whether it flows through
9
+ * the Markdown engine or this one), and `coord` is the one SVG coordinate
10
+ * quantization, so a layout pass emits geometry at exactly the precision
11
+ * the renderer would round it to — which is what keeps the golden JSON and
12
+ * the SVG it renders to byte-stable instead of carrying float noise.
13
+ */
14
+
15
+ export { hashContent } from '@jarenjs/core/string';
16
+ export { coord } from '@jarenjs/view/helpers';
17
+
18
+ /**
19
+ * The first whitespace-delimited token of a line: everything up to the
20
+ * first space or tab (the whole line when there is neither). The
21
+ * grammars are line-oriented and keyword-led, so this is how both the
22
+ * type dispatcher and the sequence parser read a line's keyword —
23
+ * no allocation beyond the returned slice.
24
+ * @param {string} line
25
+ * @returns {string}
26
+ */
27
+ export function firstToken(line) {
28
+ let i = 0;
29
+ while (i < line.length && line.charCodeAt(i) !== 0x20 && line.charCodeAt(i) !== 0x09) i++;
30
+ return line.slice(0, i);
31
+ }
32
+
33
+ /**
34
+ * Split a source string into lines, dropping a single trailing newline
35
+ * and normalizing CRLF/CR to LF first. Comment/blank stripping is the
36
+ * caller's job (each dialect handles its own comment marker).
37
+ * @param {string} source
38
+ * @returns {string[]}
39
+ */
40
+ export function toLines(source) {
41
+ const normalized = source.replace(/\r\n?/g, '\n');
42
+ const end = normalized.endsWith('\n') ? normalized.length - 1 : normalized.length;
43
+ return normalized.slice(0, end).split('\n');
44
+ }
45
+
46
+ /**
47
+ * Accumulate a `{ ... }` body that may span multiple lines. `head` is
48
+ * the text after the opening brace on the current line; lines are
49
+ * consumed until one contains the closing brace. Returns the body text
50
+ * before the `}` and the advanced line index so the caller's loop can
51
+ * continue after the block (class and ER entity bodies share this).
52
+ * @param {string[]} lines
53
+ * @param {number} li - index of the line the `{` sits on
54
+ * @param {string} head - text after the opening brace
55
+ * @returns {{ body: string, li: number }}
56
+ */
57
+ export function collectBraceBody(lines, li, head) {
58
+ let body = head;
59
+ while (body.indexOf('}') === -1 && li + 1 < lines.length) {
60
+ li++;
61
+ body += '\n' + lines[li];
62
+ }
63
+ return { body: body.slice(0, body.indexOf('}')), li };
64
+ }
@@ -0,0 +1,115 @@
1
+ /* @jarenjs/mermaid — component stylesheet.
2
+ *
3
+ * Two things happen here, decoupled from the site's design tokens (sizes
4
+ * in `em`, like md.css):
5
+ *
6
+ * 1. It extends the unscoped `.md-mermaid` placeholder rules from
7
+ * `@jarenjs/md`'s md.css (centered block, max-width svg) so a mermaid
8
+ * fence rendered inside a Markdown article sits correctly.
9
+ * 2. It defines a self-scoped `.mermaid` class carrying the `--mm-*`
10
+ * token vocabulary (light values plus a `.dark` override) and maps
11
+ * each `mm-*` shape class to `var(--mm-*)`. The render pass stamps
12
+ * the same variables *inline* on the root `<svg>`, and inline custom
13
+ * properties beat these stylesheet blocks — so for pipeline-rendered
14
+ * SVGs the stamp decides the colors. Re-theming a live diagram goes
15
+ * through the `'host'` theme (`src/theme.js` `HOST_VARS`), whose
16
+ * stamped values reference the host's tokens. The token blocks below
17
+ * theme class-marked markup that carries no inline stamp, and are the
18
+ * reference list of the vocabulary. `toSvgString()` output stays
19
+ * standalone-valid via concrete presentation-attribute colors.
20
+ */
21
+
22
+ /* --- md-embedded diagrams ----------------------------------------- */
23
+ .md-mermaid.mermaid-block {
24
+ margin: 0.9em 0;
25
+ text-align: center;
26
+ /* A wide diagram scrolls. It must not be scaled to fit: the layout already
27
+ chose a legible font size, and shrinking the SVG scales that text down
28
+ with everything else — on a phone a 14px label lands near 11px, which is
29
+ the whole reason this rule is not `max-width: 100%`. */
30
+ overflow-x: auto;
31
+ overscroll-behavior-x: contain;
32
+ }
33
+ .md-mermaid.mermaid-block svg { max-width: none; height: auto; }
34
+ /* Once zoomed the viewBox owns the view, so the element is constrained to the
35
+ frame and panning has something to pan within. */
36
+ .md-mermaid.mermaid-block.mm-zoomed { overflow: hidden; }
37
+ .md-mermaid.mermaid-block.mm-zoomed svg { max-width: 100%; }
38
+
39
+ /* --- self-scoped diagram ------------------------------------------ */
40
+ .mermaid {
41
+ --mm-node-fill: #dbeafe;
42
+ --mm-node-stroke: #2563eb;
43
+ --mm-node-text: #1f2020;
44
+ --mm-line-color: #333333;
45
+ --mm-edge-label-text: #333333;
46
+ --mm-edge-label-bg: #ffffff;
47
+ --mm-cluster-fill: #f1f5f9;
48
+ --mm-cluster-stroke: #94a3b8;
49
+ --mm-actor-fill: #dbeafe;
50
+ --mm-actor-stroke: #2563eb;
51
+ --mm-actor-text: #1f2020;
52
+ --mm-lifeline: #999999;
53
+ --mm-activation-fill: #f4f4f4;
54
+ --mm-activation-stroke: #666666;
55
+ --mm-note-fill: #fef3c7;
56
+ --mm-note-stroke: #d97706;
57
+ --mm-note-text: #1f2020;
58
+ --mm-err-fill: #fdf2f2;
59
+ --mm-err-stroke: #e74c3c;
60
+ --mm-err-title: #c0392b;
61
+ --mm-err-text: #7b241c;
62
+ --mm-err-source: #555555;
63
+
64
+ display: block;
65
+ max-width: 100%;
66
+ height: auto;
67
+ }
68
+
69
+ /* Cascade over the render pass's presentation attributes. */
70
+ .mermaid .mm-node-shape { fill: var(--mm-node-fill); stroke: var(--mm-node-stroke); }
71
+ .mermaid .mm-label,
72
+ .mermaid .mm-node text { fill: var(--mm-node-text); }
73
+ .mermaid .mm-edge-line { stroke: var(--mm-line-color); }
74
+ .mermaid .mm-arrowhead { fill: var(--mm-line-color); stroke: var(--mm-line-color); }
75
+ .mermaid .mm-edge-label { fill: var(--mm-edge-label-text); }
76
+ .mermaid .mm-edge-label-bg { fill: var(--mm-edge-label-bg); }
77
+ .mermaid .mm-cluster-rect { fill: var(--mm-cluster-fill); stroke: var(--mm-cluster-stroke); }
78
+ .mermaid .mm-actor rect { fill: var(--mm-actor-fill); stroke: var(--mm-actor-stroke); }
79
+ .mermaid .mm-actor text { fill: var(--mm-actor-text); }
80
+ .mermaid .mm-lifeline { stroke: var(--mm-lifeline); }
81
+ .mermaid .mm-activation { fill: var(--mm-activation-fill); stroke: var(--mm-activation-stroke); }
82
+ .mermaid .mm-note rect { fill: var(--mm-note-fill); stroke: var(--mm-note-stroke); }
83
+ .mermaid .mm-note text { fill: var(--mm-note-text); }
84
+
85
+ .mermaid.mm-error { font-family: monospace; }
86
+ .mermaid .mm-error-box { fill: var(--mm-err-fill); stroke: var(--mm-err-stroke); }
87
+ .mermaid .mm-error-title { fill: var(--mm-err-title); }
88
+ .mermaid .mm-error-msg { fill: var(--mm-err-text); }
89
+ .mermaid .mm-error-source { fill: var(--mm-err-source); }
90
+
91
+ /* --- dark scheme --------------------------------------------------- */
92
+ .dark .mermaid, .mermaid.dark {
93
+ --mm-node-fill: #1f2020;
94
+ --mm-node-stroke: #81B1DB;
95
+ --mm-node-text: #f4f4f4;
96
+ --mm-line-color: #cccccc;
97
+ --mm-edge-label-text: #e0e0e0;
98
+ --mm-edge-label-bg: #1f2020;
99
+ --mm-cluster-fill: #2b2b3a;
100
+ --mm-cluster-stroke: #6f6f9e;
101
+ --mm-actor-fill: #252526;
102
+ --mm-actor-stroke: #81B1DB;
103
+ --mm-actor-text: #f4f4f4;
104
+ --mm-lifeline: #8a8a8a;
105
+ --mm-activation-fill: #31313a;
106
+ --mm-activation-stroke: #8a8a8a;
107
+ --mm-note-fill: #3b3b26;
108
+ --mm-note-stroke: #aaaa33;
109
+ --mm-note-text: #f4f4f4;
110
+ --mm-err-fill: #2b1b1b;
111
+ --mm-err-stroke: #e74c3c;
112
+ --mm-err-title: #f5827a;
113
+ --mm-err-text: #f0c9c5;
114
+ --mm-err-source: #8a8a8a;
115
+ }