@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.
- package/README.md +282 -0
- package/dist/types/ast.d.ts +210 -0
- package/dist/types/component/index.d.ts +81 -0
- package/dist/types/errors.d.ts +27 -0
- package/dist/types/index.d.ts +97 -0
- package/dist/types/interactive.d.ts +36 -0
- package/dist/types/layout/flowchart.d.ts +12 -0
- package/dist/types/layout/sequence.d.ts +12 -0
- package/dist/types/layout/state.d.ts +19 -0
- package/dist/types/parser/class.d.ts +11 -0
- package/dist/types/parser/config.d.ts +61 -0
- package/dist/types/parser/er.d.ts +11 -0
- package/dist/types/parser/flowchart.d.ts +26 -0
- package/dist/types/parser/gantt.d.ts +11 -0
- package/dist/types/parser/index.d.ts +22 -0
- package/dist/types/parser/pie.d.ts +11 -0
- package/dist/types/parser/sequence.d.ts +16 -0
- package/dist/types/parser/state.d.ts +22 -0
- package/dist/types/plugin.d.ts +50 -0
- package/dist/types/render/error.d.ts +23 -0
- package/dist/types/render/flowchart.d.ts +18 -0
- package/dist/types/render/index.d.ts +23 -0
- package/dist/types/render/misc.d.ts +54 -0
- package/dist/types/render/sequence.d.ts +16 -0
- package/dist/types/styles.d.ts +81 -0
- package/dist/types/theme.d.ts +47 -0
- package/dist/types/to-mermaid.d.ts +20 -0
- package/dist/types/utils.d.ts +47 -0
- package/docs/MERMAID-FORMAT.md +242 -0
- package/package.json +84 -0
- package/schemas/jaren-mermaid-ast.schema.json +78 -0
- package/schemas/jaren-workflow.schema.json +28 -0
- package/src/ast.js +252 -0
- package/src/component/index.js +109 -0
- package/src/errors.js +35 -0
- package/src/index.js +155 -0
- package/src/interactive.js +244 -0
- package/src/layout/flowchart.js +352 -0
- package/src/layout/sequence.js +178 -0
- package/src/layout/state.js +65 -0
- package/src/parser/class.js +90 -0
- package/src/parser/config.js +215 -0
- package/src/parser/er.js +86 -0
- package/src/parser/flowchart.js +413 -0
- package/src/parser/gantt.js +49 -0
- package/src/parser/index.js +122 -0
- package/src/parser/pie.js +32 -0
- package/src/parser/sequence.js +156 -0
- package/src/parser/state.js +137 -0
- package/src/plugin.js +76 -0
- package/src/render/error.js +55 -0
- package/src/render/flowchart.js +249 -0
- package/src/render/index.js +93 -0
- package/src/render/misc.js +135 -0
- package/src/render/sequence.js +152 -0
- package/src/styles.js +181 -0
- package/src/theme.js +180 -0
- package/src/to-mermaid.js +317 -0
- package/src/utils.js +64 -0
- package/styles/mermaid.css +115 -0
- package/stylesheets/dag-to-flowchart.jslt.json +62 -0
- package/stylesheets/flowchart-to-dag.jslt.json +29 -0
- package/stylesheets/state-to-workflow.jslt.json +26 -0
- 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
|
+
}
|