From 9ef6090c8c70832e747b60b88aff9e7c557138fd Mon Sep 17 00:00:00 2001 From: Saurabh Gore Date: Tue, 31 Dec 2024 17:40:52 +0530 Subject: [PATCH] convert flowDb to class. --- .../src/diagrams/flowchart/flowDb.spec.ts | 7 +- .../mermaid/src/diagrams/flowchart/flowDb.ts | 1921 ++++++++--------- .../src/diagrams/flowchart/flowDiagram.ts | 8 +- .../flowchart/flowRenderer-v3-unified.ts | 3 +- .../flowchart/parser/flow-arrows.spec.js | 4 +- .../flowchart/parser/flow-comments.spec.js | 4 +- .../flowchart/parser/flow-direction.spec.js | 4 +- .../flowchart/parser/flow-edges.spec.js | 4 +- .../flowchart/parser/flow-huge.spec.js | 4 +- .../parser/flow-interactions.spec.js | 4 +- .../flowchart/parser/flow-lines.spec.js | 4 +- .../flowchart/parser/flow-md-string.spec.js | 4 +- .../flowchart/parser/flow-node-data.spec.js | 4 +- .../flowchart/parser/flow-singlenode.spec.js | 4 +- .../flowchart/parser/flow-style.spec.js | 4 +- .../flowchart/parser/flow-text.spec.js | 4 +- .../parser/flow-vertice-chaining.spec.js | 4 +- .../diagrams/flowchart/parser/flow.spec.js | 4 +- .../flowchart/parser/subgraph.spec.js | 4 +- 19 files changed, 991 insertions(+), 1008 deletions(-) diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts index 5983bf04c..841a9d9fe 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts @@ -1,9 +1,11 @@ -import flowDb from './flowDb.js'; +import { FlowDb } from './flowDb.js'; import type { FlowSubGraph } from './types.js'; describe('flow db subgraphs', () => { + let flowDb: FlowDb; let subgraphs: FlowSubGraph[]; beforeEach(() => { + flowDb = new FlowDb(); subgraphs = [ { nodes: ['a', 'b', 'c', 'e'] }, { nodes: ['f', 'g', 'h'] }, @@ -44,8 +46,9 @@ describe('flow db subgraphs', () => { }); describe('flow db addClass', () => { + let flowDb: FlowDb; beforeEach(() => { - flowDb.clear(); + flowDb = new FlowDb(); }); it('should detect many classes', () => { flowDb.addClass('a,b', ['stroke-width: 8px']); diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts index 1dbc789c9..8b88fc723 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts @@ -27,1037 +27,1016 @@ import type { import type { NodeMetaData } from '../../types.js'; const MERMAID_DOM_ID_PREFIX = 'flowchart-'; -let vertexCounter = 0; -let config = getConfig(); -let vertices = new Map(); -let edges: FlowEdge[] & { defaultInterpolate?: string; defaultStyle?: string[] } = []; -let classes = new Map(); -let subGraphs: FlowSubGraph[] = []; -let subGraphLookup = new Map(); -let tooltips = new Map(); -let subCount = 0; -let firstGraphFlag = true; -let direction: string; -let version: string; // As in graph +export class FlowDb { + private vertexCounter = 0; + private config = getConfig(); + private vertices = new Map(); + private edges: FlowEdge[] & { defaultInterpolate?: string; defaultStyle?: string[] } = []; + private classes = new Map(); + private subGraphs: FlowSubGraph[] = []; + private subGraphLookup = new Map(); + private tooltips = new Map(); + private subCount = 0; + private firstGraphFlag = true; + private direction: string | undefined; -// Functions to be run after graph rendering -let funs: ((element: Element) => void)[] = []; // cspell:ignore funs + private version: string | undefined; // As in graph -const sanitizeText = (txt: string) => common.sanitizeText(txt, config); + private secCount = -1; + private posCrossRef: number[] = []; -/** - * Function to lookup domId from id in the graph definition. - * - * @param id - id of the node - */ -export const lookUpDomId = function (id: string) { - for (const vertex of vertices.values()) { - if (vertex.id === id) { - return vertex.domId; + // Functions to be run after graph rendering + private funs: ((element: Element) => void)[] = []; // cspell:ignore funs + + constructor() { + this.funs.push(this.setupToolTips); + this.clear(); + this.setGen('gen-2'); + } + + private sanitizeText = (txt: string) => common.sanitizeText(txt, this.config); + + /** + * Function to lookup domId from id in the graph definition. + * + * @param id - id of the node + */ + public lookUpDomId = (id: string) => { + for (const vertex of this.vertices.values()) { + if (vertex.id === id) { + return vertex.domId; + } } - } - return id; -}; + return id; + }; -/** - * Function called by parser when a node definition has been found - */ -export const addVertex = function ( - id: string, - textObj: FlowText, - type: FlowVertexTypeParam, - style: string[], - classes: string[], - dir: string, - props = {}, - shapeData: any -) { - // console.log('addVertex', id, shapeData); - if (!id || id.trim().length === 0) { - return; - } - let txt; - - let vertex = vertices.get(id); - if (vertex === undefined) { - vertex = { - id, - labelType: 'text', - domId: MERMAID_DOM_ID_PREFIX + id + '-' + vertexCounter, - styles: [], - classes: [], - }; - vertices.set(id, vertex); - } - vertexCounter++; - - if (textObj !== undefined) { - config = getConfig(); - txt = sanitizeText(textObj.text.trim()); - vertex.labelType = textObj.type; - // strip quotes if string starts and ends with a quote - if (txt.startsWith('"') && txt.endsWith('"')) { - txt = txt.substring(1, txt.length - 1); + /** + * Function called by parser when a node definition has been found + */ + public addVertex = ( + id: string, + textObj: FlowText, + type: FlowVertexTypeParam, + style: string[], + classes: string[], + dir: string, + props = {}, + shapeData: any + ) => { + // console.log('addVertex', id, shapeData); + if (!id || id.trim().length === 0) { + return; } - vertex.text = txt; - } else { - if (vertex.text === undefined) { - vertex.text = id; - } - } - if (type !== undefined) { - vertex.type = type; - } - if (style !== undefined && style !== null) { - style.forEach(function (s) { - vertex.styles.push(s); - }); - } - if (classes !== undefined && classes !== null) { - classes.forEach(function (s) { - vertex.classes.push(s); - }); - } - if (dir !== undefined) { - vertex.dir = dir; - } - if (vertex.props === undefined) { - vertex.props = props; - } else if (props !== undefined) { - Object.assign(vertex.props, props); - } + let txt; - if (shapeData !== undefined) { - let yamlData; - // detect if shapeData contains a newline character - // console.log('shapeData', shapeData); - if (!shapeData.includes('\n')) { - // console.log('yamlData shapeData has no new lines', shapeData); - yamlData = '{\n' + shapeData + '\n}'; + let vertex = this.vertices.get(id); + if (vertex === undefined) { + vertex = { + id, + labelType: 'text', + domId: MERMAID_DOM_ID_PREFIX + id + '-' + this.vertexCounter, + styles: [], + classes: [], + }; + this.vertices.set(id, vertex); + } + this.vertexCounter++; + + if (textObj !== undefined) { + this.config = getConfig(); + txt = this.sanitizeText(textObj.text.trim()); + vertex.labelType = textObj.type; + // strip quotes if string starts and ends with a quote + if (txt.startsWith('"') && txt.endsWith('"')) { + txt = txt.substring(1, txt.length - 1); + } + vertex.text = txt; } else { - // console.log('yamlData shapeData has new lines', shapeData); - yamlData = shapeData + '\n'; - } - // console.log('yamlData', yamlData); - const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData; - if (doc.shape) { - if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) { - throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`); - } else if (!isValidShape(doc.shape)) { - throw new Error(`No such shape: ${doc.shape}.`); - } - vertex.type = doc?.shape; - } - - if (doc?.label) { - vertex.text = doc?.label; - } - if (doc?.icon) { - vertex.icon = doc?.icon; - if (!doc.label?.trim() && vertex.text === id) { - vertex.text = ''; + if (vertex.text === undefined) { + vertex.text = id; } } - if (doc?.form) { - vertex.form = doc?.form; + if (type !== undefined) { + vertex.type = type; } - if (doc?.pos) { - vertex.pos = doc?.pos; + if (style !== undefined && style !== null) { + style.forEach((s) => { + vertex.styles.push(s); + }); } - if (doc?.img) { - vertex.img = doc?.img; - if (!doc.label?.trim() && vertex.text === id) { - vertex.text = ''; + if (classes !== undefined && classes !== null) { + classes.forEach((s) => { + vertex.classes.push(s); + }); + } + if (dir !== undefined) { + vertex.dir = dir; + } + if (vertex.props === undefined) { + vertex.props = props; + } else if (props !== undefined) { + Object.assign(vertex.props, props); + } + + if (shapeData !== undefined) { + let yamlData; + // detect if shapeData contains a newline character + // console.log('shapeData', shapeData); + if (!shapeData.includes('\n')) { + // console.log('yamlData shapeData has no new lines', shapeData); + yamlData = '{\n' + shapeData + '\n}'; + } else { + // console.log('yamlData shapeData has new lines', shapeData); + yamlData = shapeData + '\n'; + } + // console.log('yamlData', yamlData); + const doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData; + if (doc.shape) { + if (doc.shape !== doc.shape.toLowerCase() || doc.shape.includes('_')) { + throw new Error(`No such shape: ${doc.shape}. Shape names should be lowercase.`); + } else if (!isValidShape(doc.shape)) { + throw new Error(`No such shape: ${doc.shape}.`); + } + vertex.type = doc?.shape; + } + + if (doc?.label) { + vertex.text = doc?.label; + } + if (doc?.icon) { + vertex.icon = doc?.icon; + if (!doc.label?.trim() && vertex.text === id) { + vertex.text = ''; + } + } + if (doc?.form) { + vertex.form = doc?.form; + } + if (doc?.pos) { + vertex.pos = doc?.pos; + } + if (doc?.img) { + vertex.img = doc?.img; + if (!doc.label?.trim() && vertex.text === id) { + vertex.text = ''; + } + } + if (doc?.constraint) { + vertex.constraint = doc.constraint; + } + if (doc.w) { + vertex.assetWidth = Number(doc.w); + } + if (doc.h) { + vertex.assetHeight = Number(doc.h); } } - if (doc?.constraint) { - vertex.constraint = doc.constraint; + }; + + /** + * Function called by parser when a link/edge definition has been found + * + */ + private addSingleLink = (_start: string, _end: string, type: any) => { + const start = _start; + const end = _end; + + const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' }; + log.info('abc78 Got edge...', edge); + const linkTextObj = type.text; + + if (linkTextObj !== undefined) { + edge.text = this.sanitizeText(linkTextObj.text.trim()); + + // strip quotes if string starts and ends with a quote + if (edge.text.startsWith('"') && edge.text.endsWith('"')) { + edge.text = edge.text.substring(1, edge.text.length - 1); + } + edge.labelType = linkTextObj.type; } - if (doc.w) { - vertex.assetWidth = Number(doc.w); + + if (type !== undefined) { + edge.type = type.type; + edge.stroke = type.stroke; + edge.length = type.length > 10 ? 10 : type.length; } - if (doc.h) { - vertex.assetHeight = Number(doc.h); - } - } -}; -/** - * Function called by parser when a link/edge definition has been found - * - */ -export const addSingleLink = function (_start: string, _end: string, type: any) { - const start = _start; - const end = _end; - - const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' }; - log.info('abc78 Got edge...', edge); - const linkTextObj = type.text; - - if (linkTextObj !== undefined) { - edge.text = sanitizeText(linkTextObj.text.trim()); - - // strip quotes if string starts and ends with a quote - if (edge.text.startsWith('"') && edge.text.endsWith('"')) { - edge.text = edge.text.substring(1, edge.text.length - 1); - } - edge.labelType = linkTextObj.type; - } - - if (type !== undefined) { - edge.type = type.type; - edge.stroke = type.stroke; - edge.length = type.length > 10 ? 10 : type.length; - } - - if (edges.length < (config.maxEdges ?? 500)) { - log.info('Pushing edge...'); - edges.push(edge); - } else { - throw new Error( - `Edge limit exceeded. ${edges.length} edges found, but the limit is ${config.maxEdges}. + if (this.edges.length < (this.config.maxEdges ?? 500)) { + log.info('Pushing edge...'); + this.edges.push(edge); + } else { + throw new Error( + `Edge limit exceeded. ${this.edges.length} edges found, but the limit is ${this.config.maxEdges}. Initialize mermaid with maxEdges set to a higher number to allow more edges. You cannot set this config via configuration inside the diagram as it is a secure config. You have to call mermaid.initialize.` - ); - } -}; - -export const addLink = function (_start: string[], _end: string[], type: unknown) { - log.info('addLink', _start, _end, type); - for (const start of _start) { - for (const end of _end) { - addSingleLink(start, end, type); - } - } -}; - -/** - * Updates a link's line interpolation algorithm - * - */ -export const updateLinkInterpolate = function ( - positions: ('default' | number)[], - interpolate: string -) { - positions.forEach(function (pos) { - if (pos === 'default') { - edges.defaultInterpolate = interpolate; - } else { - edges[pos].interpolate = interpolate; - } - }); -}; - -/** - * Updates a link with a style - * - */ -export const updateLink = function (positions: ('default' | number)[], style: string[]) { - positions.forEach(function (pos) { - if (typeof pos === 'number' && pos >= edges.length) { - throw new Error( - `The index ${pos} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${ - edges.length - 1 - }. (Help: Ensure that the index is within the range of existing edges.)` ); } - if (pos === 'default') { - edges.defaultStyle = style; - } else { - // if (utils.isSubstringInArray('fill', style) === -1) { - // style.push('fill:none'); - // } - edges[pos].style = style; - // if edges[pos].style does have fill not set, set it to none - if ( - (edges[pos]?.style?.length ?? 0) > 0 && - !edges[pos]?.style?.some((s) => s?.startsWith('fill')) - ) { - edges[pos]?.style?.push('fill:none'); + }; + + public addLink = (_start: string[], _end: string[], type: unknown) => { + log.info('addLink', _start, _end, type); + for (const start of _start) { + for (const end of _end) { + this.addSingleLink(start, end, type); } } - }); -}; + }; -export const addClass = function (ids: string, style: string[]) { - ids.split(',').forEach(function (id) { - let classNode = classes.get(id); - if (classNode === undefined) { - classNode = { id, styles: [], textStyles: [] }; - classes.set(id, classNode); - } - - if (style !== undefined && style !== null) { - style.forEach(function (s) { - if (/color/.exec(s)) { - const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill'); - classNode.textStyles.push(newStyle); - } - classNode.styles.push(s); - }); - } - }); -}; - -/** - * Called by parser when a graph definition is found, stores the direction of the chart. - * - */ -export const setDirection = function (dir: string) { - direction = dir; - if (/.*/.exec(direction)) { - direction = 'LR'; - } - if (/.*v/.exec(direction)) { - direction = 'TB'; - } - if (direction === 'TD') { - direction = 'TB'; - } -}; - -/** - * Called by parser when a special node is found, e.g. a clickable element. - * - * @param ids - Comma separated list of ids - * @param className - Class to add - */ -export const setClass = function (ids: string, className: string) { - for (const id of ids.split(',')) { - const vertex = vertices.get(id); - if (vertex) { - vertex.classes.push(className); - } - const subGraph = subGraphLookup.get(id); - if (subGraph) { - subGraph.classes.push(className); - } - } -}; - -const setTooltip = function (ids: string, tooltip: string) { - if (tooltip === undefined) { - return; - } - tooltip = sanitizeText(tooltip); - for (const id of ids.split(',')) { - tooltips.set(version === 'gen-1' ? lookUpDomId(id) : id, tooltip); - } -}; - -const setClickFun = function (id: string, functionName: string, functionArgs: string) { - const domId = lookUpDomId(id); - // if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; - if (getConfig().securityLevel !== 'loose') { - return; - } - if (functionName === undefined) { - return; - } - let argList: string[] = []; - if (typeof functionArgs === 'string') { - /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */ - argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/); - for (let i = 0; i < argList.length; i++) { - let item = argList[i].trim(); - /* Removes all double quotes at the start and end of an argument */ - /* This preserves all starting and ending whitespace inside */ - if (item.startsWith('"') && item.endsWith('"')) { - item = item.substr(1, item.length - 2); + /** + * Updates a link's line interpolation algorithm + * + */ + public updateLinkInterpolate = (positions: ('default' | number)[], interpolate: string) => { + positions.forEach((pos) => { + if (pos === 'default') { + this.edges.defaultInterpolate = interpolate; + } else { + this.edges[pos].interpolate = interpolate; } - argList[i] = item; - } - } + }); + }; - /* if no arguments passed into callback, default to passing in id */ - if (argList.length === 0) { - argList.push(id); - } - - const vertex = vertices.get(id); - if (vertex) { - vertex.haveCallback = true; - funs.push(function () { - const elem = document.querySelector(`[id="${domId}"]`); - if (elem !== null) { - elem.addEventListener( - 'click', - function () { - utils.runFunc(functionName, ...argList); - }, - false + /** + * Updates a link with a style + * + */ + public updateLink = (positions: ('default' | number)[], style: string[]) => { + positions.forEach((pos) => { + if (typeof pos === 'number' && pos >= this.edges.length) { + throw new Error( + `The index ${pos} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${ + this.edges.length - 1 + }. (Help: Ensure that the index is within the range of existing edges.)` ); } - }); - } -}; - -/** - * Called by parser when a link is found. Adds the URL to the vertex data. - * - * @param ids - Comma separated list of ids - * @param linkStr - URL to create a link for - * @param target - Target attribute for the link - */ -export const setLink = function (ids: string, linkStr: string, target: string) { - ids.split(',').forEach(function (id) { - const vertex = vertices.get(id); - if (vertex !== undefined) { - vertex.link = utils.formatUrl(linkStr, config); - vertex.linkTarget = target; - } - }); - setClass(ids, 'clickable'); -}; - -export const getTooltip = function (id: string) { - return tooltips.get(id); -}; - -/** - * Called by parser when a click definition is found. Registers an event handler. - * - * @param ids - Comma separated list of ids - * @param functionName - Function to be called on click - * @param functionArgs - Arguments to be passed to the function - */ -export const setClickEvent = function (ids: string, functionName: string, functionArgs: string) { - ids.split(',').forEach(function (id) { - setClickFun(id, functionName, functionArgs); - }); - setClass(ids, 'clickable'); -}; - -export const bindFunctions = function (element: Element) { - funs.forEach(function (fun) { - fun(element); - }); -}; -export const getDirection = function () { - return direction.trim(); -}; -/** - * Retrieval function for fetching the found nodes after parsing has completed. - * - */ -export const getVertices = function () { - return vertices; -}; - -/** - * Retrieval function for fetching the found links after parsing has completed. - * - */ -export const getEdges = function () { - return edges; -}; - -/** - * Retrieval function for fetching the found class definitions after parsing has completed. - * - */ -export const getClasses = function () { - return classes; -}; - -const setupToolTips = function (element: Element) { - let tooltipElem = select('.mermaidTooltip'); - // @ts-ignore TODO: fix this - if ((tooltipElem._groups || tooltipElem)[0][0] === null) { - // @ts-ignore TODO: fix this - tooltipElem = select('body').append('div').attr('class', 'mermaidTooltip').style('opacity', 0); - } - - const svg = select(element).select('svg'); - - const nodes = svg.selectAll('g.node'); - nodes - .on('mouseover', function () { - const el = select(this); - const title = el.attr('title'); - - // Don't try to draw a tooltip if no data is provided - if (title === null) { - return; - } - const rect = (this as Element)?.getBoundingClientRect(); - - tooltipElem.transition().duration(200).style('opacity', '.9'); - tooltipElem - .text(el.attr('title')) - .style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px') - .style('top', window.scrollY + rect.bottom + 'px'); - tooltipElem.html(tooltipElem.html().replace(/<br\/>/g, '
')); - el.classed('hover', true); - }) - .on('mouseout', function () { - tooltipElem.transition().duration(500).style('opacity', 0); - const el = select(this); - el.classed('hover', false); - }); -}; -funs.push(setupToolTips); - -/** - * Clears the internal graph db so that a new graph can be parsed. - * - */ -export const clear = function (ver = 'gen-1') { - vertices = new Map(); - classes = new Map(); - edges = []; - funs = [setupToolTips]; - subGraphs = []; - subGraphLookup = new Map(); - subCount = 0; - tooltips = new Map(); - firstGraphFlag = true; - version = ver; - config = getConfig(); - commonClear(); -}; - -export const setGen = (ver: string) => { - version = ver || 'gen-2'; -}; - -export const defaultStyle = function () { - return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;'; -}; - -export const addSubGraph = function ( - _id: { text: string }, - list: string[], - _title: { text: string; type: string } -) { - let id: string | undefined = _id.text.trim(); - let title = _title.text; - if (_id === _title && /\s/.exec(_title.text)) { - id = undefined; - } - - function uniq(a: any[]) { - const prims: any = { boolean: {}, number: {}, string: {} }; - const objs: any[] = []; - - let dir; // = undefined; direction.trim(); - const nodeList = a.filter(function (item) { - const type = typeof item; - if (item.stmt && item.stmt === 'dir') { - dir = item.value; - return false; - } - if (item.trim() === '') { - return false; - } - if (type in prims) { - return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true); + if (pos === 'default') { + this.edges.defaultStyle = style; } else { - return objs.includes(item) ? false : objs.push(item); + // if (utils.isSubstringInArray('fill', style) === -1) { + // style.push('fill:none'); + // } + this.edges[pos].style = style; + // if edges[pos].style does have fill not set, set it to none + if ( + (this.edges[pos]?.style?.length ?? 0) > 0 && + !this.edges[pos]?.style?.some((s) => s?.startsWith('fill')) + ) { + this.edges[pos]?.style?.push('fill:none'); + } } }); - return { nodeList, dir }; - } - - const { nodeList, dir } = uniq(list.flat()); - if (version === 'gen-1') { - for (let i = 0; i < nodeList.length; i++) { - nodeList[i] = lookUpDomId(nodeList[i]); - } - } - - id = id ?? 'subGraph' + subCount; - title = title || ''; - title = sanitizeText(title); - subCount = subCount + 1; - const subGraph = { - id: id, - nodes: nodeList, - title: title.trim(), - classes: [], - dir, - labelType: _title.type, }; - log.info('Adding', subGraph.id, subGraph.nodes, subGraph.dir); + public addClass = (ids: string, style: string[]) => { + ids.split(',').forEach((id) => { + let classNode = this.classes.get(id); + if (classNode === undefined) { + classNode = { id, styles: [], textStyles: [] }; + this.classes.set(id, classNode); + } - // Remove the members in the new subgraph if they already belong to another subgraph - subGraph.nodes = makeUniq(subGraph, subGraphs).nodes; - subGraphs.push(subGraph); - subGraphLookup.set(id, subGraph); - return id; -}; + if (style !== undefined && style !== null) { + style.forEach((s) => { + if (/color/.exec(s)) { + const newStyle = s.replace('fill', 'bgFill'); // .replace('color', 'fill'); + classNode.textStyles.push(newStyle); + } + classNode.styles.push(s); + }); + } + }); + }; -const getPosForId = function (id: string) { - for (const [i, subGraph] of subGraphs.entries()) { - if (subGraph.id === id) { - return i; + /** + * Called by parser when a graph definition is found, stores the direction of the chart. + * + */ + public setDirection = (dir: string) => { + this.direction = dir; + if (/.* 2000) { + if (/.*\^/.exec(this.direction)) { + this.direction = 'BT'; + } + if (/.*>/.exec(this.direction)) { + this.direction = 'LR'; + } + if (/.*v/.exec(this.direction)) { + this.direction = 'TB'; + } + if (this.direction === 'TD') { + this.direction = 'TB'; + } + }; + + /** + * Called by parser when a special node is found, e.g. a clickable element. + * + * @param ids - Comma separated list of ids + * @param className - Class to add + */ + public setClass = (ids: string, className: string) => { + for (const id of ids.split(',')) { + const vertex = this.vertices.get(id); + if (vertex) { + vertex.classes.push(className); + } + const subGraph = this.subGraphLookup.get(id); + if (subGraph) { + subGraph.classes.push(className); + } + } + }; + + public setTooltip = (ids: string, tooltip: string) => { + if (tooltip === undefined) { + return; + } + tooltip = this.sanitizeText(tooltip); + for (const id of ids.split(',')) { + this.tooltips.set(this.version === 'gen-1' ? this.lookUpDomId(id) : id, tooltip); + } + }; + + private setClickFun = (id: string, functionName: string, functionArgs: string) => { + const domId = this.lookUpDomId(id); + // if (_id[0].match(/\d/)) id = MERMAID_DOM_ID_PREFIX + id; + if (getConfig().securityLevel !== 'loose') { + return; + } + if (functionName === undefined) { + return; + } + let argList: string[] = []; + if (typeof functionArgs === 'string') { + /* Splits functionArgs by ',', ignoring all ',' in double quoted strings */ + argList = functionArgs.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/); + for (let i = 0; i < argList.length; i++) { + let item = argList[i].trim(); + /* Removes all double quotes at the start and end of an argument */ + /* This preserves all starting and ending whitespace inside */ + if (item.startsWith('"') && item.endsWith('"')) { + item = item.substr(1, item.length - 2); + } + argList[i] = item; + } + } + + /* if no arguments passed into callback, default to passing in id */ + if (argList.length === 0) { + argList.push(id); + } + + const vertex = this.vertices.get(id); + if (vertex) { + vertex.haveCallback = true; + this.funs.push(() => { + const elem = document.querySelector(`[id="${domId}"]`); + if (elem !== null) { + elem.addEventListener( + 'click', + () => { + utils.runFunc(functionName, ...argList); + }, + false + ); + } + }); + } + }; + + /** + * Called by parser when a link is found. Adds the URL to the vertex data. + * + * @param ids - Comma separated list of ids + * @param linkStr - URL to create a link for + * @param target - Target attribute for the link + */ + public setLink = (ids: string, linkStr: string, target: string) => { + ids.split(',').forEach((id) => { + const vertex = this.vertices.get(id); + if (vertex !== undefined) { + vertex.link = utils.formatUrl(linkStr, this.config); + vertex.linkTarget = target; + } + }); + this.setClass(ids, 'clickable'); + }; + + public getTooltip = (id: string) => { + return this.tooltips.get(id); + }; + + /** + * Called by parser when a click definition is found. Registers an event handler. + * + * @param ids - Comma separated list of ids + * @param functionName - Function to be called on click + * @param functionArgs - Arguments to be passed to the function + */ + public setClickEvent = (ids: string, functionName: string, functionArgs: string) => { + ids.split(',').forEach((id) => { + this.setClickFun(id, functionName, functionArgs); + }); + this.setClass(ids, 'clickable'); + }; + + public bindFunctions = (element: Element) => { + this.funs.forEach((fun) => { + fun(element); + }); + }; + public getDirection = () => { + return this.direction?.trim(); + }; + /** + * Retrieval function for fetching the found nodes after parsing has completed. + * + */ + public getVertices = () => { + return this.vertices; + }; + + /** + * Retrieval function for fetching the found links after parsing has completed. + * + */ + public getEdges = () => { + return this.edges; + }; + + /** + * Retrieval function for fetching the found class definitions after parsing has completed. + * + */ + public getClasses = () => { + return this.classes; + }; + + private setupToolTips = (element: Element) => { + let tooltipElem = select('.mermaidTooltip'); + // @ts-ignore TODO: fix this + if ((tooltipElem._groups || tooltipElem)[0][0] === null) { + // @ts-ignore TODO: fix this + tooltipElem = select('body') + .append('div') + .attr('class', 'mermaidTooltip') + .style('opacity', 0); + } + + const svg = select(element).select('svg'); + + const nodes = svg.selectAll('g.node'); + nodes + .on('mouseover', (e: MouseEvent) => { + const el = select(e.currentTarget as Element); + const title = el.attr('title'); + + // Don't try to draw a tooltip if no data is provided + if (title === null) { + return; + } + const rect = (e.currentTarget as Element)?.getBoundingClientRect(); + + tooltipElem.transition().duration(200).style('opacity', '.9'); + tooltipElem + .text(el.attr('title')) + .style('left', window.scrollX + rect.left + (rect.right - rect.left) / 2 + 'px') + .style('top', window.scrollY + rect.bottom + 'px'); + tooltipElem.html(tooltipElem.html().replace(/<br\/>/g, '
')); + el.classed('hover', true); + }) + .on('mouseout', (e: MouseEvent) => { + tooltipElem.transition().duration(500).style('opacity', 0); + const el = select(e.currentTarget as Element); + el.classed('hover', false); + }); + }; + + /** + * Clears the internal graph db so that a new graph can be parsed. + * + */ + public clear = (ver = 'gen-1') => { + this.vertices = new Map(); + this.classes = new Map(); + this.edges = []; + this.funs = [this.setupToolTips]; + this.subGraphs = []; + this.subGraphLookup = new Map(); + this.subCount = 0; + this.tooltips = new Map(); + this.firstGraphFlag = true; + this.version = ver; + this.config = getConfig(); + commonClear(); + }; + + public setGen = (ver: string) => { + this.version = ver || 'gen-2'; + }; + + public defaultStyle = () => { + return 'fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;'; + }; + + public addSubGraph = ( + _id: { text: string }, + list: string[], + _title: { text: string; type: string } + ) => { + let id: string | undefined = _id.text.trim(); + let title = _title.text; + if (_id === _title && /\s/.exec(_title.text)) { + id = undefined; + } + + const uniq = (a: any[]) => { + const prims: any = { boolean: {}, number: {}, string: {} }; + const objs: any[] = []; + + let dir; // = undefined; direction.trim(); + const nodeList = a.filter(function (item) { + const type = typeof item; + if (item.stmt && item.stmt === 'dir') { + dir = item.value; + return false; + } + if (item.trim() === '') { + return false; + } + if (type in prims) { + return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true); + } else { + return objs.includes(item) ? false : objs.push(item); + } + }); + return { nodeList, dir }; + }; + + const { nodeList, dir } = uniq(list.flat()); + if (this.version === 'gen-1') { + for (let i = 0; i < nodeList.length; i++) { + nodeList[i] = this.lookUpDomId(nodeList[i]); + } + } + + id = id ?? 'subGraph' + this.subCount; + title = title || ''; + title = this.sanitizeText(title); + this.subCount = this.subCount + 1; + const subGraph = { + id: id, + nodes: nodeList, + title: title.trim(), + classes: [], + dir, + labelType: _title.type, + }; + + log.info('Adding', subGraph.id, subGraph.nodes, subGraph.dir); + + // Remove the members in the new subgraph if they already belong to another subgraph + subGraph.nodes = this.makeUniq(subGraph, this.subGraphs).nodes; + this.subGraphs.push(subGraph); + this.subGraphLookup.set(id, subGraph); + return id; + }; + + private getPosForId = (id: string) => { + for (const [i, subGraph] of this.subGraphs.entries()) { + if (subGraph.id === id) { + return i; + } + } + return -1; + }; + + private indexNodes2 = (id: string, pos: number): { result: boolean; count: number } => { + const nodes = this.subGraphs[pos].nodes; + this.secCount = this.secCount + 1; + if (this.secCount > 2000) { + return { + result: false, + count: 0, + }; + } + this.posCrossRef[this.secCount] = pos; + // Check if match + if (this.subGraphs[pos].id === id) { + return { + result: true, + count: 0, + }; + } + + let count = 0; + let posCount = 1; + while (count < nodes.length) { + const childPos = this.getPosForId(nodes[count]); + // Ignore regular nodes (pos will be -1) + if (childPos >= 0) { + const res = this.indexNodes2(id, childPos); + if (res.result) { + return { + result: true, + count: posCount + res.count, + }; + } else { + posCount = posCount + res.count; + } + } + count = count + 1; + } + return { result: false, - count: 0, + count: posCount, }; - } - posCrossRef[secCount] = pos; - // Check if match - if (subGraphs[pos].id === id) { - return { - result: true, - count: 0, - }; - } - - let count = 0; - let posCount = 1; - while (count < nodes.length) { - const childPos = getPosForId(nodes[count]); - // Ignore regular nodes (pos will be -1) - if (childPos >= 0) { - const res = indexNodes2(id, childPos); - if (res.result) { - return { - result: true, - count: posCount + res.count, - }; - } else { - posCount = posCount + res.count; - } - } - count = count + 1; - } - - return { - result: false, - count: posCount, }; -}; -export const getDepthFirstPos = function (pos: number) { - return posCrossRef[pos]; -}; -export const indexNodes = function () { - secCount = -1; - if (subGraphs.length > 0) { - indexNodes2('none', subGraphs.length - 1); - } -}; - -export const getSubGraphs = function () { - return subGraphs; -}; - -export const firstGraph = () => { - if (firstGraphFlag) { - firstGraphFlag = false; - return true; - } - return false; -}; - -const destructStartLink = (_str: string): FlowLink => { - let str = _str.trim(); - let type = 'arrow_open'; - - switch (str[0]) { - case '<': - type = 'arrow_point'; - str = str.slice(1); - break; - case 'x': - type = 'arrow_cross'; - str = str.slice(1); - break; - case 'o': - type = 'arrow_circle'; - str = str.slice(1); - break; - } - - let stroke = 'normal'; - - if (str.includes('=')) { - stroke = 'thick'; - } - - if (str.includes('.')) { - stroke = 'dotted'; - } - - return { type, stroke }; -}; - -const countChar = (char: string, str: string) => { - const length = str.length; - let count = 0; - for (let i = 0; i < length; ++i) { - if (str[i] === char) { - ++count; + public getDepthFirstPos = (pos: number) => { + return this.posCrossRef[pos]; + }; + public indexNodes = () => { + this.secCount = -1; + if (this.subGraphs.length > 0) { + this.indexNodes2('none', this.subGraphs.length - 1); } - } - return count; -}; + }; -const destructEndLink = (_str: string) => { - const str = _str.trim(); - let line = str.slice(0, -1); - let type = 'arrow_open'; + public getSubGraphs = () => { + return this.subGraphs; + }; - switch (str.slice(-1)) { - case 'x': - type = 'arrow_cross'; - if (str.startsWith('x')) { - type = 'double_' + type; - line = line.slice(1); - } - break; - case '>': - type = 'arrow_point'; - if (str.startsWith('<')) { - type = 'double_' + type; - line = line.slice(1); - } - break; - case 'o': - type = 'arrow_circle'; - if (str.startsWith('o')) { - type = 'double_' + type; - line = line.slice(1); - } - break; - } + public firstGraph = () => { + if (this.firstGraphFlag) { + this.firstGraphFlag = false; + return true; + } + return false; + }; - let stroke = 'normal'; - let length = line.length - 1; + private destructStartLink = (_str: string): FlowLink => { + let str = _str.trim(); + let type = 'arrow_open'; - if (line.startsWith('=')) { - stroke = 'thick'; - } - - if (line.startsWith('~')) { - stroke = 'invisible'; - } - - const dots = countChar('.', line); - - if (dots) { - stroke = 'dotted'; - length = dots; - } - - return { type, stroke, length }; -}; - -export const destructLink = (_str: string, _startStr: string) => { - const info = destructEndLink(_str); - let startInfo; - if (_startStr) { - startInfo = destructStartLink(_startStr); - - if (startInfo.stroke !== info.stroke) { - return { type: 'INVALID', stroke: 'INVALID' }; + switch (str[0]) { + case '<': + type = 'arrow_point'; + str = str.slice(1); + break; + case 'x': + type = 'arrow_cross'; + str = str.slice(1); + break; + case 'o': + type = 'arrow_circle'; + str = str.slice(1); + break; } - if (startInfo.type === 'arrow_open') { - // -- xyz --> - take arrow type from ending - startInfo.type = info.type; - } else { - // x-- xyz --> - not supported - if (startInfo.type !== info.type) { + let stroke = 'normal'; + + if (str.includes('=')) { + stroke = 'thick'; + } + + if (str.includes('.')) { + stroke = 'dotted'; + } + + return { type, stroke }; + }; + + private countChar = (char: string, str: string) => { + const length = str.length; + let count = 0; + for (let i = 0; i < length; ++i) { + if (str[i] === char) { + ++count; + } + } + return count; + }; + + private destructEndLink = (_str: string) => { + const str = _str.trim(); + let line = str.slice(0, -1); + let type = 'arrow_open'; + + switch (str.slice(-1)) { + case 'x': + type = 'arrow_cross'; + if (str.startsWith('x')) { + type = 'double_' + type; + line = line.slice(1); + } + break; + case '>': + type = 'arrow_point'; + if (str.startsWith('<')) { + type = 'double_' + type; + line = line.slice(1); + } + break; + case 'o': + type = 'arrow_circle'; + if (str.startsWith('o')) { + type = 'double_' + type; + line = line.slice(1); + } + break; + } + + let stroke = 'normal'; + let length = line.length - 1; + + if (line.startsWith('=')) { + stroke = 'thick'; + } + + if (line.startsWith('~')) { + stroke = 'invisible'; + } + + const dots = this.countChar('.', line); + + if (dots) { + stroke = 'dotted'; + length = dots; + } + + return { type, stroke, length }; + }; + + public destructLink = (_str: string, _startStr: string) => { + const info = this.destructEndLink(_str); + let startInfo; + if (_startStr) { + startInfo = this.destructStartLink(_startStr); + + if (startInfo.stroke !== info.stroke) { return { type: 'INVALID', stroke: 'INVALID' }; } - startInfo.type = 'double_' + startInfo.type; + if (startInfo.type === 'arrow_open') { + // -- xyz --> - take arrow type from ending + startInfo.type = info.type; + } else { + // x-- xyz --> - not supported + if (startInfo.type !== info.type) { + return { type: 'INVALID', stroke: 'INVALID' }; + } + + startInfo.type = 'double_' + startInfo.type; + } + + if (startInfo.type === 'double_arrow') { + startInfo.type = 'double_arrow_point'; + } + + startInfo.length = info.length; + return startInfo; } - if (startInfo.type === 'double_arrow') { - startInfo.type = 'double_arrow_point'; + return info; + }; + + // Todo optimizer this by caching existing nodes + public exists = (allSgs: FlowSubGraph[], _id: string) => { + for (const sg of allSgs) { + if (sg.nodes.includes(_id)) { + return true; + } } - - startInfo.length = info.length; - return startInfo; - } - - return info; -}; - -// Todo optimizer this by caching existing nodes -const exists = (allSgs: FlowSubGraph[], _id: string) => { - for (const sg of allSgs) { - if (sg.nodes.includes(_id)) { - return true; - } - } - return false; -}; -/** - * Deletes an id from all subgraphs - * - */ -const makeUniq = (sg: FlowSubGraph, allSubgraphs: FlowSubGraph[]) => { - const res: string[] = []; - sg.nodes.forEach((_id, pos) => { - if (!exists(allSubgraphs, _id)) { - res.push(sg.nodes[pos]); - } - }); - return { nodes: res }; -}; - -export const lex = { - firstGraph, -}; - -const getTypeFromVertex = (vertex: FlowVertex): ShapeID => { - if (vertex.img) { - return 'imageSquare'; - } - if (vertex.icon) { - if (vertex.form === 'circle') { - return 'iconCircle'; - } - if (vertex.form === 'square') { - return 'iconSquare'; - } - if (vertex.form === 'rounded') { - return 'iconRounded'; - } - return 'icon'; - } - switch (vertex.type) { - case 'square': - case undefined: - return 'squareRect'; - case 'round': - return 'roundedRect'; - case 'ellipse': - // @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976 - return 'ellipse'; - default: - return vertex.type; - } -}; - -const findNode = (nodes: Node[], id: string) => nodes.find((node) => node.id === id); -const destructEdgeType = (type: string | undefined) => { - let arrowTypeStart = 'none'; - let arrowTypeEnd = 'arrow_point'; - switch (type) { - case 'arrow_point': - case 'arrow_circle': - case 'arrow_cross': - arrowTypeEnd = type; - break; - - case 'double_arrow_point': - case 'double_arrow_circle': - case 'double_arrow_cross': - arrowTypeStart = type.replace('double_', ''); - arrowTypeEnd = arrowTypeStart; - break; - } - return { arrowTypeStart, arrowTypeEnd }; -}; - -const addNodeFromVertex = ( - vertex: FlowVertex, - nodes: Node[], - parentDB: Map, - subGraphDB: Map, - config: any, - look: string -) => { - const parentId = parentDB.get(vertex.id); - const isGroup = subGraphDB.get(vertex.id) ?? false; - - const node = findNode(nodes, vertex.id); - if (node) { - node.cssStyles = vertex.styles; - node.cssCompiledStyles = getCompiledStyles(vertex.classes); - node.cssClasses = vertex.classes.join(' '); - } else { - const baseNode = { - id: vertex.id, - label: vertex.text, - labelStyle: '', - parentId, - padding: config.flowchart?.padding || 8, - cssStyles: vertex.styles, - cssCompiledStyles: getCompiledStyles(['default', 'node', ...vertex.classes]), - cssClasses: 'default ' + vertex.classes.join(' '), - dir: vertex.dir, - domId: vertex.domId, - look, - link: vertex.link, - linkTarget: vertex.linkTarget, - tooltip: getTooltip(vertex.id), - icon: vertex.icon, - pos: vertex.pos, - img: vertex.img, - assetWidth: vertex.assetWidth, - assetHeight: vertex.assetHeight, - constraint: vertex.constraint, - }; - if (isGroup) { - nodes.push({ - ...baseNode, - isGroup: true, - shape: 'rect', - }); - } else { - nodes.push({ - ...baseNode, - isGroup: false, - shape: getTypeFromVertex(vertex), - }); - } - } -}; - -function getCompiledStyles(classDefs: string[]) { - let compiledStyles: string[] = []; - for (const customClass of classDefs) { - const cssClass = classes.get(customClass); - if (cssClass?.styles) { - compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim()); - } - if (cssClass?.textStyles) { - compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim()); - } - } - return compiledStyles; -} - -export const getData = () => { - const config = getConfig(); - const nodes: Node[] = []; - const edges: Edge[] = []; - - const subGraphs = getSubGraphs(); - const parentDB = new Map(); - const subGraphDB = new Map(); - - // Setup the subgraph data for adding nodes - for (let i = subGraphs.length - 1; i >= 0; i--) { - const subGraph = subGraphs[i]; - if (subGraph.nodes.length > 0) { - subGraphDB.set(subGraph.id, true); - } - for (const id of subGraph.nodes) { - parentDB.set(id, subGraph.id); - } - } - - // Data is setup, add the nodes - for (let i = subGraphs.length - 1; i >= 0; i--) { - const subGraph = subGraphs[i]; - nodes.push({ - id: subGraph.id, - label: subGraph.title, - labelStyle: '', - parentId: parentDB.get(subGraph.id), - padding: 8, - cssCompiledStyles: getCompiledStyles(subGraph.classes), - cssClasses: subGraph.classes.join(' '), - shape: 'rect', - dir: subGraph.dir, - isGroup: true, - look: config.look, + return false; + }; + /** + * Deletes an id from all subgraphs + * + */ + public makeUniq = (sg: FlowSubGraph, allSubgraphs: FlowSubGraph[]) => { + const res: string[] = []; + sg.nodes.forEach((_id, pos) => { + if (!this.exists(allSubgraphs, _id)) { + res.push(sg.nodes[pos]); + } }); - } + return { nodes: res }; + }; - const n = getVertices(); - n.forEach((vertex) => { - addNodeFromVertex(vertex, nodes, parentDB, subGraphDB, config, config.look || 'classic'); - }); + public lex = { + firstGraph: this.firstGraph, + }; - const e = getEdges(); - e.forEach((rawEdge, index) => { - const { arrowTypeStart, arrowTypeEnd } = destructEdgeType(rawEdge.type); - const styles = [...(e.defaultStyle ?? [])]; - - if (rawEdge.style) { - styles.push(...rawEdge.style); + private getTypeFromVertex = (vertex: FlowVertex): ShapeID => { + if (vertex.img) { + return 'imageSquare'; } - const edge: Edge = { - id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }), - start: rawEdge.start, - end: rawEdge.end, - type: rawEdge.type ?? 'normal', - label: rawEdge.text, - labelpos: 'c', - thickness: rawEdge.stroke, - minlen: rawEdge.length, - classes: - rawEdge?.stroke === 'invisible' - ? '' - : 'edge-thickness-normal edge-pattern-solid flowchart-link', - arrowTypeStart: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeStart, - arrowTypeEnd: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeEnd, - arrowheadStyle: 'fill: #333', - labelStyle: styles, - style: styles, - pattern: rawEdge.stroke, - look: config.look, - }; - edges.push(edge); - }); + if (vertex.icon) { + if (vertex.form === 'circle') { + return 'iconCircle'; + } + if (vertex.form === 'square') { + return 'iconSquare'; + } + if (vertex.form === 'rounded') { + return 'iconRounded'; + } + return 'icon'; + } + switch (vertex.type) { + case 'square': + case undefined: + return 'squareRect'; + case 'round': + return 'roundedRect'; + case 'ellipse': + // @ts-expect-error -- Ellipses are broken, see https://github.com/mermaid-js/mermaid/issues/5976 + return 'ellipse'; + default: + return vertex.type; + } + }; - return { nodes, edges, other: {}, config }; -}; + private findNode = (nodes: Node[], id: string) => nodes.find((node) => node.id === id); + private destructEdgeType = (type: string | undefined) => { + let arrowTypeStart = 'none'; + let arrowTypeEnd = 'arrow_point'; + switch (type) { + case 'arrow_point': + case 'arrow_circle': + case 'arrow_cross': + arrowTypeEnd = type; + break; -export default { - defaultConfig: () => defaultConfig.flowchart, - setAccTitle, - getAccTitle, - getAccDescription, - getData, - setAccDescription, - addVertex, - lookUpDomId, - addLink, - updateLinkInterpolate, - updateLink, - addClass, - setDirection, - setClass, - setTooltip, - getTooltip, - setClickEvent, - setLink, - bindFunctions, - getDirection, - getVertices, - getEdges, - getClasses, - clear, - setGen, - defaultStyle, - addSubGraph, - getDepthFirstPos, - indexNodes, - getSubGraphs, - destructLink, - lex, - exists, - makeUniq, - setDiagramTitle, - getDiagramTitle, -}; + case 'double_arrow_point': + case 'double_arrow_circle': + case 'double_arrow_cross': + arrowTypeStart = type.replace('double_', ''); + arrowTypeEnd = arrowTypeStart; + break; + } + return { arrowTypeStart, arrowTypeEnd }; + }; + + private addNodeFromVertex = ( + vertex: FlowVertex, + nodes: Node[], + parentDB: Map, + subGraphDB: Map, + config: any, + look: string + ) => { + const parentId = parentDB.get(vertex.id); + const isGroup = subGraphDB.get(vertex.id) ?? false; + + const node = this.findNode(nodes, vertex.id); + if (node) { + node.cssStyles = vertex.styles; + node.cssCompiledStyles = this.getCompiledStyles(vertex.classes); + node.cssClasses = vertex.classes.join(' '); + } else { + const baseNode = { + id: vertex.id, + label: vertex.text, + labelStyle: '', + parentId, + padding: config.flowchart?.padding || 8, + cssStyles: vertex.styles, + cssCompiledStyles: this.getCompiledStyles(['default', 'node', ...vertex.classes]), + cssClasses: 'default ' + vertex.classes.join(' '), + dir: vertex.dir, + domId: vertex.domId, + look, + link: vertex.link, + linkTarget: vertex.linkTarget, + tooltip: this.getTooltip(vertex.id), + icon: vertex.icon, + pos: vertex.pos, + img: vertex.img, + assetWidth: vertex.assetWidth, + assetHeight: vertex.assetHeight, + constraint: vertex.constraint, + }; + if (isGroup) { + nodes.push({ + ...baseNode, + isGroup: true, + shape: 'rect', + }); + } else { + nodes.push({ + ...baseNode, + isGroup: false, + shape: this.getTypeFromVertex(vertex), + }); + } + } + }; + + private getCompiledStyles = (classDefs: string[]) => { + let compiledStyles: string[] = []; + for (const customClass of classDefs) { + const cssClass = this.classes.get(customClass); + if (cssClass?.styles) { + compiledStyles = [...compiledStyles, ...(cssClass.styles ?? [])].map((s) => s.trim()); + } + if (cssClass?.textStyles) { + compiledStyles = [...compiledStyles, ...(cssClass.textStyles ?? [])].map((s) => s.trim()); + } + } + return compiledStyles; + }; + + public getData = () => { + const config = getConfig(); + const nodes: Node[] = []; + const edges: Edge[] = []; + + const subGraphs = this.getSubGraphs(); + const parentDB = new Map(); + const subGraphDB = new Map(); + + // Setup the subgraph data for adding nodes + for (let i = subGraphs.length - 1; i >= 0; i--) { + const subGraph = subGraphs[i]; + if (subGraph.nodes.length > 0) { + subGraphDB.set(subGraph.id, true); + } + for (const id of subGraph.nodes) { + parentDB.set(id, subGraph.id); + } + } + + // Data is setup, add the nodes + for (let i = subGraphs.length - 1; i >= 0; i--) { + const subGraph = subGraphs[i]; + nodes.push({ + id: subGraph.id, + label: subGraph.title, + labelStyle: '', + parentId: parentDB.get(subGraph.id), + padding: 8, + cssCompiledStyles: this.getCompiledStyles(subGraph.classes), + cssClasses: subGraph.classes.join(' '), + shape: 'rect', + dir: subGraph.dir, + isGroup: true, + look: config.look, + }); + } + + const n = this.getVertices(); + n.forEach((vertex) => { + this.addNodeFromVertex(vertex, nodes, parentDB, subGraphDB, config, config.look || 'classic'); + }); + + const e = this.getEdges(); + e.forEach((rawEdge, index) => { + const { arrowTypeStart, arrowTypeEnd } = this.destructEdgeType(rawEdge.type); + const styles = [...(e.defaultStyle ?? [])]; + + if (rawEdge.style) { + styles.push(...rawEdge.style); + } + const edge: Edge = { + id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }), + start: rawEdge.start, + end: rawEdge.end, + type: rawEdge.type ?? 'normal', + label: rawEdge.text, + labelpos: 'c', + thickness: rawEdge.stroke, + minlen: rawEdge.length, + classes: + rawEdge?.stroke === 'invisible' + ? '' + : 'edge-thickness-normal edge-pattern-solid flowchart-link', + arrowTypeStart: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeStart, + arrowTypeEnd: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeEnd, + arrowheadStyle: 'fill: #333', + labelStyle: styles, + style: styles, + pattern: rawEdge.stroke, + look: config.look, + }; + edges.push(edge); + }); + + return { nodes, edges, other: {}, config }; + }; + + public defaultConfig = () => defaultConfig.flowchart; + public setAccTitle = setAccTitle; + public setAccDescription = setAccDescription; + public setDiagramTitle = setDiagramTitle; + public getAccTitle = getAccTitle; + public getAccDescription = getAccDescription; + public getDiagramTitle = getDiagramTitle; +} diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts index 67cdf918f..059125201 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts @@ -1,6 +1,6 @@ import type { MermaidConfig } from '../../config.type.js'; import { setConfig } from '../../diagram-api/diagramAPI.js'; -import flowDb from './flowDb.js'; +import { FlowDb } from './flowDb.js'; import renderer from './flowRenderer-v3-unified.js'; // @ts-ignore: JISON doesn't support types import flowParser from './parser/flow.jison'; @@ -8,7 +8,9 @@ import flowStyles from './styles.js'; export const diagram = { parser: flowParser, - db: flowDb, + get db() { + return new FlowDb(); + }, renderer, styles: flowStyles, init: (cnf: MermaidConfig) => { @@ -20,7 +22,5 @@ export const diagram = { } cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); - flowDb.clear(); - flowDb.setGen('gen-2'); }, }; diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts index 6cc15258d..f4c0b9e0e 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v3-unified.ts @@ -7,7 +7,6 @@ import { getRegisteredLayoutAlgorithm, render } from '../../rendering-util/rende import { setupViewPortForSVG } from '../../rendering-util/setupViewPortForSVG.js'; import type { LayoutData } from '../../rendering-util/types.js'; import utils from '../../utils.js'; -import { getDirection } from './flowDb.js'; export const getClasses = function ( text: string, @@ -37,7 +36,7 @@ export const draw = async function (text: string, id: string, _version: string, log.debug('Data: ', data4Layout); // Create the root SVG const svg = getDiagramElement(id, securityLevel); - const direction = getDirection(); + const direction = diag.db.getDirection(); data4Layout.type = diag.type; data4Layout.layoutAlgorithm = getRegisteredLayoutAlgorithm(layout); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-arrows.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-arrows.spec.js index e89398ab4..e4fec241f 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-arrows.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-arrows.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Arrows] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-comments.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-comments.spec.js index 9c2a740af..673f11f89 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-comments.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-comments.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; import { cleanupComments } from '../../../diagram-api/comments.js'; @@ -9,7 +9,7 @@ setConfig({ describe('[Comments] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-direction.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-direction.spec.js index ce6b0b0c4..9598e7303 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-direction.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-direction.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('when parsing directions', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js index 4ae289bad..ed6f53276 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -42,7 +42,7 @@ const doubleEndedEdges = [ describe('[Edges] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-huge.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-huge.spec.js index 8931c6ee1..6f2ce0fd7 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-huge.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-huge.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Text] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-interactions.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-interactions.spec.js index cb3f48cca..9e16ff837 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-interactions.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-interactions.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; import { vi } from 'vitest'; @@ -9,7 +9,9 @@ setConfig({ }); describe('[Interactions] when parsing', () => { + let flowDb; beforeEach(function () { + flowDb = new FlowDb(); flow.parser.yy = flowDb; flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-lines.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-lines.spec.js index ec157e646..25f248532 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-lines.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-lines.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Lines] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js index 55e749a22..0b3f18e40 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-md-string.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('parsing a flow chart with markdown strings', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js index 1669cfada..74eb64e59 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-node-data.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('when parsing directions', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js index f6ed123d7..550f9c9fd 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-singlenode.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -31,7 +31,7 @@ const specialChars = ['#', ':', '0', '&', ',', '*', '.', '\\', 'v', '-', '/', '_ describe('[Singlenodes] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js index 22fd48a33..775fb01f3 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Style] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js index 3754766f4..a2d069f90 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-text.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('[Text] when parsing', () => { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow-vertice-chaining.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow-vertice-chaining.spec.js index a5b6a2b6d..6e855a3d5 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-vertice-chaining.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-vertice-chaining.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('when parsing flowcharts', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/flow.spec.js index 8081c8fe4..41ed5ee52 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { cleanupComments } from '../../../diagram-api/comments.js'; import { setConfig } from '../../../config.js'; @@ -9,7 +9,7 @@ setConfig({ describe('parsing a flow chart', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/subgraph.spec.js b/packages/mermaid/src/diagrams/flowchart/parser/subgraph.spec.js index 12b2e4a39..a3a65ecbb 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/subgraph.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/subgraph.spec.js @@ -1,4 +1,4 @@ -import flowDb from '../flowDb.js'; +import { FlowDb } from '../flowDb.js'; import flow from './flow.jison'; import { setConfig } from '../../../config.js'; @@ -8,7 +8,7 @@ setConfig({ describe('when parsing subgraphs', function () { beforeEach(function () { - flow.parser.yy = flowDb; + flow.parser.yy = new FlowDb(); flow.parser.yy.clear(); flow.parser.yy.setGen('gen-2'); });