flowink 0.1.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/LICENSE +21 -0
- package/NOTICE.md +17 -0
- package/README.md +301 -0
- package/dist/errors.d.ts +13 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +3560 -0
- package/dist/index.js.map +7 -0
- package/dist/layout.d.ts +13 -0
- package/dist/parse.d.ts +3 -0
- package/dist/parser/generated.d.ts +3 -0
- package/dist/parser/preprocess.d.ts +11 -0
- package/dist/parser/semantic.d.ts +76 -0
- package/dist/place-labels.d.ts +15 -0
- package/dist/rasterize.d.ts +47 -0
- package/dist/text.d.ts +36 -0
- package/dist/types.d.ts +88 -0
- package/examples/basic.mmd +4 -0
- package/examples/chinese.mmd +2 -0
- package/examples/cycle.mmd +2 -0
- package/examples/decision.mmd +4 -0
- package/examples/hero.mmd +2 -0
- package/examples/subgraph.mmd +5 -0
- package/package.json +71 -0
- package/vendor/mermaid/LICENSE +21 -0
- package/vendor/mermaid/README.md +16 -0
- package/vendor/mermaid/flow.jison +635 -0
- package/vendor/mermaid/flowDb.ts +1336 -0
- package/vendor/mermaid/flowParser.ts +12 -0
- package/vendor/mermaid/provenance.json +14 -0
|
@@ -0,0 +1,1336 @@
|
|
|
1
|
+
import { select } from 'd3';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import { getConfig, defaultConfig } from '../../diagram-api/diagramAPI.js';
|
|
4
|
+
import type { DiagramDB } from '../../diagram-api/types.js';
|
|
5
|
+
import { log } from '../../logger.js';
|
|
6
|
+
import { isValidShape, type ShapeID } from '../../rendering-util/rendering-elements/shapes.js';
|
|
7
|
+
import type { Edge, Node } from '../../rendering-util/types.js';
|
|
8
|
+
import type { EdgeMetaData, NodeMetaData } from '../../types.js';
|
|
9
|
+
import utils, { getEdgeId } from '../../utils.js';
|
|
10
|
+
import common from '../common/common.js';
|
|
11
|
+
import {
|
|
12
|
+
setAccTitle,
|
|
13
|
+
getAccTitle,
|
|
14
|
+
getAccDescription,
|
|
15
|
+
setAccDescription,
|
|
16
|
+
clear as commonClear,
|
|
17
|
+
setDiagramTitle,
|
|
18
|
+
getDiagramTitle,
|
|
19
|
+
} from '../common/commonDb.js';
|
|
20
|
+
import { createTooltip } from '../common/svgDrawCommon.js';
|
|
21
|
+
import type {
|
|
22
|
+
FlowClass,
|
|
23
|
+
FlowEdge,
|
|
24
|
+
FlowLink,
|
|
25
|
+
FlowSubGraph,
|
|
26
|
+
FlowText,
|
|
27
|
+
FlowVertex,
|
|
28
|
+
FlowVertexTypeParam,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
import DOMPurify from 'dompurify';
|
|
31
|
+
interface LinkData {
|
|
32
|
+
id: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MERMAID_DOM_ID_PREFIX = 'flowchart-';
|
|
36
|
+
|
|
37
|
+
// We are using arrow functions assigned to class instance fields instead of methods as they are required by flow JISON
|
|
38
|
+
export class FlowDB implements DiagramDB {
|
|
39
|
+
private vertexCounter = 0;
|
|
40
|
+
private config = getConfig();
|
|
41
|
+
private diagramId = '';
|
|
42
|
+
private vertices = new Map<string, FlowVertex>();
|
|
43
|
+
private edges: FlowEdge[] & { defaultInterpolate?: string; defaultStyle?: string[] } = [];
|
|
44
|
+
private classes = new Map<string, FlowClass>();
|
|
45
|
+
private subGraphs: FlowSubGraph[] = [];
|
|
46
|
+
private subGraphLookup = new Map<string, FlowSubGraph>();
|
|
47
|
+
private tooltips = new Map<string, string>();
|
|
48
|
+
private subCount = 0;
|
|
49
|
+
private firstGraphFlag = true;
|
|
50
|
+
private direction: string | undefined;
|
|
51
|
+
private version: string | undefined; // As in graph
|
|
52
|
+
private secCount = -1;
|
|
53
|
+
private posCrossRef: number[] = [];
|
|
54
|
+
|
|
55
|
+
// Functions to be run after graph rendering
|
|
56
|
+
private funs: ((element: Element) => void)[] = []; // cspell:ignore funs
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
this.funs.push(this.setupToolTips.bind(this));
|
|
60
|
+
|
|
61
|
+
// Needed for JISON since it only supports direct properties
|
|
62
|
+
this.addVertex = this.addVertex.bind(this);
|
|
63
|
+
this.firstGraph = this.firstGraph.bind(this);
|
|
64
|
+
this.setDirection = this.setDirection.bind(this);
|
|
65
|
+
this.addSubGraph = this.addSubGraph.bind(this);
|
|
66
|
+
this.addLink = this.addLink.bind(this);
|
|
67
|
+
this.setLink = this.setLink.bind(this);
|
|
68
|
+
this.updateLink = this.updateLink.bind(this);
|
|
69
|
+
this.addClass = this.addClass.bind(this);
|
|
70
|
+
this.setClass = this.setClass.bind(this);
|
|
71
|
+
this.destructLink = this.destructLink.bind(this);
|
|
72
|
+
this.setClickEvent = this.setClickEvent.bind(this);
|
|
73
|
+
this.setTooltip = this.setTooltip.bind(this);
|
|
74
|
+
this.updateLinkInterpolate = this.updateLinkInterpolate.bind(this);
|
|
75
|
+
this.setClickFun = this.setClickFun.bind(this);
|
|
76
|
+
this.bindFunctions = this.bindFunctions.bind(this);
|
|
77
|
+
|
|
78
|
+
this.lex = {
|
|
79
|
+
firstGraph: this.firstGraph.bind(this),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.clear();
|
|
83
|
+
this.setGen('gen-2');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private sanitizeText(txt: string) {
|
|
87
|
+
return common.sanitizeText(txt, this.config);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private sanitizeNodeLabelType(labelType?: string) {
|
|
91
|
+
switch (labelType) {
|
|
92
|
+
case 'markdown':
|
|
93
|
+
case 'string':
|
|
94
|
+
case 'text':
|
|
95
|
+
return labelType;
|
|
96
|
+
default:
|
|
97
|
+
return 'markdown';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Sets the diagram's SVG element ID, used to prefix domIds for uniqueness
|
|
103
|
+
* across multiple diagrams on the same page.
|
|
104
|
+
*/
|
|
105
|
+
public setDiagramId(svgElementId: string) {
|
|
106
|
+
this.diagramId = svgElementId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Function to lookup domId from id in the graph definition.
|
|
111
|
+
* When diagramId is set, returns the prefixed version for DOM uniqueness.
|
|
112
|
+
*
|
|
113
|
+
* @param id - id of the node
|
|
114
|
+
*/
|
|
115
|
+
public lookUpDomId(id: string) {
|
|
116
|
+
for (const vertex of this.vertices.values()) {
|
|
117
|
+
if (vertex.id === id) {
|
|
118
|
+
return this.diagramId ? `${this.diagramId}-${vertex.domId}` : vertex.domId;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return this.diagramId ? `${this.diagramId}-${id}` : id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Function called by parser when a node definition has been found
|
|
126
|
+
*/
|
|
127
|
+
public addVertex(
|
|
128
|
+
id: string,
|
|
129
|
+
textObj: FlowText,
|
|
130
|
+
type: FlowVertexTypeParam,
|
|
131
|
+
style: string[],
|
|
132
|
+
classes: string[],
|
|
133
|
+
dir: string,
|
|
134
|
+
props = {},
|
|
135
|
+
metadata: any
|
|
136
|
+
) {
|
|
137
|
+
if (!id || id.trim().length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Extract the metadata from the shapeData, the syntax for adding metadata for nodes and edges is the same
|
|
141
|
+
// so at this point we don't know if it's a node or an edge, but we can still extract the metadata
|
|
142
|
+
let doc;
|
|
143
|
+
if (metadata !== undefined) {
|
|
144
|
+
let yamlData;
|
|
145
|
+
// detect if shapeData contains a newline character
|
|
146
|
+
if (!metadata.includes('\n')) {
|
|
147
|
+
yamlData = '{\n' + metadata + '\n}';
|
|
148
|
+
} else {
|
|
149
|
+
yamlData = metadata + '\n';
|
|
150
|
+
}
|
|
151
|
+
doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if this is metadata for an already-declared subgraph
|
|
155
|
+
// (e.g. `sub1@{ view: collapsed }`). The id refers to a subgraph, so
|
|
156
|
+
// route the metadata onto the subgraph instead of creating a vertex.
|
|
157
|
+
const subGraph = this.subGraphLookup.get(id);
|
|
158
|
+
if (subGraph && doc) {
|
|
159
|
+
subGraph.metadata = { ...subGraph.metadata, ...doc };
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if this is an edge
|
|
164
|
+
const edge = this.edges.find((e) => e.id === id);
|
|
165
|
+
if (edge) {
|
|
166
|
+
const edgeDoc = doc as EdgeMetaData;
|
|
167
|
+
if (edgeDoc?.animate !== undefined) {
|
|
168
|
+
edge.animate = edgeDoc.animate;
|
|
169
|
+
}
|
|
170
|
+
if (edgeDoc?.animation !== undefined) {
|
|
171
|
+
edge.animation = edgeDoc.animation;
|
|
172
|
+
}
|
|
173
|
+
if (edgeDoc?.curve !== undefined) {
|
|
174
|
+
edge.interpolate = edgeDoc.curve;
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let txt;
|
|
180
|
+
|
|
181
|
+
let vertex = this.vertices.get(id);
|
|
182
|
+
if (vertex === undefined) {
|
|
183
|
+
if (textObj === undefined && type === undefined && style !== undefined && style !== null) {
|
|
184
|
+
log.warn(
|
|
185
|
+
`Style applied to unknown node "${id}". This may indicate a typo. The node will be created automatically.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
vertex = {
|
|
189
|
+
id,
|
|
190
|
+
labelType: 'text',
|
|
191
|
+
domId: MERMAID_DOM_ID_PREFIX + id + '-' + this.vertexCounter,
|
|
192
|
+
styles: [],
|
|
193
|
+
classes: [],
|
|
194
|
+
};
|
|
195
|
+
this.vertices.set(id, vertex);
|
|
196
|
+
}
|
|
197
|
+
this.vertexCounter++;
|
|
198
|
+
|
|
199
|
+
if (textObj !== undefined) {
|
|
200
|
+
this.config = getConfig();
|
|
201
|
+
txt = this.sanitizeText(textObj.text.trim());
|
|
202
|
+
vertex.labelType = textObj.type;
|
|
203
|
+
// strip quotes if string starts and ends with a quote
|
|
204
|
+
if (txt.startsWith('"') && txt.endsWith('"')) {
|
|
205
|
+
txt = txt.substring(1, txt.length - 1);
|
|
206
|
+
}
|
|
207
|
+
vertex.text = txt;
|
|
208
|
+
} else {
|
|
209
|
+
if (vertex.text === undefined) {
|
|
210
|
+
vertex.text = id;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (type !== undefined) {
|
|
214
|
+
vertex.type = type;
|
|
215
|
+
}
|
|
216
|
+
if (style !== undefined && style !== null) {
|
|
217
|
+
style.forEach((s) => {
|
|
218
|
+
vertex.styles.push(s);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (classes !== undefined && classes !== null) {
|
|
222
|
+
classes.forEach((s) => {
|
|
223
|
+
vertex.classes.push(s);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (dir !== undefined) {
|
|
227
|
+
vertex.dir = dir;
|
|
228
|
+
}
|
|
229
|
+
if (vertex.props === undefined) {
|
|
230
|
+
vertex.props = props;
|
|
231
|
+
} else if (props !== undefined) {
|
|
232
|
+
Object.assign(vertex.props, props);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (doc !== undefined) {
|
|
236
|
+
if (doc.shape) {
|
|
237
|
+
if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) {
|
|
238
|
+
throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`);
|
|
239
|
+
} else if (!isValidShape(doc.shape)) {
|
|
240
|
+
throw new Error(`No such shape: ${doc.shape}.`);
|
|
241
|
+
}
|
|
242
|
+
vertex.type = doc?.shape;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (doc?.label) {
|
|
246
|
+
vertex.text = doc?.label;
|
|
247
|
+
vertex.labelType = this.sanitizeNodeLabelType(doc?.labelType);
|
|
248
|
+
}
|
|
249
|
+
if (doc?.icon) {
|
|
250
|
+
vertex.icon = doc?.icon;
|
|
251
|
+
if (!doc.label?.trim() && vertex.text === id) {
|
|
252
|
+
vertex.text = '';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (doc?.form) {
|
|
256
|
+
vertex.form = doc?.form;
|
|
257
|
+
}
|
|
258
|
+
if (doc?.pos) {
|
|
259
|
+
vertex.pos = doc?.pos;
|
|
260
|
+
}
|
|
261
|
+
if (doc?.img) {
|
|
262
|
+
vertex.img = doc?.img;
|
|
263
|
+
if (!doc.label?.trim() && vertex.text === id) {
|
|
264
|
+
vertex.text = '';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (doc?.constraint) {
|
|
268
|
+
vertex.constraint = doc.constraint;
|
|
269
|
+
}
|
|
270
|
+
if (doc.w) {
|
|
271
|
+
vertex.assetWidth = Number(doc.w);
|
|
272
|
+
}
|
|
273
|
+
if (doc.h) {
|
|
274
|
+
vertex.assetHeight = Number(doc.h);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Function called by parser when a link/edge definition has been found
|
|
281
|
+
*
|
|
282
|
+
*/
|
|
283
|
+
public addSingleLink(_start: string, _end: string, type: any, id?: string) {
|
|
284
|
+
const start = _start;
|
|
285
|
+
const end = _end;
|
|
286
|
+
|
|
287
|
+
const edge: FlowEdge = {
|
|
288
|
+
start: start,
|
|
289
|
+
end: end,
|
|
290
|
+
type: undefined,
|
|
291
|
+
text: '',
|
|
292
|
+
labelType: 'text',
|
|
293
|
+
classes: [],
|
|
294
|
+
isUserDefinedId: false,
|
|
295
|
+
interpolate: this.edges.defaultInterpolate,
|
|
296
|
+
};
|
|
297
|
+
log.info('abc78 Got edge...', edge);
|
|
298
|
+
const linkTextObj = type.text;
|
|
299
|
+
|
|
300
|
+
if (linkTextObj !== undefined) {
|
|
301
|
+
edge.text = this.sanitizeText(linkTextObj.text.trim());
|
|
302
|
+
|
|
303
|
+
// strip quotes if string starts and ends with a quote
|
|
304
|
+
if (edge.text.startsWith('"') && edge.text.endsWith('"')) {
|
|
305
|
+
edge.text = edge.text.substring(1, edge.text.length - 1);
|
|
306
|
+
}
|
|
307
|
+
edge.labelType = this.sanitizeNodeLabelType(linkTextObj.type);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (type !== undefined) {
|
|
311
|
+
edge.type = type.type;
|
|
312
|
+
edge.stroke = type.stroke;
|
|
313
|
+
edge.length = type.length > 10 ? 10 : type.length;
|
|
314
|
+
}
|
|
315
|
+
if (id && !this.edges.some((e) => e.id === id)) {
|
|
316
|
+
edge.id = id;
|
|
317
|
+
edge.isUserDefinedId = true;
|
|
318
|
+
} else {
|
|
319
|
+
const existingLinks = this.edges.filter((e) => e.start === edge.start && e.end === edge.end);
|
|
320
|
+
if (existingLinks.length === 0) {
|
|
321
|
+
edge.id = getEdgeId(edge.start, edge.end, { counter: 0, prefix: 'L' });
|
|
322
|
+
} else {
|
|
323
|
+
edge.id = getEdgeId(edge.start, edge.end, {
|
|
324
|
+
counter: existingLinks.length + 1,
|
|
325
|
+
prefix: 'L',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (this.edges.length < (this.config.maxEdges ?? 500)) {
|
|
331
|
+
log.info('Pushing edge...');
|
|
332
|
+
this.edges.push(edge);
|
|
333
|
+
} else {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Edge limit exceeded. ${this.edges.length} edges found, but the limit is ${this.config.maxEdges}.
|
|
336
|
+
|
|
337
|
+
Initialize mermaid with maxEdges set to a higher number to allow more edges.
|
|
338
|
+
You cannot set this config via configuration inside the diagram as it is a secure config.
|
|
339
|
+
You have to call mermaid.initialize.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private isLinkData(value: unknown): value is LinkData {
|
|
345
|
+
return (
|
|
346
|
+
value !== null &&
|
|
347
|
+
typeof value === 'object' &&
|
|
348
|
+
'id' in value &&
|
|
349
|
+
typeof (value as LinkData).id === 'string'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
public addLink(_start: string[], _end: string[], linkData: unknown) {
|
|
354
|
+
const id = this.isLinkData(linkData) ? linkData.id.replace('@', '') : undefined;
|
|
355
|
+
|
|
356
|
+
log.info('addLink', _start, _end, id);
|
|
357
|
+
|
|
358
|
+
// for a group syntax like A e1@--> B & C, only the first edge should have a userDefined id
|
|
359
|
+
// the rest of the edges should have auto generated ids
|
|
360
|
+
for (const start of _start) {
|
|
361
|
+
for (const end of _end) {
|
|
362
|
+
//use the id only for last node in _start and first node in _end
|
|
363
|
+
const isLastStart = start === _start[_start.length - 1];
|
|
364
|
+
const isFirstEnd = end === _end[0];
|
|
365
|
+
if (isLastStart && isFirstEnd) {
|
|
366
|
+
this.addSingleLink(start, end, linkData, id);
|
|
367
|
+
} else {
|
|
368
|
+
this.addSingleLink(start, end, linkData, undefined);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Updates a link's line interpolation algorithm
|
|
376
|
+
*/
|
|
377
|
+
public updateLinkInterpolate(positions: ('default' | number)[], interpolate: string) {
|
|
378
|
+
positions.forEach((pos) => {
|
|
379
|
+
if (pos === 'default') {
|
|
380
|
+
this.edges.defaultInterpolate = interpolate;
|
|
381
|
+
} else {
|
|
382
|
+
this.edges[pos].interpolate = interpolate;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Updates a link with a style
|
|
389
|
+
*
|
|
390
|
+
*/
|
|
391
|
+
public updateLink(positions: ('default' | number)[], style: string[]) {
|
|
392
|
+
positions.forEach((pos) => {
|
|
393
|
+
if (typeof pos === 'number' && pos >= this.edges.length) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
`The index ${pos} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${
|
|
396
|
+
this.edges.length - 1
|
|
397
|
+
}. (Help: Ensure that the index is within the range of existing edges.)`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (pos === 'default') {
|
|
401
|
+
this.edges.defaultStyle = style;
|
|
402
|
+
} else {
|
|
403
|
+
this.edges[pos].style = style;
|
|
404
|
+
// if edges[pos].style does have fill not set, set it to none
|
|
405
|
+
if (
|
|
406
|
+
(this.edges[pos]?.style?.length ?? 0) > 0 &&
|
|
407
|
+
!this.edges[pos]?.style?.some((s) => s?.startsWith('fill'))
|
|
408
|
+
) {
|
|
409
|
+
this.edges[pos]?.style?.push('fill:none');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
public addClass(ids: string, _style: string[]) {
|
|
416
|
+
const style = _style
|
|
417
|
+
.join()
|
|
418
|
+
.replace(/\\,/g, '§§§')
|
|
419
|
+
.replace(/,/g, ';')
|
|
420
|
+
.replace(/§§§/g, ',')
|
|
421
|
+
.split(';');
|
|
422
|
+
ids.split(',').forEach((id) => {
|
|
423
|
+
let classNode = this.classes.get(id);
|
|
424
|
+
if (classNode === undefined) {
|
|
425
|
+
classNode = { id, styles: [], textStyles: [] };
|
|
426
|
+
this.classes.set(id, classNode);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (style !== undefined && style !== null) {
|
|
430
|
+
style.forEach((s) => {
|
|
431
|
+
if (/color/.exec(s)) {
|
|
432
|
+
const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill');
|
|
433
|
+
classNode.textStyles.push(newStyle);
|
|
434
|
+
}
|
|
435
|
+
classNode.styles.push(s);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Called by parser when a graph definition is found, stores the direction of the chart.
|
|
443
|
+
*
|
|
444
|
+
*/
|
|
445
|
+
public setDirection(dir: string) {
|
|
446
|
+
this.direction = dir.trim();
|
|
447
|
+
|
|
448
|
+
if (/.*</.exec(this.direction)) {
|
|
449
|
+
this.direction = 'RL';
|
|
450
|
+
}
|
|
451
|
+
if (/.*\^/.exec(this.direction)) {
|
|
452
|
+
this.direction = 'BT';
|
|
453
|
+
}
|
|
454
|
+
if (/.*>/.exec(this.direction)) {
|
|
455
|
+
this.direction = 'LR';
|
|
456
|
+
}
|
|
457
|
+
if (/.*v/.exec(this.direction)) {
|
|
458
|
+
this.direction = 'TB';
|
|
459
|
+
}
|
|
460
|
+
if (this.direction === 'TD') {
|
|
461
|
+
this.direction = 'TB';
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Called by parser when a special node is found, e.g. a clickable element.
|
|
467
|
+
*
|
|
468
|
+
* @param ids - Comma separated list of ids
|
|
469
|
+
* @param className - Class to add
|
|
470
|
+
*/
|
|
471
|
+
public setClass(ids: string, className: string) {
|
|
472
|
+
for (const id of ids.split(',')) {
|
|
473
|
+
const vertex = this.vertices.get(id);
|
|
474
|
+
if (vertex) {
|
|
475
|
+
vertex.classes.push(className);
|
|
476
|
+
}
|
|
477
|
+
const edge = this.edges.find((e) => e.id === id);
|
|
478
|
+
if (edge) {
|
|
479
|
+
edge.classes.push(className);
|
|
480
|
+
}
|
|
481
|
+
const subGraph = this.subGraphLookup.get(id);
|
|
482
|
+
if (subGraph) {
|
|
483
|
+
subGraph.classes.push(className);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
public setTooltip(ids: string, tooltip: string) {
|
|
489
|
+
if (tooltip === undefined) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
tooltip = this.sanitizeText(tooltip);
|
|
493
|
+
for (const id of ids.split(',')) {
|
|
494
|
+
this.tooltips.set(this.version === 'gen-1' ? this.lookUpDomId(id) : id, tooltip);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private setClickFun(id: string, functionName: string, functionArgs: string) {
|
|
499
|
+
// if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id;
|
|
500
|
+
if (getConfig().securityLevel !== 'loose') {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (functionName === undefined) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
let argList: string[] = [];
|
|
507
|
+
if (typeof functionArgs === 'string') {
|
|
508
|
+
/* Splits functionArgs by ',', ignoring all ',' in double quoted strings */
|
|
509
|
+
argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
|
|
510
|
+
for (let i = 0; i < argList.length; i++) {
|
|
511
|
+
let item = argList[i].trim();
|
|
512
|
+
/* Removes all double quotes at the start and end of an argument */
|
|
513
|
+
/* This preserves all starting and ending whitespace inside */
|
|
514
|
+
if (item.startsWith('"') && item.endsWith('"')) {
|
|
515
|
+
item = item.substr(1, item.length - 2);
|
|
516
|
+
}
|
|
517
|
+
argList[i] = item;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* if no arguments passed into callback, default to passing in id */
|
|
522
|
+
if (argList.length === 0) {
|
|
523
|
+
argList.push(id);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const vertex = this.vertices.get(id);
|
|
527
|
+
if (vertex) {
|
|
528
|
+
vertex.haveCallback = true;
|
|
529
|
+
this.funs.push(() => {
|
|
530
|
+
// Defer lookUpDomId to bind time so it includes the diagramId prefix
|
|
531
|
+
const domId = this.lookUpDomId(id);
|
|
532
|
+
const elem = document.querySelector(`[id="${domId}"]`);
|
|
533
|
+
if (elem !== null) {
|
|
534
|
+
elem.addEventListener(
|
|
535
|
+
'click',
|
|
536
|
+
() => {
|
|
537
|
+
utils.runFunc(functionName, ...argList);
|
|
538
|
+
},
|
|
539
|
+
false
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Called by parser when a link is found. Adds the URL to the vertex data.
|
|
548
|
+
*
|
|
549
|
+
* @param ids - Comma separated list of ids
|
|
550
|
+
* @param linkStr - URL to create a link for
|
|
551
|
+
* @param target - Target attribute for the link
|
|
552
|
+
*/
|
|
553
|
+
public setLink(ids: string, linkStr: string, target: string) {
|
|
554
|
+
ids.split(',').forEach((id) => {
|
|
555
|
+
const vertex = this.vertices.get(id);
|
|
556
|
+
if (vertex !== undefined) {
|
|
557
|
+
vertex.link = utils.formatUrl(linkStr, this.config);
|
|
558
|
+
vertex.linkTarget = target;
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
this.setClass(ids, 'clickable');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
public getTooltip(id: string) {
|
|
565
|
+
return this.tooltips.get(id);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Called by parser when a click definition is found. Registers an event handler.
|
|
570
|
+
*
|
|
571
|
+
* @param ids - Comma separated list of ids
|
|
572
|
+
* @param functionName - Function to be called on click
|
|
573
|
+
* @param functionArgs - Arguments to be passed to the function
|
|
574
|
+
*/
|
|
575
|
+
public setClickEvent(ids: string, functionName: string, functionArgs: string) {
|
|
576
|
+
ids.split(',').forEach((id) => {
|
|
577
|
+
this.setClickFun(id, functionName, functionArgs);
|
|
578
|
+
});
|
|
579
|
+
this.setClass(ids, 'clickable');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
public bindFunctions(element: Element) {
|
|
583
|
+
this.funs.forEach((fun) => {
|
|
584
|
+
fun(element);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
public getDirection() {
|
|
588
|
+
return this.direction?.trim();
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Retrieval function for fetching the found nodes after parsing has completed.
|
|
592
|
+
*
|
|
593
|
+
*/
|
|
594
|
+
public getVertices() {
|
|
595
|
+
return this.vertices;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Retrieval function for fetching the found links after parsing has completed.
|
|
600
|
+
*
|
|
601
|
+
*/
|
|
602
|
+
public getEdges() {
|
|
603
|
+
return this.edges;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Retrieval function for fetching the found class definitions after parsing has completed.
|
|
608
|
+
*
|
|
609
|
+
*/
|
|
610
|
+
public getClasses() {
|
|
611
|
+
return this.classes;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private setupToolTips(element: Element) {
|
|
615
|
+
const tooltipElem = createTooltip();
|
|
616
|
+
|
|
617
|
+
const svg = select(element).select('svg');
|
|
618
|
+
|
|
619
|
+
const nodes = svg.selectAll('g.node');
|
|
620
|
+
nodes
|
|
621
|
+
.on('mouseover', (e: MouseEvent) => {
|
|
622
|
+
const el = select(e.currentTarget as Element);
|
|
623
|
+
const title = el.attr('title');
|
|
624
|
+
|
|
625
|
+
// Don't try to draw a tooltip if no data is provided
|
|
626
|
+
if (title === null) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
const rect = (e.currentTarget as Element)?.getBoundingClientRect();
|
|
630
|
+
|
|
631
|
+
tooltipElem.transition().duration(200).style('opacity', '.9');
|
|
632
|
+
tooltipElem
|
|
633
|
+
.text(el.attr('title'))
|
|
634
|
+
.style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px')
|
|
635
|
+
.style('top', window.scrollY + rect.bottom + 'px');
|
|
636
|
+
tooltipElem.html(DOMPurify.sanitize(title));
|
|
637
|
+
el.classed('hover', true);
|
|
638
|
+
})
|
|
639
|
+
.on('mouseout', (e: MouseEvent) => {
|
|
640
|
+
tooltipElem.transition().duration(500).style('opacity', 0);
|
|
641
|
+
const el = select(e.currentTarget as Element);
|
|
642
|
+
el.classed('hover', false);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Clears the internal graph db so that a new graph can be parsed.
|
|
648
|
+
*
|
|
649
|
+
*/
|
|
650
|
+
public clear(ver = 'gen-2') {
|
|
651
|
+
this.vertices = new Map();
|
|
652
|
+
this.classes = new Map();
|
|
653
|
+
this.edges = [];
|
|
654
|
+
this.funs = [this.setupToolTips.bind(this)];
|
|
655
|
+
this.diagramId = '';
|
|
656
|
+
this.subGraphs = [];
|
|
657
|
+
this.subGraphLookup = new Map();
|
|
658
|
+
this.subCount = 0;
|
|
659
|
+
this.tooltips = new Map();
|
|
660
|
+
this.firstGraphFlag = true;
|
|
661
|
+
this.version = ver;
|
|
662
|
+
this.config = getConfig();
|
|
663
|
+
commonClear();
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
public setGen(ver: string) {
|
|
667
|
+
this.version = ver || 'gen-2';
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
public defaultStyle() {
|
|
671
|
+
return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;';
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
public addSubGraph(
|
|
675
|
+
_id: { text: string },
|
|
676
|
+
list: string[],
|
|
677
|
+
_title: { text: string; type: string }
|
|
678
|
+
) {
|
|
679
|
+
let id: string | undefined = _id.text.trim();
|
|
680
|
+
let title = _title.text;
|
|
681
|
+
if (_id === _title && /\s/.exec(_title.text)) {
|
|
682
|
+
id = undefined;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const uniq = (a: any[]) => {
|
|
686
|
+
const prims: any = { boolean: {}, number: {}, string: {} };
|
|
687
|
+
const objs: any[] = [];
|
|
688
|
+
|
|
689
|
+
let dir: string | undefined;
|
|
690
|
+
|
|
691
|
+
const nodeList = a.filter(function (item) {
|
|
692
|
+
const type = typeof item;
|
|
693
|
+
if (item.stmt && item.stmt === 'dir') {
|
|
694
|
+
dir = item.value;
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
if (item.trim() === '') {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
if (type in prims) {
|
|
701
|
+
return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true);
|
|
702
|
+
} else {
|
|
703
|
+
return objs.includes(item) ? false : objs.push(item);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return { nodeList, dir };
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const result = uniq(list.flat());
|
|
710
|
+
const nodeList = result.nodeList;
|
|
711
|
+
let dir = result.dir;
|
|
712
|
+
const flowchartConfig = getConfig().flowchart ?? {};
|
|
713
|
+
dir =
|
|
714
|
+
dir ??
|
|
715
|
+
(flowchartConfig.inheritDir
|
|
716
|
+
? (this.getDirection() ?? (getConfig() as any).direction ?? undefined)
|
|
717
|
+
: undefined);
|
|
718
|
+
|
|
719
|
+
if (this.version === 'gen-1') {
|
|
720
|
+
for (let i = 0; i < nodeList.length; i++) {
|
|
721
|
+
nodeList[i] = this.lookUpDomId(nodeList[i]);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
id = id ?? 'subGraph' + this.subCount;
|
|
726
|
+
title = title || '';
|
|
727
|
+
title = this.sanitizeText(title);
|
|
728
|
+
this.subCount = this.subCount + 1;
|
|
729
|
+
|
|
730
|
+
const subGraph = {
|
|
731
|
+
id: id,
|
|
732
|
+
nodes: nodeList,
|
|
733
|
+
title: title.trim(),
|
|
734
|
+
classes: [],
|
|
735
|
+
dir,
|
|
736
|
+
labelType: this.sanitizeNodeLabelType(_title?.type),
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
log.info('Adding', subGraph.id, subGraph.nodes, subGraph.dir);
|
|
740
|
+
|
|
741
|
+
// Remove the members in the new subgraph if they already belong to another subgraph
|
|
742
|
+
subGraph.nodes = this.makeUniq(subGraph, this.subGraphs).nodes;
|
|
743
|
+
this.subGraphs.push(subGraph);
|
|
744
|
+
this.subGraphLookup.set(id, subGraph);
|
|
745
|
+
return id;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private getPosForId(id: string) {
|
|
749
|
+
for (const [i, subGraph] of this.subGraphs.entries()) {
|
|
750
|
+
if (subGraph.id === id) {
|
|
751
|
+
return i;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return -1;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private indexNodes2(id: string, pos: number): { result: boolean; count: number } {
|
|
758
|
+
const nodes = this.subGraphs[pos].nodes;
|
|
759
|
+
this.secCount = this.secCount + 1;
|
|
760
|
+
if (this.secCount > 2000) {
|
|
761
|
+
return {
|
|
762
|
+
result: false,
|
|
763
|
+
count: 0,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
this.posCrossRef[this.secCount] = pos;
|
|
767
|
+
// Check if match
|
|
768
|
+
if (this.subGraphs[pos].id === id) {
|
|
769
|
+
return {
|
|
770
|
+
result: true,
|
|
771
|
+
count: 0,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
let count = 0;
|
|
776
|
+
let posCount = 1;
|
|
777
|
+
while (count < nodes.length) {
|
|
778
|
+
const childPos = this.getPosForId(nodes[count]);
|
|
779
|
+
// Ignore regular nodes (pos will be -1)
|
|
780
|
+
if (childPos >= 0) {
|
|
781
|
+
const res = this.indexNodes2(id, childPos);
|
|
782
|
+
if (res.result) {
|
|
783
|
+
return {
|
|
784
|
+
result: true,
|
|
785
|
+
count: posCount + res.count,
|
|
786
|
+
};
|
|
787
|
+
} else {
|
|
788
|
+
posCount = posCount + res.count;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
count = count + 1;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return {
|
|
795
|
+
result: false,
|
|
796
|
+
count: posCount,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
public getDepthFirstPos(pos: number) {
|
|
801
|
+
return this.posCrossRef[pos];
|
|
802
|
+
}
|
|
803
|
+
public indexNodes() {
|
|
804
|
+
this.secCount = -1;
|
|
805
|
+
if (this.subGraphs.length > 0) {
|
|
806
|
+
this.indexNodes2('none', this.subGraphs.length - 1);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
public getSubGraphs() {
|
|
811
|
+
return this.subGraphs;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
public firstGraph() {
|
|
815
|
+
if (this.firstGraphFlag) {
|
|
816
|
+
this.firstGraphFlag = false;
|
|
817
|
+
return true;
|
|
818
|
+
}
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private destructStartLink(_str: string): FlowLink {
|
|
823
|
+
let str = _str.trim();
|
|
824
|
+
let type = 'arrow_open';
|
|
825
|
+
|
|
826
|
+
switch (str[0]) {
|
|
827
|
+
case '<':
|
|
828
|
+
type = 'arrow_point';
|
|
829
|
+
str = str.slice(1);
|
|
830
|
+
break;
|
|
831
|
+
case 'x':
|
|
832
|
+
type = 'arrow_cross';
|
|
833
|
+
str = str.slice(1);
|
|
834
|
+
break;
|
|
835
|
+
case 'o':
|
|
836
|
+
type = 'arrow_circle';
|
|
837
|
+
str = str.slice(1);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
let stroke = 'normal';
|
|
842
|
+
|
|
843
|
+
if (str.includes('=')) {
|
|
844
|
+
stroke = 'thick';
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (str.includes('.')) {
|
|
848
|
+
stroke = 'dotted';
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return { type, stroke };
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
private countChar(char: string, str: string) {
|
|
855
|
+
const length = str.length;
|
|
856
|
+
let count = 0;
|
|
857
|
+
for (let i = 0; i < length; ++i) {
|
|
858
|
+
if (str[i] === char) {
|
|
859
|
+
++count;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return count;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private destructEndLink(_str: string) {
|
|
866
|
+
const str = _str.trim();
|
|
867
|
+
let line = str.slice(0, -1);
|
|
868
|
+
let type = 'arrow_open';
|
|
869
|
+
|
|
870
|
+
switch (str.slice(-1)) {
|
|
871
|
+
case 'x':
|
|
872
|
+
type = 'arrow_cross';
|
|
873
|
+
if (str.startsWith('x')) {
|
|
874
|
+
type = 'double_' + type;
|
|
875
|
+
line = line.slice(1);
|
|
876
|
+
}
|
|
877
|
+
break;
|
|
878
|
+
case '>':
|
|
879
|
+
type = 'arrow_point';
|
|
880
|
+
if (str.startsWith('<')) {
|
|
881
|
+
type = 'double_' + type;
|
|
882
|
+
line = line.slice(1);
|
|
883
|
+
}
|
|
884
|
+
break;
|
|
885
|
+
case 'o':
|
|
886
|
+
type = 'arrow_circle';
|
|
887
|
+
if (str.startsWith('o')) {
|
|
888
|
+
type = 'double_' + type;
|
|
889
|
+
line = line.slice(1);
|
|
890
|
+
}
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
let stroke = 'normal';
|
|
895
|
+
let length = line.length - 1;
|
|
896
|
+
|
|
897
|
+
if (line.startsWith('=')) {
|
|
898
|
+
stroke = 'thick';
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (line.startsWith('~')) {
|
|
902
|
+
stroke = 'invisible';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const dots = this.countChar('.', line);
|
|
906
|
+
|
|
907
|
+
if (dots) {
|
|
908
|
+
stroke = 'dotted';
|
|
909
|
+
length = dots;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
return { type, stroke, length };
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
public destructLink(_str: string, _startStr: string) {
|
|
916
|
+
const info = this.destructEndLink(_str);
|
|
917
|
+
let startInfo;
|
|
918
|
+
if (_startStr) {
|
|
919
|
+
startInfo = this.destructStartLink(_startStr);
|
|
920
|
+
|
|
921
|
+
if (startInfo.stroke !== info.stroke) {
|
|
922
|
+
return { type: 'INVALID', stroke: 'INVALID' };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (startInfo.type === 'arrow_open') {
|
|
926
|
+
// -- xyz --> - take arrow type from ending
|
|
927
|
+
startInfo.type = info.type;
|
|
928
|
+
} else {
|
|
929
|
+
// x-- xyz --> - not supported
|
|
930
|
+
if (startInfo.type !== info.type) {
|
|
931
|
+
return { type: 'INVALID', stroke: 'INVALID' };
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
startInfo.type = 'double_' + startInfo.type;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (startInfo.type === 'double_arrow') {
|
|
938
|
+
startInfo.type = 'double_arrow_point';
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
startInfo.length = info.length;
|
|
942
|
+
return startInfo;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return info;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Todo optimizer this by caching existing nodes
|
|
949
|
+
public exists(allSgs: FlowSubGraph[], _id: string) {
|
|
950
|
+
for (const sg of allSgs) {
|
|
951
|
+
if (sg.nodes.includes(_id)) {
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Deletes an id from all subgraphs
|
|
959
|
+
*
|
|
960
|
+
*/
|
|
961
|
+
public makeUniq(sg: FlowSubGraph, allSubgraphs: FlowSubGraph[]) {
|
|
962
|
+
const res: string[] = [];
|
|
963
|
+
sg.nodes.forEach((_id, pos) => {
|
|
964
|
+
if (!this.exists(allSubgraphs, _id)) {
|
|
965
|
+
res.push(sg.nodes[pos]);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
return { nodes: res };
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
public lex: { firstGraph: typeof FlowDB.prototype.firstGraph };
|
|
972
|
+
|
|
973
|
+
private getTypeFromVertex(vertex: FlowVertex): ShapeID {
|
|
974
|
+
if (vertex.img) {
|
|
975
|
+
return 'imageSquare';
|
|
976
|
+
}
|
|
977
|
+
if (vertex.icon) {
|
|
978
|
+
if (vertex.form === 'circle') {
|
|
979
|
+
return 'iconCircle';
|
|
980
|
+
}
|
|
981
|
+
if (vertex.form === 'square') {
|
|
982
|
+
return 'iconSquare';
|
|
983
|
+
}
|
|
984
|
+
if (vertex.form === 'rounded') {
|
|
985
|
+
return 'iconRounded';
|
|
986
|
+
}
|
|
987
|
+
return 'icon';
|
|
988
|
+
}
|
|
989
|
+
switch (vertex.type) {
|
|
990
|
+
case 'square':
|
|
991
|
+
case undefined:
|
|
992
|
+
return 'squareRect';
|
|
993
|
+
case 'round':
|
|
994
|
+
return 'roundedRect';
|
|
995
|
+
case 'ellipse':
|
|
996
|
+
// @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976
|
|
997
|
+
return 'ellipse';
|
|
998
|
+
default:
|
|
999
|
+
return vertex.type;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
private findNode(nodes: Node[], id: string) {
|
|
1004
|
+
return nodes.find((node) => node.id === id);
|
|
1005
|
+
}
|
|
1006
|
+
private destructEdgeType(type: string | undefined) {
|
|
1007
|
+
let arrowTypeStart = 'none';
|
|
1008
|
+
let arrowTypeEnd = 'arrow_point';
|
|
1009
|
+
switch (type) {
|
|
1010
|
+
case 'arrow_point':
|
|
1011
|
+
case 'arrow_circle':
|
|
1012
|
+
case 'arrow_cross':
|
|
1013
|
+
arrowTypeEnd = type;
|
|
1014
|
+
break;
|
|
1015
|
+
|
|
1016
|
+
case 'double_arrow_point':
|
|
1017
|
+
case 'double_arrow_circle':
|
|
1018
|
+
case 'double_arrow_cross':
|
|
1019
|
+
arrowTypeStart = type.replace('double_', '');
|
|
1020
|
+
arrowTypeEnd = arrowTypeStart;
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
return { arrowTypeStart, arrowTypeEnd };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private addNodeFromVertex(
|
|
1027
|
+
vertex: FlowVertex,
|
|
1028
|
+
nodes: Node[],
|
|
1029
|
+
parentDB: Map<string, string>,
|
|
1030
|
+
subGraphDB: Map<string, boolean>,
|
|
1031
|
+
config: any,
|
|
1032
|
+
look: string
|
|
1033
|
+
) {
|
|
1034
|
+
const parentId = parentDB.get(vertex.id);
|
|
1035
|
+
const isGroup = subGraphDB.get(vertex.id) ?? false;
|
|
1036
|
+
|
|
1037
|
+
const node = this.findNode(nodes, vertex.id);
|
|
1038
|
+
if (node) {
|
|
1039
|
+
node.cssStyles = vertex.styles;
|
|
1040
|
+
node.cssCompiledStyles = this.getCompiledStyles(vertex.classes);
|
|
1041
|
+
node.cssClasses = vertex.classes.join(' ');
|
|
1042
|
+
} else {
|
|
1043
|
+
const baseNode = {
|
|
1044
|
+
id: vertex.id,
|
|
1045
|
+
label: vertex.text,
|
|
1046
|
+
labelType: vertex.labelType,
|
|
1047
|
+
labelStyle: '',
|
|
1048
|
+
parentId,
|
|
1049
|
+
padding: config.flowchart?.padding || 8,
|
|
1050
|
+
cssStyles: vertex.styles,
|
|
1051
|
+
cssCompiledStyles: this.getCompiledStyles(['default', 'node', ...vertex.classes]),
|
|
1052
|
+
cssClasses: 'default ' + vertex.classes.join(' '),
|
|
1053
|
+
dir: vertex.dir,
|
|
1054
|
+
domId: vertex.domId,
|
|
1055
|
+
look,
|
|
1056
|
+
link: vertex.link,
|
|
1057
|
+
linkTarget: vertex.linkTarget,
|
|
1058
|
+
tooltip: this.getTooltip(vertex.id),
|
|
1059
|
+
icon: vertex.icon,
|
|
1060
|
+
pos: vertex.pos,
|
|
1061
|
+
img: vertex.img,
|
|
1062
|
+
assetWidth: vertex.assetWidth,
|
|
1063
|
+
assetHeight: vertex.assetHeight,
|
|
1064
|
+
constraint: vertex.constraint,
|
|
1065
|
+
};
|
|
1066
|
+
if (isGroup) {
|
|
1067
|
+
nodes.push({
|
|
1068
|
+
...baseNode,
|
|
1069
|
+
isGroup: true,
|
|
1070
|
+
shape: 'rect',
|
|
1071
|
+
});
|
|
1072
|
+
} else {
|
|
1073
|
+
nodes.push({
|
|
1074
|
+
...baseNode,
|
|
1075
|
+
isGroup: false,
|
|
1076
|
+
shape: this.getTypeFromVertex(vertex),
|
|
1077
|
+
minWidth: config.flowchart?.minNodeWidth,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
private getCompiledStyles(classDefs: string[]) {
|
|
1084
|
+
let compiledStyles: string[] = [];
|
|
1085
|
+
for (const customClass of classDefs) {
|
|
1086
|
+
const cssClass = this.classes.get(customClass);
|
|
1087
|
+
if (cssClass?.styles) {
|
|
1088
|
+
compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim());
|
|
1089
|
+
}
|
|
1090
|
+
if (cssClass?.textStyles) {
|
|
1091
|
+
compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim());
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return compiledStyles;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
public getData() {
|
|
1098
|
+
const config = getConfig();
|
|
1099
|
+
const nodes: Node[] = [];
|
|
1100
|
+
const edges: Edge[] = [];
|
|
1101
|
+
|
|
1102
|
+
const subGraphs = this.getSubGraphs();
|
|
1103
|
+
const parentDB = new Map<string, string>();
|
|
1104
|
+
const subGraphDB = new Map<string, boolean>();
|
|
1105
|
+
|
|
1106
|
+
// ── Collapsible subgraphs (issue #7784) ──────────────────────────────
|
|
1107
|
+
// A subgraph carrying `@{ view: collapsed }` is drawn as a single compact
|
|
1108
|
+
// node; its descendants are hidden and any edge that crosses the boundary
|
|
1109
|
+
// is redirected to the outermost collapsed ancestor.
|
|
1110
|
+
//
|
|
1111
|
+
// `subGraphParent` maps a subgraph id to the subgraph that directly
|
|
1112
|
+
// contains it, so we can walk up the containment chain to find the
|
|
1113
|
+
// outermost collapsed ancestor regardless of declaration order.
|
|
1114
|
+
const subGraphParent = new Map<string, string>();
|
|
1115
|
+
for (const sg of subGraphs) {
|
|
1116
|
+
for (const childId of sg.nodes) {
|
|
1117
|
+
if (this.subGraphLookup.has(childId)) {
|
|
1118
|
+
subGraphParent.set(childId, sg.id);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
/* Colour slots follow the order the subgraphs appear in the source.
|
|
1123
|
+
*
|
|
1124
|
+
* `subGraphs` is not in that order: the grammar reduces a subgraph when it *closes*,
|
|
1125
|
+
* so a nested one lands before its parent -- `Outer { InnerOne, InnerTwo }, Sibling`
|
|
1126
|
+
* arrives as [InnerOne, InnerTwo, Outer, Sibling]. Taking the array index directly
|
|
1127
|
+
* would hand Outer slot 2 while its own children took 0 and 1.
|
|
1128
|
+
*
|
|
1129
|
+
* A pre-order walk of the containment forest recovers source order: roots complete in
|
|
1130
|
+
* source order relative to each other, and a parent is always declared before the
|
|
1131
|
+
* children it contains. Assigned once here so the collapsed and expanded branches
|
|
1132
|
+
* below cannot drift apart.
|
|
1133
|
+
*/
|
|
1134
|
+
const declarationIndex = new Map<string, number>();
|
|
1135
|
+
const childrenOf = new Map<string, string[]>();
|
|
1136
|
+
for (const sg of subGraphs) {
|
|
1137
|
+
const parent = subGraphParent.get(sg.id);
|
|
1138
|
+
if (parent !== undefined) {
|
|
1139
|
+
childrenOf.set(parent, [...(childrenOf.get(parent) ?? []), sg.id]);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
let nextDeclarationIndex = 0;
|
|
1143
|
+
const walk = (sgId: string) => {
|
|
1144
|
+
declarationIndex.set(sgId, nextDeclarationIndex++);
|
|
1145
|
+
for (const childId of childrenOf.get(sgId) ?? []) {
|
|
1146
|
+
walk(childId);
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
for (const sg of subGraphs) {
|
|
1150
|
+
if (!subGraphParent.has(sg.id)) {
|
|
1151
|
+
walk(sg.id);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const isCollapsed = (sgId: string) =>
|
|
1156
|
+
this.subGraphLookup.get(sgId)?.metadata?.view === 'collapsed';
|
|
1157
|
+
const outermostCollapsed = (sgId: string): string | undefined => {
|
|
1158
|
+
let result: string | undefined;
|
|
1159
|
+
const seen = new Set<string>();
|
|
1160
|
+
let current: string | undefined = sgId;
|
|
1161
|
+
while (current !== undefined && !seen.has(current)) {
|
|
1162
|
+
seen.add(current);
|
|
1163
|
+
if (isCollapsed(current)) {
|
|
1164
|
+
result = current;
|
|
1165
|
+
}
|
|
1166
|
+
current = subGraphParent.get(current);
|
|
1167
|
+
}
|
|
1168
|
+
return result;
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
// `hiddenIds` are nodes/subgraphs that are not drawn; `collapsedAncestorMap`
|
|
1172
|
+
// maps each hidden id to the visible collapsed node that replaces it.
|
|
1173
|
+
const hiddenIds = new Set<string>();
|
|
1174
|
+
const collapsedAncestorMap = new Map<string, string>();
|
|
1175
|
+
for (const sg of subGraphs) {
|
|
1176
|
+
const ancestor = outermostCollapsed(sg.id);
|
|
1177
|
+
if (ancestor === undefined) {
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
// Hide the subgraph itself unless it is the visible collapsed node.
|
|
1181
|
+
if (sg.id !== ancestor) {
|
|
1182
|
+
hiddenIds.add(sg.id);
|
|
1183
|
+
collapsedAncestorMap.set(sg.id, ancestor);
|
|
1184
|
+
}
|
|
1185
|
+
// Hide every member, redirecting it to the visible collapsed node.
|
|
1186
|
+
for (const childId of sg.nodes) {
|
|
1187
|
+
if (childId === ancestor) {
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
hiddenIds.add(childId);
|
|
1191
|
+
collapsedAncestorMap.set(childId, ancestor);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Setup the subgraph data for adding nodes
|
|
1196
|
+
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
|
1197
|
+
const subGraph = subGraphs[i];
|
|
1198
|
+
if (hiddenIds.has(subGraph.id)) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
if (subGraph.nodes.length > 0) {
|
|
1202
|
+
subGraphDB.set(subGraph.id, true);
|
|
1203
|
+
}
|
|
1204
|
+
for (const id of subGraph.nodes) {
|
|
1205
|
+
parentDB.set(id, subGraph.id);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Data is setup, add the nodes
|
|
1210
|
+
for (let i = subGraphs.length - 1; i >= 0; i--) {
|
|
1211
|
+
const subGraph = subGraphs[i];
|
|
1212
|
+
if (hiddenIds.has(subGraph.id)) {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
if (subGraph.metadata?.view === 'collapsed') {
|
|
1216
|
+
// Collapsed: draw as a single compact node instead of a container.
|
|
1217
|
+
nodes.push({
|
|
1218
|
+
id: subGraph.id,
|
|
1219
|
+
label: subGraph.title,
|
|
1220
|
+
labelStyle: '',
|
|
1221
|
+
labelType: subGraph.labelType,
|
|
1222
|
+
parentId: parentDB.get(subGraph.id),
|
|
1223
|
+
padding: 8,
|
|
1224
|
+
cssCompiledStyles: this.getCompiledStyles(subGraph.classes),
|
|
1225
|
+
cssClasses: subGraph.classes.join(' '),
|
|
1226
|
+
shape: 'collapsedGroup',
|
|
1227
|
+
dir: subGraph.dir,
|
|
1228
|
+
isGroup: false,
|
|
1229
|
+
look: config.look,
|
|
1230
|
+
// A collapsed subgraph still consumes its slot, so the cycle does not shift
|
|
1231
|
+
// when one is collapsed.
|
|
1232
|
+
colorIndex: declarationIndex.get(subGraph.id),
|
|
1233
|
+
});
|
|
1234
|
+
} else {
|
|
1235
|
+
nodes.push({
|
|
1236
|
+
id: subGraph.id,
|
|
1237
|
+
label: subGraph.title,
|
|
1238
|
+
labelStyle: '',
|
|
1239
|
+
labelType: subGraph.labelType,
|
|
1240
|
+
parentId: parentDB.get(subGraph.id),
|
|
1241
|
+
padding: 8,
|
|
1242
|
+
cssCompiledStyles: this.getCompiledStyles(subGraph.classes),
|
|
1243
|
+
cssClasses: subGraph.classes.join(' '),
|
|
1244
|
+
shape: 'rect',
|
|
1245
|
+
dir: subGraph.dir,
|
|
1246
|
+
isGroup: true,
|
|
1247
|
+
look: config.look,
|
|
1248
|
+
colorIndex: declarationIndex.get(subGraph.id),
|
|
1249
|
+
// Forwarded so layout engines can read per-container settings such as
|
|
1250
|
+
// `@{ algorithm: elk.box }`. `view` is consumed above; everything else
|
|
1251
|
+
// is opaque here and simply passed through. The cast is the
|
|
1252
|
+
// interface-vs-index-signature gap: `NodeMetaData` is an interface, so
|
|
1253
|
+
// it is not structurally assignable to `Record<string, unknown>`.
|
|
1254
|
+
metadata: subGraph.metadata as Record<string, unknown> | undefined,
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const n = this.getVertices();
|
|
1260
|
+
n.forEach((vertex) => {
|
|
1261
|
+
// Skip vertices hidden inside a collapsed subgraph
|
|
1262
|
+
if (hiddenIds.has(vertex.id)) {
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
this.addNodeFromVertex(vertex, nodes, parentDB, subGraphDB, config, config.look || 'classic');
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
const e = this.getEdges();
|
|
1269
|
+
e.forEach((rawEdge, index) => {
|
|
1270
|
+
const { arrowTypeStart, arrowTypeEnd } = this.destructEdgeType(rawEdge.type);
|
|
1271
|
+
const styles = [...(e.defaultStyle ?? [])];
|
|
1272
|
+
|
|
1273
|
+
// Redirect boundary-crossing edges to the visible collapsed node. An
|
|
1274
|
+
// edge that becomes a self-loop purely because both endpoints collapsed
|
|
1275
|
+
// into the same node (i.e. it was internal to a collapsed subgraph) is
|
|
1276
|
+
// dropped — self-loops on nodes that are not collapsed are preserved.
|
|
1277
|
+
const start = collapsedAncestorMap.get(rawEdge.start) ?? rawEdge.start;
|
|
1278
|
+
const end = collapsedAncestorMap.get(rawEdge.end) ?? rawEdge.end;
|
|
1279
|
+
if (
|
|
1280
|
+
start === end &&
|
|
1281
|
+
(collapsedAncestorMap.has(rawEdge.start) || collapsedAncestorMap.has(rawEdge.end))
|
|
1282
|
+
) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (rawEdge.style) {
|
|
1287
|
+
styles.push(...rawEdge.style);
|
|
1288
|
+
}
|
|
1289
|
+
const edge: Edge = {
|
|
1290
|
+
id: getEdgeId(start, end, { counter: index, prefix: 'L' }, rawEdge.id),
|
|
1291
|
+
isUserDefinedId: rawEdge.isUserDefinedId,
|
|
1292
|
+
start,
|
|
1293
|
+
end,
|
|
1294
|
+
type: rawEdge.type ?? 'normal',
|
|
1295
|
+
label: rawEdge.text,
|
|
1296
|
+
labelType: rawEdge.labelType,
|
|
1297
|
+
labelpos: 'c',
|
|
1298
|
+
thickness: rawEdge.stroke,
|
|
1299
|
+
minlen: rawEdge.length,
|
|
1300
|
+
classes:
|
|
1301
|
+
rawEdge?.stroke === 'invisible'
|
|
1302
|
+
? ''
|
|
1303
|
+
: 'edge-thickness-normal edge-pattern-solid flowchart-link',
|
|
1304
|
+
arrowTypeStart:
|
|
1305
|
+
rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open'
|
|
1306
|
+
? 'none'
|
|
1307
|
+
: arrowTypeStart,
|
|
1308
|
+
arrowTypeEnd:
|
|
1309
|
+
rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open' ? 'none' : arrowTypeEnd,
|
|
1310
|
+
arrowheadStyle: 'fill: #333',
|
|
1311
|
+
cssCompiledStyles: this.getCompiledStyles(rawEdge.classes),
|
|
1312
|
+
labelStyle: styles,
|
|
1313
|
+
style: styles,
|
|
1314
|
+
pattern: rawEdge.stroke,
|
|
1315
|
+
look: config.look,
|
|
1316
|
+
animate: rawEdge.animate,
|
|
1317
|
+
animation: rawEdge.animation,
|
|
1318
|
+
curve: rawEdge.interpolate || this.edges.defaultInterpolate || config.flowchart?.curve,
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
edges.push(edge);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
return { nodes, edges, other: {}, config };
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
public defaultConfig() {
|
|
1328
|
+
return defaultConfig.flowchart;
|
|
1329
|
+
}
|
|
1330
|
+
public setAccTitle = setAccTitle;
|
|
1331
|
+
public setAccDescription = setAccDescription;
|
|
1332
|
+
public setDiagramTitle = setDiagramTitle;
|
|
1333
|
+
public getAccTitle = getAccTitle;
|
|
1334
|
+
public getAccDescription = getAccDescription;
|
|
1335
|
+
public getDiagramTitle = getDiagramTitle;
|
|
1336
|
+
}
|