@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,93 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The render dispatcher. Turns a `DiagramDocument` (or raw source)
4
+ * into a pure-vnode SVG, choosing the specialized layout+render closure
5
+ * per diagram type, and **never throws**: a
6
+ * parse/layout error becomes an error vnode.
7
+ */
8
+
9
+ import { parseMermaid } from '../parser/index.js';
10
+ import { createTheme } from '../theme.js';
11
+ import { MermaidParseError } from '../errors.js';
12
+ import { layoutFlowchart } from '../layout/flowchart.js';
13
+ import { layoutState } from '../layout/state.js';
14
+ import { layoutSequence } from '../layout/sequence.js';
15
+ import { renderFlowchart } from './flowchart.js';
16
+ import { renderSequence } from './sequence.js';
17
+ import { renderPie, renderStructured, renderPlaceholder, structuredSections } from './misc.js';
18
+ import { errorVnode } from './error.js';
19
+
20
+ /**
21
+ * Compute the pure `PositionedDiagram` scene for a document (no vnode).
22
+ * Returns `null` for types without a geometric layout (structured/raw).
23
+ * @param {import('../ast.js').DiagramDocument} doc
24
+ * @returns {any}
25
+ */
26
+ export function layoutDiagram(doc) {
27
+ switch (doc.diagram) {
28
+ case 'flowchart': return layoutFlowchart(doc.ast);
29
+ case 'state': return layoutState(doc.ast);
30
+ case 'sequence': return layoutSequence(doc.ast);
31
+ default: return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Render a doc or source to a pure-vnode SVG. Error-safe.
37
+ * @param {import('../ast.js').DiagramDocument | string} docOrSource
38
+ * @param {{ theme?: any, [k: string]: any }} [options]
39
+ * @returns {any} an SVG vnode
40
+ */
41
+ export function diagramToVnode(docOrSource, options = {}) {
42
+ let doc;
43
+ try {
44
+ doc = typeof docOrSource === 'string' ? parseMermaid(docOrSource, options) : docOrSource;
45
+ }
46
+ catch (err) {
47
+ // a parse failure has no document, so only the caller's theme is known
48
+ return toError(err, typeof docOrSource === 'string' ? docOrSource : '', createTheme(options.theme ?? 'default'));
49
+ }
50
+ try {
51
+ const themeArg = options.theme ?? doc.config?.theme ?? doc.config ?? 'default';
52
+ const theme = createTheme(themeArg);
53
+ const hash = doc.meta?.hash ?? '0';
54
+ switch (doc.diagram) {
55
+ case 'flowchart':
56
+ return renderFlowchart(layoutFlowchart(doc.ast), theme, hash);
57
+ case 'state':
58
+ // a state diagram IS a graph: the state adapter feeds the same
59
+ // layout and renderer as flowcharts (MERMAID-FORMAT §6)
60
+ return renderFlowchart(layoutState(doc.ast), theme, hash, 'mermaid mm-svg mm-state');
61
+ case 'sequence':
62
+ return renderSequence(layoutSequence(doc.ast), theme, hash);
63
+ case 'pie':
64
+ return renderPie(doc.ast, theme, hash);
65
+ case 'class':
66
+ case 'er':
67
+ case 'gantt': {
68
+ const { title, sections } = structuredSections(doc.diagram, doc.ast);
69
+ return renderStructured(title, sections, theme, hash);
70
+ }
71
+ default:
72
+ return renderPlaceholder(doc.diagram, theme, hash);
73
+ }
74
+ }
75
+ catch (err) {
76
+ return toError(err, '', createTheme(options.theme ?? 'default'));
77
+ }
78
+ }
79
+
80
+ /**
81
+ * @param {any} err
82
+ * @param {string} source
83
+ * @param {ReturnType<typeof createTheme>} theme
84
+ * @returns {any}
85
+ */
86
+ function toError(err, source, theme) {
87
+ if (err instanceof MermaidParseError) {
88
+ const lines = source.split(/\r\n?|\n/);
89
+ const sourceLine = err.line > 0 && err.line <= lines.length ? lines[err.line - 1] : '';
90
+ return errorVnode(err.message, err.line, sourceLine, theme);
91
+ }
92
+ return errorVnode(err && err.message ? String(err.message) : String(err), 0, '', theme);
93
+ }
@@ -0,0 +1,135 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Renderers for the first-class types beyond flowchart and
4
+ * sequence, and an honest placeholder for the deferred secondary
5
+ * types. Pie is
6
+ * a real chart; class/ER/state/gantt render as structured panels — a
7
+ * readable, geometry-light view that renders without error and so counts
8
+ * honestly in the coverage scorecard. Secondary types (mindmap,
9
+ * gitGraph, journey, timeline) render a labeled "not yet laid out"
10
+ * placeholder.
11
+ */
12
+
13
+ import { svgRoot, rect, path, textAt, num, textWidth } from '@jarenjs/view/helpers';
14
+ import { buildPieAST, renderPieAST, CATEGORICAL } from '@jarenjs/charts';
15
+ import { mermaidPieToChartAST } from '@jarenjs/charts/transforms/mermaid-adapter';
16
+
17
+ /**
18
+ * Pie rendering delegates to `@jarenjs/charts` (the pie engine's single
19
+ * home); the options carry the mermaid class names, palette and theme
20
+ * so the SVG is byte-identical to the pre-delegation renderer.
21
+ * @param {any} ast pie AST
22
+ * @param {any} theme
23
+ * @param {string} hash
24
+ * @returns {any}
25
+ */
26
+ export function renderPie(ast, theme, hash) {
27
+ const { config, data } = mermaidPieToChartAST(ast);
28
+ return renderPieAST(buildPieAST(data, config), theme, hash, {
29
+ rootClass: 'mermaid mm-svg',
30
+ keyPrefix: 'mmpie-',
31
+ sliceClass: 'mm-pie-slice',
32
+ legendClass: 'mm-pie-legend',
33
+ palette: CATEGORICAL,
34
+ textColor: theme.tokens.nodeText,
35
+ sliceStroke: '#fff',
36
+ // A mermaid diagram's SVG is a byte-stable contract; the per-slice
37
+ // hover text charts adds for its own pies would break it.
38
+ titles: false,
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Render a structured panel: a title and a list of sections, each a
44
+ * bordered box with a heading and text rows.
45
+ * @param {string} title
46
+ * @param {{ heading: string, rows: string[] }[]} sections
47
+ * @param {any} theme
48
+ * @param {string} hash
49
+ * @returns {any}
50
+ */
51
+ export function renderStructured(title, sections, theme, hash) {
52
+ const t = theme.tokens;
53
+ const fs = 13;
54
+ const rowH = 22;
55
+ const headH = 26;
56
+ const boxW = Math.max(180, ...sections.flatMap((s) => [
57
+ textWidth(s.heading, fs, 700) + 24,
58
+ ...s.rows.map((r) => textWidth(r, fs) + 24),
59
+ ]));
60
+ const gap = 16;
61
+ const children = [];
62
+ let y = 40;
63
+ children.push(textAt(16, 24, title, fs + 3, { 'font-weight': 'bold', fill: t.nodeText }));
64
+ for (const section of sections) {
65
+ const boxH = headH + section.rows.length * rowH + 8;
66
+ children.push(rect(16, y, boxW, boxH, { rx: 4, fill: t.nodeFill, stroke: t.nodeStroke, 'stroke-width': 1, class: 'mm-panel' }));
67
+ children.push(textAt(26, y + 17, section.heading, fs, { 'font-weight': 'bold', fill: t.nodeText }));
68
+ children.push(path(`M16,${num(y + headH)} h${boxW}`, { stroke: t.nodeStroke, 'stroke-width': 1 }));
69
+ for (let i = 0; i < section.rows.length; i++) {
70
+ children.push(textAt(26, y + headH + 16 + i * rowH, section.rows[i], fs, { fill: t.nodeText }));
71
+ }
72
+ y += boxH + gap;
73
+ }
74
+ return svgRoot('mermaid mm-svg', boxW + 32, y, theme, children,
75
+ 'mmstruct-' + hash, { fit: false });
76
+ }
77
+
78
+ /**
79
+ * The honest placeholder for a parse-accepted secondary type.
80
+ * @param {string} type
81
+ * @param {any} theme
82
+ * @param {string} hash
83
+ * @returns {any}
84
+ */
85
+ export function renderPlaceholder(type, theme, hash) {
86
+ const t = theme.tokens;
87
+ const width = 380;
88
+ const height = 84;
89
+ return svgRoot('mermaid mm-svg', width, height, theme, [
90
+ rect(1, 1, width - 2, height - 2, { rx: 6, fill: t.clusterFill, stroke: t.clusterStroke, 'stroke-width': 1, 'stroke-dasharray': '5 4' }),
91
+ textAt(width / 2, 36, `${type} diagram`, 15, { 'font-weight': 'bold', 'text-anchor': 'middle', fill: t.nodeText }),
92
+ textAt(width / 2, 58, 'parsed — not yet laid out in v1', 12, { 'text-anchor': 'middle', fill: t.nodeText }),
93
+ ], 'mmph-' + hash);
94
+ }
95
+
96
+ /**
97
+ * Build the sections for the structured renderers.
98
+ * @param {string} diagram
99
+ * @param {any} ast
100
+ * @returns {{ title: string, sections: { heading: string, rows: string[] }[] }}
101
+ */
102
+ export function structuredSections(diagram, ast) {
103
+ if (diagram === 'class') {
104
+ const sections = ast.classes.map((c) => ({
105
+ heading: c.name,
106
+ rows: c.members.map((m) => `${m.visibility ?? ''}${m.text}`),
107
+ }));
108
+ if (ast.relations.length) {
109
+ sections.push({ heading: 'relations', rows: ast.relations.map((r) => `${r.from} ${r.type} ${r.to}${r.label ? ' : ' + r.label : ''}`) });
110
+ }
111
+ return { title: 'Class diagram', sections };
112
+ }
113
+ if (diagram === 'er') {
114
+ const sections = ast.entities.map((e) => ({
115
+ heading: e.name,
116
+ rows: e.attributes.map((a) => `${a.type} ${a.name}${a.keys.length ? ' ' + a.keys.join(',') : ''}`),
117
+ }));
118
+ if (ast.relationships.length) {
119
+ sections.push({ heading: 'relationships', rows: ast.relationships.map((r) => `${r.left} ${r.leftCard}--${r.rightCard} ${r.right} : ${r.label}`) });
120
+ }
121
+ return { title: 'Entity–relationship', sections };
122
+ }
123
+ // state diagrams left this path for the graph layout; a stray call
124
+ // must fail loudly rather than quietly resurrect the old panel
125
+ if (diagram !== 'gantt') {
126
+ throw new Error(`structuredSections: '${diagram}' is not a structured-panel type`);
127
+ }
128
+ return {
129
+ title: 'Gantt' + (ast.meta.title ? ': ' + ast.meta.title : ''),
130
+ sections: ast.sections.map((s) => ({
131
+ heading: s.name ?? 'tasks',
132
+ rows: s.tasks.map((tk) => `${tk.name} — ${tk.info}`),
133
+ })),
134
+ };
135
+ }
@@ -0,0 +1,152 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Sequence renderer: `PositionedDiagram` → pure-vnode SVG.
4
+ * Actor boxes (top and bottom), dashed lifelines, activation bars,
5
+ * messages with solid/dotted lines and arrow/open/cross/async heads,
6
+ * note boxes and block frames.
7
+ */
8
+
9
+ import { svgRoot, group, rect, path, line, polygon, textAt, textLines, num } from '@jarenjs/view/helpers';
10
+
11
+ /**
12
+ * @param {any} scene PositionedDiagram (sequence)
13
+ * @param {{ tokens: Record<string,string>, cssVars: Record<string,string> }} theme
14
+ * @param {string} hash
15
+ * @returns {any}
16
+ */
17
+ export function renderSequence(scene, theme, hash) {
18
+ const t = theme.tokens;
19
+ const fs = scene.fontSize;
20
+ const children = [];
21
+
22
+ // Block frames (behind).
23
+ for (let i = 0; i < scene.blocks.length; i++) {
24
+ const b = scene.blocks[i];
25
+ children.push(renderBlock(b, t, fs, i));
26
+ }
27
+
28
+ // Lifelines.
29
+ for (const a of scene.actors) {
30
+ children.push(line(a.x, scene.lineTop, a.x, a.bottomY, {
31
+ class: 'mm-lifeline', stroke: t.lifeline, 'stroke-width': 1, 'stroke-dasharray': '3 3',
32
+ }));
33
+ }
34
+
35
+ // Activation bars.
36
+ for (const act of scene.activations) {
37
+ children.push(rect(act.x, act.y, act.w, act.h, {
38
+ class: 'mm-activation', fill: t.activationFill, stroke: t.activationStroke, 'stroke-width': 1,
39
+ }));
40
+ }
41
+
42
+ // Actor boxes, top and bottom.
43
+ for (const a of scene.actors) {
44
+ children.push(actorBox(a, a.boxY, t, fs));
45
+ children.push(actorBox(a, a.bottomY, t, fs));
46
+ }
47
+
48
+ // Notes.
49
+ for (let i = 0; i < scene.notes.length; i++) {
50
+ const nnode = scene.notes[i];
51
+ children.push(group({ class: 'mm-note', key: 'note-' + i }, [
52
+ rect(nnode.x, nnode.y, nnode.w, nnode.h, { fill: t.noteFill, stroke: t.noteStroke, 'stroke-width': 1 }),
53
+ textLines(nnode.x + nnode.w / 2, nnode.y + nnode.h / 2, nnode.text.split('\n'), fs, { fill: t.noteText }),
54
+ ]));
55
+ }
56
+
57
+ // Messages.
58
+ for (let i = 0; i < scene.messages.length; i++) {
59
+ children.push(renderMessage(scene.messages[i], t, fs, i));
60
+ }
61
+
62
+ return svgRoot('mermaid mm-svg', scene.width, scene.height, theme, children,
63
+ 'mmseq-' + hash, { fit: false });
64
+ }
65
+
66
+ /**
67
+ * @param {any} a actor @param {number} y box top @param {Record<string,string>} t @param {number} fs
68
+ * @returns {any}
69
+ */
70
+ function actorBox(a, y, t, fs) {
71
+ return group({ class: 'mm-actor' }, [
72
+ rect(a.boxX, y, a.w, a.h, { rx: 3, fill: t.actorFill, stroke: t.actorStroke, 'stroke-width': 1 }),
73
+ textLines(a.x, y + a.h / 2, [a.label], fs, { fill: t.actorText, 'font-weight': 'bold' }),
74
+ ]);
75
+ }
76
+
77
+ /**
78
+ * @param {any} m @param {Record<string,string>} t @param {number} fs @param {number} i
79
+ * @returns {any}
80
+ */
81
+ function renderMessage(m, t, fs, i) {
82
+ const stroke = t.lineColor;
83
+ const dash = m.line === 'dotted' ? '4 3' : null;
84
+ const parts = [];
85
+ const lineProps = { class: 'mm-message-line', stroke, 'stroke-width': 1.2, fill: 'none' };
86
+ if (dash) lineProps['stroke-dasharray'] = dash;
87
+
88
+ if (m.self) {
89
+ const loopW = 40;
90
+ const y0 = m.y;
91
+ const y1 = m.y + 26;
92
+ parts.push(path(`M${num(m.x1)},${num(y0)} h${loopW} v${y1 - y0} h${-loopW}`, lineProps));
93
+ parts.push(arrowHead({ x: m.x1 + 6, y: y1 }, { x: m.x1 + loopW, y: y1 }, m.head, stroke));
94
+ parts.push(textAt(m.x1 + loopW + 6, (y0 + y1) / 2, m.label, fs, { class: 'mm-message-label', fill: t.edgeLabelText }));
95
+ }
96
+ else {
97
+ parts.push(line(m.x1, m.y, m.x2, m.y, lineProps));
98
+ parts.push(arrowHead({ x: m.x2, y: m.y }, { x: m.x1, y: m.y }, m.head, stroke));
99
+ const midX = (m.x1 + m.x2) / 2;
100
+ parts.push(textAt(midX, m.y - 6, m.label, fs, {
101
+ 'text-anchor': 'middle', class: 'mm-message-label', fill: t.edgeLabelText,
102
+ }));
103
+ }
104
+ return group({ class: 'mm-message', key: 'msg-' + i }, parts);
105
+ }
106
+
107
+ /**
108
+ * @param {{x:number,y:number}} tip @param {{x:number,y:number}} from
109
+ * @param {string} head @param {string} color
110
+ * @returns {any}
111
+ */
112
+ function arrowHead(tip, from, head, color) {
113
+ const dir = tip.x >= from.x ? 1 : -1;
114
+ const size = 8;
115
+ if (head === 'cross') {
116
+ const s = 5;
117
+ return path(
118
+ `M${num(tip.x)},${num(tip.y - s)} L${num(tip.x - dir * s)},${num(tip.y + s)}`
119
+ + ` M${num(tip.x)},${num(tip.y + s)} L${num(tip.x - dir * s)},${num(tip.y - s)}`,
120
+ { stroke: color, 'stroke-width': 1.5, fill: 'none' });
121
+ }
122
+ if (head === 'open') {
123
+ return path(
124
+ `M${num(tip.x - dir * size)},${num(tip.y - 5)} L${num(tip.x)},${num(tip.y)} L${num(tip.x - dir * size)},${num(tip.y + 5)}`,
125
+ { stroke: color, 'stroke-width': 1.2, fill: 'none' });
126
+ }
127
+ // filled arrow / async point → filled triangle
128
+ return polygon([
129
+ { x: tip.x, y: tip.y },
130
+ { x: tip.x - dir * size, y: tip.y - 4 },
131
+ { x: tip.x - dir * size, y: tip.y + 4 },
132
+ ], { class: 'mm-arrowhead', fill: color, stroke: color });
133
+ }
134
+
135
+ /**
136
+ * @param {any} b @param {Record<string,string>} t @param {number} fs @param {number} i
137
+ * @returns {any}
138
+ */
139
+ function renderBlock(b, t, fs, i) {
140
+ const label = b.blockType + (b.label ? ' [' + b.label + ']' : '');
141
+ const tabW = Math.max(48, label.length * fs * 0.5 + 16);
142
+ const parts = [
143
+ rect(b.x, b.y, b.w, b.h, { class: 'mm-block', fill: 'none', stroke: t.actorStroke, 'stroke-width': 1 }),
144
+ path(`M${num(b.x)},${num(b.y + 18)} h${num(tabW)} l-8,6 h${num(-tabW + 8)} Z`, { fill: t.clusterFill, stroke: t.actorStroke, 'stroke-width': 1 }),
145
+ textAt(b.x + 6, b.y + 14, label, fs - 1, { 'font-weight': 'bold', class: 'mm-block-label', fill: t.nodeText }),
146
+ ];
147
+ for (const d of b.dividers) {
148
+ parts.push(line(b.x, d.y, b.x + b.w, d.y, { stroke: t.actorStroke, 'stroke-width': 1, 'stroke-dasharray': '2 2' }));
149
+ if (d.label) parts.push(textAt(b.x + b.w / 2, d.y - 4, '[' + d.label + ']', fs - 1, { 'text-anchor': 'middle', class: 'mm-block-divider-label', fill: t.nodeText }));
150
+ }
151
+ return group({ class: 'mm-block-group', key: 'blk-' + i }, parts);
152
+ }
package/src/styles.js ADDED
@@ -0,0 +1,181 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `classDef` / `class` / `style` resolution.
4
+ *
5
+ * The parser has always recorded these three statements; nothing consumed
6
+ * them, so a diagram that styled a node rendered identically to one that did
7
+ * not. This module turns them into per-node style objects the layout attaches
8
+ * to its positioned nodes, which is what lets a diagram carry emphasis —
9
+ * "this box is the input", "this one is an aside" — instead of every node
10
+ * looking the same.
11
+ *
12
+ * One built-in class ships: **`note`**. Mermaid has no flowchart note, and a
13
+ * diagram that cannot annotate a node loses exactly the information an ASCII
14
+ * drawing used to carry in a margin comment. Rather than invent syntax for
15
+ * it, `note` is a class any diagram can apply with the standard `class`
16
+ * statement, themed from the `note*` tokens the sequence renderer already
17
+ * uses. A dotted link to a note-classed node reads as an annotation and stays
18
+ * valid Mermaid that other tools can still parse.
19
+ */
20
+
21
+ import { inkFor, isHexColor } from '@jarenjs/core/color';
22
+
23
+ /** Style properties that mean something to a shape, mapped to SVG attributes. */
24
+ const SHAPE_PROPS = {
25
+ 'fill': 'fill',
26
+ 'stroke': 'stroke',
27
+ 'stroke-width': 'stroke-width',
28
+ 'stroke-dasharray': 'stroke-dasharray',
29
+ 'opacity': 'opacity',
30
+ };
31
+
32
+ /**
33
+ * Parse a Mermaid style string (`fill:#eee,stroke-width:2px`) into an object.
34
+ * Unknown properties are kept: a consumer may understand more than this one,
35
+ * and silently dropping a declaration the author wrote is the same class of
36
+ * dishonesty as dropping a schema constraint.
37
+ * @param {string} source
38
+ * @returns {Record<string,string>}
39
+ */
40
+ export function parseStyleString(source) {
41
+ /** @type {Record<string,string>} */
42
+ const out = {};
43
+ if (typeof source !== 'string') return out;
44
+ for (const part of source.split(',')) {
45
+ const colon = part.indexOf(':');
46
+ if (colon === -1) continue;
47
+ const key = part.slice(0, colon).trim().toLowerCase();
48
+ const value = part.slice(colon + 1).trim();
49
+ if (key !== '' && value !== '') out[key] = value;
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * Resolve every node's styles from a flowchart AST.
56
+ *
57
+ * Precedence, lowest to highest: the built-in `note` class, then each
58
+ * `classDef` in the order the node's `class` statements applied them, then a
59
+ * per-node `style` statement. That is Mermaid's own order — later wins — and
60
+ * it is what lets a diagram say "these are all notes, except this one is
61
+ * red".
62
+ * @param {any} ast flowchart AST
63
+ * @returns {Map<string, Record<string,string>>} node id -> style properties
64
+ */
65
+ export function resolveNodeStyles(ast) {
66
+ /** @type {Map<string, Record<string,string>>} */
67
+ const out = new Map();
68
+ if (ast === null || typeof ast !== 'object') return out;
69
+
70
+ /** @type {Map<string, Record<string,string>>} */
71
+ const defs = new Map();
72
+ for (const def of ast.classDefs ?? []) {
73
+ if (def === null || typeof def.name !== 'string') continue;
74
+ // A classDef may name several classes at once (`classDef a,b fill:#eee`).
75
+ for (const name of def.name.split(',')) {
76
+ const key = name.trim();
77
+ if (key === '') continue;
78
+ defs.set(key, { ...(defs.get(key) ?? {}), ...parseStyleString(def.styles) });
79
+ }
80
+ }
81
+
82
+ const add = (id, props) => {
83
+ if (typeof id !== 'string' || id === '') return;
84
+ out.set(id, { ...(out.get(id) ?? {}), ...props });
85
+ };
86
+
87
+ for (const applied of ast.classes ?? []) {
88
+ if (applied === null) continue;
89
+ // `class a,b name` applies one class to several nodes.
90
+ const nodes = String(applied.node ?? '').split(',');
91
+ for (const rawName of String(applied.name ?? '').split(',')) {
92
+ const name = rawName.trim();
93
+ if (name === '') continue;
94
+ // The built-in class carries no properties of its own: it is a marker
95
+ // the renderer resolves against the theme, because a note's colors have
96
+ // to follow light/dark like every other themed element.
97
+ const props = name === 'note' ? { 'mm-builtin': 'note' } : defs.get(name);
98
+ if (props === undefined) continue;
99
+ for (const node of nodes) add(node.trim(), props);
100
+ }
101
+ }
102
+
103
+ for (const style of ast.styles ?? []) {
104
+ if (style === null) continue;
105
+ for (const node of String(style.node ?? '').split(','))
106
+ add(node.trim(), parseStyleString(style.styles));
107
+ }
108
+
109
+ return out;
110
+ }
111
+
112
+ /**
113
+ * The author's styles as an inline CSS declaration, or `null` when there are
114
+ * none.
115
+ *
116
+ * This has to be a `style` attribute rather than presentation attributes:
117
+ * the bundled stylesheet sets `.mermaid .mm-node-shape { fill: var(...) }` so
118
+ * a themed page can retheme every diagram at once, and a CSS rule outranks a
119
+ * presentation attribute. Emitting `fill="..."` therefore looked correct in
120
+ * the SSR string and was silently overridden the moment the stylesheet
121
+ * loaded — a classDef that worked in a test and did nothing on the page.
122
+ * Inline style outranks the rule, which is the precedence an author asking
123
+ * for a specific colour expects.
124
+ * @param {Record<string,string>|undefined} styles
125
+ * @param {Record<string,string>} tokens theme tokens
126
+ * @returns {string|null}
127
+ */
128
+ export function shapeStyle(styles, tokens) {
129
+ const parts = [];
130
+ for (const [key, value] of Object.entries(shapeAttributes(styles, tokens)))
131
+ parts.push(`${key}:${value}`);
132
+ return parts.length === 0 ? null : parts.join(';');
133
+ }
134
+
135
+ /**
136
+ * The SVG attributes a resolved style object contributes to a shape, with the
137
+ * built-in `note` marker expanded against the live theme.
138
+ * @param {Record<string,string>|undefined} styles
139
+ * @param {Record<string,string>} tokens theme tokens
140
+ * @returns {Record<string,string>}
141
+ */
142
+ export function shapeAttributes(styles, tokens) {
143
+ /** @type {Record<string,string>} */
144
+ const out = {};
145
+ if (styles === undefined) return out;
146
+ if (styles['mm-builtin'] === 'note') {
147
+ out.fill = tokens.noteFill;
148
+ out.stroke = tokens.noteStroke;
149
+ out['stroke-dasharray'] = '4 3';
150
+ }
151
+ for (const [key, attribute] of Object.entries(SHAPE_PROPS)) {
152
+ const value = styles[key];
153
+ if (value === undefined) continue;
154
+ out[attribute] = key === 'stroke-width' ? value.replace(/px$/i, '') : value;
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /**
160
+ * The text color for a node, or null to keep the theme's.
161
+ *
162
+ * An author-specified `fill` is a CONSTANT — it does not follow light/dark —
163
+ * so the ink over it must not follow the theme either. A pale `fill:#dcfce7`
164
+ * under a dark theme would otherwise get the theme's light text and the label
165
+ * would vanish into its own box. The ink is therefore derived from the fill's
166
+ * luminance, exactly as chart tiles do it, unless the author named a `color`
167
+ * themselves.
168
+ *
169
+ * The built-in `note` class is the opposite case and keeps the theme's note
170
+ * ink: its fill is a theme token too, so the pair moves together.
171
+ * @param {Record<string,string>|undefined} styles
172
+ * @param {Record<string,string>} tokens theme tokens
173
+ * @returns {string|null}
174
+ */
175
+ export function textColor(styles, tokens) {
176
+ if (styles === undefined) return null;
177
+ if (typeof styles.color === 'string') return styles.color;
178
+ if (styles['mm-builtin'] === 'note' && !isHexColor(styles.fill)) return tokens.noteText;
179
+ if (isHexColor(styles.fill)) return inkFor(styles.fill);
180
+ return null;
181
+ }