@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
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
|
+
}
|