From ba0d21682ac776320af09b47894178210deb1d47 Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Fri, 9 Aug 2024 14:47:51 +0200 Subject: [PATCH] #5237 Removing old flowchart renderer --- .../src/diagrams/flowchart/flowDiagram-v2.ts | 4 +- .../src/diagrams/flowchart/flowDiagram.ts | 11 +- .../src/diagrams/flowchart/flowRenderer-v2.js | 515 ------------------ .../flowchart/flowRenderer.addEdges.spec.js | 150 ----- .../src/diagrams/flowchart/flowRenderer.js | 503 ----------------- .../diagrams/flowchart/flowRenderer.spec.js | 277 ---------- 6 files changed, 7 insertions(+), 1453 deletions(-) delete mode 100644 packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js delete mode 100644 packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js delete mode 100644 packages/mermaid/src/diagrams/flowchart/flowRenderer.js delete mode 100644 packages/mermaid/src/diagrams/flowchart/flowRenderer.spec.js diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts index 279b7a7240..5b8012edea 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDiagram-v2.ts @@ -1,7 +1,7 @@ // @ts-ignore: JISON doesn't support types import flowParser from './parser/flow.jison'; import flowDb from './flowDb.js'; -import flowRendererV3 from './flowRenderer-v3-unified.js'; +import renderer from './flowRenderer-v3-unified.js'; import flowStyles from './styles.js'; import type { MermaidConfig } from '../../config.type.js'; import { setConfig } from '../../diagram-api/diagramAPI.js'; @@ -9,7 +9,7 @@ import { setConfig } from '../../diagram-api/diagramAPI.js'; export const diagram = { parser: flowParser, db: flowDb, - renderer: flowRendererV3, + renderer, styles: flowStyles, init: (cnf: MermaidConfig) => { if (!cnf.flowchart) { diff --git a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts index ca4f8fba8a..5b8012edea 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDiagram.ts @@ -1,24 +1,23 @@ // @ts-ignore: JISON doesn't support types import flowParser from './parser/flow.jison'; import flowDb from './flowDb.js'; -import flowRenderer from './flowRenderer.js'; -import flowRendererV2 from './flowRenderer-v2.js'; +import renderer from './flowRenderer-v3-unified.js'; import flowStyles from './styles.js'; import type { MermaidConfig } from '../../config.type.js'; +import { setConfig } from '../../diagram-api/diagramAPI.js'; export const diagram = { parser: flowParser, db: flowDb, - renderer: flowRendererV2, + renderer, styles: flowStyles, init: (cnf: MermaidConfig) => { if (!cnf.flowchart) { cnf.flowchart = {}; } - // TODO, broken as of 2022-09-14 (13809b50251845475e6dca65cc395761be38fbd2) cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; - flowRenderer.setConf(cnf.flowchart); + setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); flowDb.clear(); - flowDb.setGen('gen-1'); + flowDb.setGen('gen-2'); }, }; diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js deleted file mode 100644 index 148ff2f869..0000000000 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer-v2.js +++ /dev/null @@ -1,515 +0,0 @@ -import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; -import { select, curveLinear, selectAll } from 'd3'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; -import utils, { getEdgeId } from '../../utils.js'; -import { render } from '../../dagre-wrapper/index.js'; -import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js'; -import { log } from '../../logger.js'; -import common, { evaluate, renderKatex } from '../common/common.js'; -import { interpolateToCurve, getStylesFromArray } from '../../utils.js'; -import { setupGraphViewbox } from '../../setupGraphViewbox.js'; - -const conf = {}; -export const setConf = function (cnf) { - const keys = Object.keys(cnf); - for (const key of keys) { - conf[key] = cnf[key]; - } -}; - -/** - * Function that adds the vertices found during parsing to the graph to be rendered. - * - * @param vert Object containing the vertices. - * @param g The graph that is to be drawn. - * @param svgId - * @param root - * @param doc - * @param diagObj - */ -export const addVertices = async function (vert, g, svgId, root, doc, diagObj) { - const svg = root.select(`[id="${svgId}"]`); - - const keys = vert.keys(); - - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - for (const id of keys) { - const vertex = vert.get(id); - - /** - * Variable for storing the classes for the vertex - * - * @type {string} - */ - let classStr = 'default'; - if (vertex.classes.length > 0) { - classStr = vertex.classes.join(' '); - } - classStr = classStr + ' flowchart-label'; - const styles = getStylesFromArray(vertex.styles); - - // Use vertex id as text in the box if no text is provided by the graph definition - let vertexText = vertex.text !== undefined ? vertex.text : vertex.id; - - // We create a SVG label, either by delegating to addHtmlLabel or manually - let vertexNode; - log.info('vertex', vertex, vertex.labelType); - if (vertex.labelType === 'markdown') { - log.info('vertex', vertex, vertex.labelType); - } else { - if (evaluate(getConfig().flowchart.htmlLabels)) { - // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? - const node = { - label: vertexText, - }; - vertexNode = addHtmlLabel(svg, node).node(); - vertexNode.parentNode.removeChild(vertexNode); - } else { - const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text'); - svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:')); - - const rows = vertexText.split(common.lineBreakRegex); - - for (const row of rows) { - const tspan = doc.createElementNS('http://www.w3.org/2000/svg', 'tspan'); - tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); - tspan.setAttribute('dy', '1em'); - tspan.setAttribute('x', '1'); - tspan.textContent = row; - svgLabel.appendChild(tspan); - } - vertexNode = svgLabel; - } - } - - let radius = 0; - let _shape = ''; - // Set the shape based parameters - switch (vertex.type) { - case 'round': - radius = 5; - _shape = 'rect'; - break; - case 'square': - _shape = 'rect'; - break; - case 'diamond': - _shape = 'question'; - break; - case 'hexagon': - _shape = 'hexagon'; - break; - case 'odd': - _shape = 'rect_left_inv_arrow'; - break; - case 'lean_right': - _shape = 'lean_right'; - break; - case 'lean_left': - _shape = 'lean_left'; - break; - case 'trapezoid': - _shape = 'trapezoid'; - break; - case 'inv_trapezoid': - _shape = 'inv_trapezoid'; - break; - case 'odd_right': - _shape = 'rect_left_inv_arrow'; - break; - case 'circle': - _shape = 'circle'; - break; - case 'ellipse': - _shape = 'ellipse'; - break; - case 'stadium': - _shape = 'stadium'; - break; - case 'subroutine': - _shape = 'subroutine'; - break; - case 'cylinder': - _shape = 'cylinder'; - break; - case 'group': - _shape = 'rect'; - break; - case 'doublecircle': - _shape = 'doublecircle'; - break; - default: - _shape = 'rect'; - } - const labelText = await renderKatex(vertexText, getConfig()); - - // Add the node - g.setNode(vertex.id, { - labelStyle: styles.labelStyle, - shape: _shape, - labelText, - labelType: vertex.labelType, - rx: radius, - ry: radius, - class: classStr, - style: styles.style, - id: vertex.id, - link: vertex.link, - linkTarget: vertex.linkTarget, - tooltip: diagObj.db.getTooltip(vertex.id) || '', - domId: diagObj.db.lookUpDomId(vertex.id), - haveCallback: vertex.haveCallback, - width: vertex.type === 'group' ? 500 : undefined, - dir: vertex.dir, - type: vertex.type, - props: vertex.props, - padding: getConfig().flowchart.padding, - }); - - log.info('setNode', { - labelStyle: styles.labelStyle, - labelType: vertex.labelType, - shape: _shape, - labelText, - rx: radius, - ry: radius, - class: classStr, - style: styles.style, - id: vertex.id, - domId: diagObj.db.lookUpDomId(vertex.id), - width: vertex.type === 'group' ? 500 : undefined, - type: vertex.type, - dir: vertex.dir, - props: vertex.props, - padding: getConfig().flowchart.padding, - }); - } -}; - -/** - * Add edges to graph based on parsed graph definition - * - * @param {object} edges The edges to add to the graph - * @param {object} g The graph object - */ -export const addEdges = async function (edges, g) { - log.info('abc78 edges = ', edges); - let cnt = 0; - let linkIdCnt = {}; - - let defaultStyle; - let defaultLabelStyle; - - if (edges.defaultStyle !== undefined) { - const defaultStyles = getStylesFromArray(edges.defaultStyle); - defaultStyle = defaultStyles.style; - defaultLabelStyle = defaultStyles.labelStyle; - } - - for (const edge of edges) { - cnt++; - - // Identify Link - const linkIdBase = getEdgeId(edge.start, edge.end, { - counter: cnt, - prefix: 'L', - }); - - // count the links from+to the same node to give unique id - if (linkIdCnt[linkIdBase] === undefined) { - linkIdCnt[linkIdBase] = 0; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } else { - linkIdCnt[linkIdBase]++; - log.info('abc78 new entry', linkIdBase, linkIdCnt[linkIdBase]); - } - let linkId = `${linkIdBase}_${linkIdCnt[linkIdBase]}`; - - log.info('abc78 new link id to be used is', linkIdBase, linkId, linkIdCnt[linkIdBase]); - const linkNameStart = 'LS-' + edge.start; - const linkNameEnd = 'LE-' + edge.end; - - const edgeData = { style: '', labelStyle: '' }; - edgeData.minlen = edge.length || 1; - //edgeData.id = 'id' + cnt; - - // Set link type for rendering - if (edge.type === 'arrow_open') { - edgeData.arrowhead = 'none'; - } else { - edgeData.arrowhead = 'normal'; - } - - // Check of arrow types, placed here in order not to break old rendering - edgeData.arrowTypeStart = 'arrow_open'; - edgeData.arrowTypeEnd = 'arrow_open'; - - /* eslint-disable no-fallthrough */ - switch (edge.type) { - case 'double_arrow_cross': - edgeData.arrowTypeStart = 'arrow_cross'; - case 'arrow_cross': - edgeData.arrowTypeEnd = 'arrow_cross'; - break; - case 'double_arrow_point': - edgeData.arrowTypeStart = 'arrow_point'; - case 'arrow_point': - edgeData.arrowTypeEnd = 'arrow_point'; - break; - case 'double_arrow_circle': - edgeData.arrowTypeStart = 'arrow_circle'; - case 'arrow_circle': - edgeData.arrowTypeEnd = 'arrow_circle'; - break; - } - - let style = ''; - let labelStyle = ''; - - switch (edge.stroke) { - case 'normal': - style = 'fill:none;'; - if (defaultStyle !== undefined) { - style = defaultStyle; - } - if (defaultLabelStyle !== undefined) { - labelStyle = defaultLabelStyle; - } - edgeData.thickness = 'normal'; - edgeData.pattern = 'solid'; - break; - case 'dotted': - edgeData.thickness = 'normal'; - edgeData.pattern = 'dotted'; - edgeData.style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; - break; - case 'thick': - edgeData.thickness = 'thick'; - edgeData.pattern = 'solid'; - edgeData.style = 'stroke-width: 3.5px;fill:none;'; - break; - case 'invisible': - edgeData.thickness = 'invisible'; - edgeData.pattern = 'solid'; - edgeData.style = 'stroke-width: 0;fill:none;'; - break; - } - if (edge.style !== undefined) { - const styles = getStylesFromArray(edge.style); - style = styles.style; - labelStyle = styles.labelStyle; - } - - edgeData.style = edgeData.style += style; - edgeData.labelStyle = edgeData.labelStyle += labelStyle; - - if (edge.interpolate !== undefined) { - edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); - } else if (edges.defaultInterpolate !== undefined) { - edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); - } else { - edgeData.curve = interpolateToCurve(conf.curve, curveLinear); - } - - if (edge.text === undefined) { - if (edge.style !== undefined) { - edgeData.arrowheadStyle = 'fill: #333'; - } - } else { - edgeData.arrowheadStyle = 'fill: #333'; - edgeData.labelpos = 'c'; - } - edgeData.labelType = edge.labelType; - edgeData.label = await renderKatex(edge.text.replace(common.lineBreakRegex, '\n'), getConfig()); - - if (edge.style === undefined) { - edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none;'; - } - - edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); - - edgeData.id = linkId; - edgeData.classes = 'flowchart-link ' + linkNameStart + ' ' + linkNameEnd; - - // Add the edge to the graph - g.setEdge(edge.start, edge.end, edgeData, cnt); - } -}; - -/** - * Returns the all the styles from classDef statements in the graph definition. - * - * @param text - * @param diagObj - * @returns {Map} ClassDef styles - */ -export const getClasses = function (text, diagObj) { - return diagObj.db.getClasses(); -}; - -/** - * Draws a flowchart in the tag with id: id based on the graph definition in text. - * - * @param text - * @param id - * @param _version - * @param diagObj - */ - -export const draw = async function (text, id, _version, diagObj) { - log.info('Drawing flowchart'); - - // Fetch the default direction, use TD if none was found - let dir = diagObj.db.getDirection(); - if (dir === undefined) { - dir = 'TD'; - } - - const { securityLevel, flowchart: conf } = getConfig(); - const nodeSpacing = conf.nodeSpacing ?? 50; - const rankSpacing = conf.rankSpacing ?? 50; - - // Handle root and document for when rendering in sandbox mode - let sandboxElement; - if (securityLevel === 'sandbox') { - sandboxElement = select('#i' + id); - } - const root = - securityLevel === 'sandbox' - ? select(sandboxElement.nodes()[0].contentDocument.body) - : select('body'); - const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - - // Create the input mermaid.graph - const g = new graphlib.Graph({ - multigraph: true, - compound: true, - }) - .setGraph({ - rankdir: dir, - nodesep: nodeSpacing, - ranksep: rankSpacing, - marginx: 0, - marginy: 0, - }) - .setDefaultEdgeLabel(function () { - return {}; - }); - - let subG; - const subGraphs = diagObj.db.getSubGraphs(); - log.info('Subgraphs - ', subGraphs); - for (let i = subGraphs.length - 1; i >= 0; i--) { - subG = subGraphs[i]; - log.info('Subgraph - ', subG); - diagObj.db.addVertex( - subG.id, - { text: subG.title, type: subG.labelType }, - 'group', - undefined, - subG.classes, - subG.dir - ); - } - - // Fetch the vertices/nodes and edges/links from the parsed graph definition - const vert = diagObj.db.getVertices(); - - const edges = diagObj.db.getEdges(); - - log.info('Edges', edges); - let i = 0; - for (i = subGraphs.length - 1; i >= 0; i--) { - subG = subGraphs[i]; - - selectAll('cluster').append('text'); - - for (const node of subG.nodes) { - log.info('Setting up subgraphs', node, subG.id); - g.setParent(node, subG.id); - } - } - await addVertices(vert, g, id, root, doc, diagObj); - await addEdges(edges, g, diagObj); - - // Add custom shapes - // flowChartShapes.addToRenderV2(addShape); - - // Set up an SVG group so that we can translate the final graph. - const svg = root.select(`[id="${id}"]`); - - // Run the renderer. This is what draws the final graph. - const element = root.select('#' + id + ' g'); - await render(element, g, ['point', 'circle', 'cross'], 'flowchart', id); - - utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle()); - - setupGraphViewbox(g, svg, conf.diagramPadding, conf.useMaxWidth); - - // Index nodes - diagObj.db.indexNodes('subGraph' + i); - - // Add label rects for non html labels - if (!conf.htmlLabels) { - const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label'); - for (const label of labels) { - // Get dimensions of label - const dim = label.getBBox(); - - const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect'); - rect.setAttribute('rx', 0); - rect.setAttribute('ry', 0); - rect.setAttribute('width', dim.width); - rect.setAttribute('height', dim.height); - - label.insertBefore(rect, label.firstChild); - } - } - - // If node has a link, wrap it in an anchor SVG object. - const keys = [...vert.keys()]; - keys.forEach((key) => { - const vertex = vert.get(key); - - if (vertex.link) { - const node = select('#' + id + ' [id="' + key + '"]'); - if (node) { - const link = doc.createElementNS('http://www.w3.org/2000/svg', 'a'); - link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' ')); - link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link); - link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener'); - if (securityLevel === 'sandbox') { - link.setAttributeNS('http://www.w3.org/2000/svg', 'target', '_top'); - } else if (vertex.linkTarget) { - link.setAttributeNS('http://www.w3.org/2000/svg', 'target', vertex.linkTarget); - } - - const linkNode = node.insert(function () { - return link; - }, ':first-child'); - - const shape = node.select('.label-container'); - if (shape) { - linkNode.append(function () { - return shape.node(); - }); - } - - const label = node.select('.label'); - if (label) { - linkNode.append(function () { - return label.node(); - }); - } - } - } - }); -}; - -export default { - setConf, - addVertices, - addEdges, - getClasses, - draw, -}; diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js deleted file mode 100644 index 00f9ef8512..0000000000 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.addEdges.spec.js +++ /dev/null @@ -1,150 +0,0 @@ -import flowDb from './flowDb.js'; -import { parser } from './parser/flow.jison'; -import flowRenderer from './flowRenderer.js'; -import { addDiagrams } from '../../diagram-api/diagram-orchestration.js'; - -const diag = { - db: flowDb, -}; -addDiagrams(); - -describe('when using mermaid and ', function () { - describe('when calling addEdges ', function () { - beforeEach(function () { - parser.yy = flowDb; - flowDb.clear(); - flowDb.setGen('gen-2'); - }); - it('should handle edges with text', async () => { - parser.parse('graph TD;A-->|text ex|B;'); - flowDb.getVertices(); - const edges = flowDb.getEdges(); - - const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); - expect(options.arrowhead).toBe('normal'); - expect(options.label.match('text ex')).toBeTruthy(); - }, - }; - - await flowRenderer.addEdges(edges, mockG, diag); - }); - - it('should handle edges without text', async function () { - parser.parse('graph TD;A-->B;'); - flowDb.getVertices(); - const edges = flowDb.getEdges(); - - const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); - expect(options.arrowhead).toBe('normal'); - }, - }; - - await flowRenderer.addEdges(edges, mockG, diag); - }); - - it('should handle open-ended edges', async () => { - parser.parse('graph TD;A---B;'); - flowDb.getVertices(); - const edges = flowDb.getEdges(); - - const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); - expect(options.arrowhead).toBe('none'); - }, - }; - - await flowRenderer.addEdges(edges, mockG, diag); - }); - - it('should handle edges with styles defined', async () => { - parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); - flowDb.getVertices(); - const edges = flowDb.getEdges(); - - const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); - expect(options.arrowhead).toBe('none'); - expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;'); - }, - }; - - await flowRenderer.addEdges(edges, mockG, diag); - }); - it('should handle edges with interpolation defined', async () => { - parser.parse('graph TD;A---B; linkStyle 0 interpolate basis'); - flowDb.getVertices(); - const edges = flowDb.getEdges(); - - const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); - expect(options.arrowhead).toBe('none'); - expect(options.curve).toBe('basis'); // mocked as string - }, - }; - - await flowRenderer.addEdges(edges, mockG, diag); - }); - it('should handle edges with text and styles defined', async () => { - parser.parse('graph TD;A---|the text|B; linkStyle 0 stroke:val1,stroke-width:val2;'); - flowDb.getVertices(); - const edges = flowDb.getEdges(); - - const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); - expect(options.arrowhead).toBe('none'); - expect(options.label.match('the text')).toBeTruthy(); - expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;'); - }, - }; - - await flowRenderer.addEdges(edges, mockG, diag); - }); - - it('should set fill to "none" by default when handling edges', async () => { - parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2;'); - flowDb.getVertices(); - const edges = flowDb.getEdges(); - - const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B'); - expect(options.arrowhead).toBe('none'); - expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:none;'); - }, - }; - - await flowRenderer.addEdges(edges, mockG, diag); - }); - - it('should not set fill to none if fill is set in linkStyle', async () => { - parser.parse('graph TD;A---B; linkStyle 0 stroke:val1,stroke-width:val2,fill:blue;'); - flowDb.getVertices(); - const edges = flowDb.getEdges(); - const mockG = { - setEdge: function (start, end, options) { - expect(start).toContain('flowchart-A-'); - expect(end).toContain('flowchart-B-'); - expect(options.arrowhead).toBe('none'); - expect(options.style).toBe('stroke:val1;stroke-width:val2;fill:blue;'); - }, - }; - - await flowRenderer.addEdges(edges, mockG, diag); - }); - }); -}); diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.js deleted file mode 100644 index 314c6aa52b..0000000000 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.js +++ /dev/null @@ -1,503 +0,0 @@ -import * as graphlib from 'dagre-d3-es/src/graphlib/index.js'; -import { select, curveLinear, selectAll } from 'd3'; -import { getConfig } from '../../diagram-api/diagramAPI.js'; -import { render as Render } from 'dagre-d3-es'; -import { applyStyle } from 'dagre-d3-es/src/dagre-js/util.js'; -import { addHtmlLabel } from 'dagre-d3-es/src/dagre-js/label/add-html-label.js'; -import { log } from '../../logger.js'; -import common, { evaluate, renderKatex } from '../common/common.js'; -import { interpolateToCurve, getStylesFromArray, getEdgeId } from '../../utils.js'; -import { setupGraphViewbox } from '../../setupGraphViewbox.js'; -import flowChartShapes from './flowChartShapes.js'; -import { replaceIconSubstring } from '../../rendering-util/createText.js'; - -const conf = {}; -export const setConf = function (cnf) { - const keys = Object.keys(cnf); - for (const key of keys) { - conf[key] = cnf[key]; - } -}; - -/** - * Function that adds the vertices found in the graph definition to the graph to be rendered. - * - * @param vert Object containing the vertices. - * @param g The graph that is to be drawn. - * @param svgId - * @param root - * @param _doc - * @param diagObj - */ -export const addVertices = async function (vert, g, svgId, root, _doc, diagObj) { - const svg = !root ? select(`[id="${svgId}"]`) : root.select(`[id="${svgId}"]`); - const doc = !_doc ? document : _doc; - const keys = Object.keys(vert); - - // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition - for (const id of keys) { - const vertex = vert[id]; - - /** - * Variable for storing the classes for the vertex - * - * @type {string} - */ - let classStr = 'default'; - if (vertex.classes.length > 0) { - classStr = vertex.classes.join(' '); - } - - const styles = getStylesFromArray(vertex.styles); - - // Use vertex id as text in the box if no text is provided by the graph definition - let vertexText = vertex.text !== undefined ? vertex.text : vertex.id; - - // We create a SVG label, either by delegating to addHtmlLabel or manually - let vertexNode; - if (evaluate(getConfig().flowchart.htmlLabels)) { - // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? - const replacedVertexText = replaceIconSubstring(vertexText); - const node = { - label: await renderKatex(replacedVertexText, getConfig()), - }; - vertexNode = addHtmlLabel(svg, node).node(); - vertexNode.parentNode.removeChild(vertexNode); - } else { - const svgLabel = doc.createElementNS('http://www.w3.org/2000/svg', 'text'); - svgLabel.setAttribute('style', styles.labelStyle.replace('color:', 'fill:')); - - const rows = vertexText.split(common.lineBreakRegex); - - for (const row of rows) { - const tspan = doc.createElementNS('http://www.w3.org/2000/svg', 'tspan'); - tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve'); - tspan.setAttribute('dy', '1em'); - tspan.setAttribute('x', '1'); - tspan.textContent = row; - svgLabel.appendChild(tspan); - } - vertexNode = svgLabel; - } - - let radius = 0; - let _shape = ''; - // Set the shape based parameters - switch (vertex.type) { - case 'round': - radius = 5; - _shape = 'rect'; - break; - case 'square': - _shape = 'rect'; - break; - case 'diamond': - _shape = 'question'; - break; - case 'hexagon': - _shape = 'hexagon'; - break; - case 'odd': - _shape = 'rect_left_inv_arrow'; - break; - case 'lean_right': - _shape = 'lean_right'; - break; - case 'lean_left': - _shape = 'lean_left'; - break; - case 'trapezoid': - _shape = 'trapezoid'; - break; - case 'inv_trapezoid': - _shape = 'inv_trapezoid'; - break; - case 'odd_right': - _shape = 'rect_left_inv_arrow'; - break; - case 'circle': - _shape = 'circle'; - break; - case 'ellipse': - _shape = 'ellipse'; - break; - case 'stadium': - _shape = 'stadium'; - break; - case 'subroutine': - _shape = 'subroutine'; - break; - case 'cylinder': - _shape = 'cylinder'; - break; - case 'group': - _shape = 'rect'; - break; - default: - _shape = 'rect'; - } - // Add the node - log.warn('Adding node', vertex.id, vertex.domId); - g.setNode(diagObj.db.lookUpDomId(vertex.id), { - labelType: 'svg', - labelStyle: styles.labelStyle, - shape: _shape, - label: vertexNode, - rx: radius, - ry: radius, - class: classStr, - style: styles.style, - id: diagObj.db.lookUpDomId(vertex.id), - }); - } -}; - -/** - * Add edges to graph based on parsed graph definition - * - * @param {object} edges The edges to add to the graph - * @param {object} g The graph object - * @param diagObj - */ -export const addEdges = async function (edges, g, diagObj) { - let cnt = 0; - - let defaultStyle; - let defaultLabelStyle; - - if (edges.defaultStyle !== undefined) { - const defaultStyles = getStylesFromArray(edges.defaultStyle); - defaultStyle = defaultStyles.style; - defaultLabelStyle = defaultStyles.labelStyle; - } - - for (const edge of edges) { - cnt++; - - // Identify Link - const linkId = getEdgeId(edge.start, edge.end, { - counter: cnt, - prefix: 'L', - }); - const linkNameStart = 'LS-' + edge.start; - const linkNameEnd = 'LE-' + edge.end; - - const edgeData = {}; - - // Set link type for rendering - if (edge.type === 'arrow_open') { - edgeData.arrowhead = 'none'; - } else { - edgeData.arrowhead = 'normal'; - } - - let style = ''; - let labelStyle = ''; - - if (edge.style !== undefined) { - const styles = getStylesFromArray(edge.style); - style = styles.style; - labelStyle = styles.labelStyle; - } else { - switch (edge.stroke) { - case 'normal': - style = 'fill:none'; - if (defaultStyle !== undefined) { - style = defaultStyle; - } - if (defaultLabelStyle !== undefined) { - labelStyle = defaultLabelStyle; - } - break; - case 'dotted': - style = 'fill:none;stroke-width:2px;stroke-dasharray:3;'; - break; - case 'thick': - style = ' stroke-width: 3.5px;fill:none'; - break; - } - } - - edgeData.style = style; - edgeData.labelStyle = labelStyle; - - if (edge.interpolate !== undefined) { - edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); - } else if (edges.defaultInterpolate !== undefined) { - edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); - } else { - edgeData.curve = interpolateToCurve(conf.curve, curveLinear); - } - - if (edge.text === undefined) { - if (edge.style !== undefined) { - edgeData.arrowheadStyle = 'fill: #333'; - } - } else { - edgeData.arrowheadStyle = 'fill: #333'; - edgeData.labelpos = 'c'; - - if (evaluate(getConfig().flowchart.htmlLabels)) { - edgeData.labelType = 'html'; - edgeData.label = `${await renderKatex(replaceIconSubstring(edge.text), getConfig())}`; - } else { - edgeData.labelType = 'text'; - edgeData.label = edge.text.replace(common.lineBreakRegex, '\n'); - - if (edge.style === undefined) { - edgeData.style = edgeData.style || 'stroke: #333; stroke-width: 1.5px;fill:none'; - } - - edgeData.labelStyle = edgeData.labelStyle.replace('color:', 'fill:'); - } - } - - edgeData.id = linkId; - edgeData.class = linkNameStart + ' ' + linkNameEnd; - edgeData.minlen = edge.length || 1; - - // Add the edge to the graph - g.setEdge(diagObj.db.lookUpDomId(edge.start), diagObj.db.lookUpDomId(edge.end), edgeData, cnt); - } -}; - -/** - * Returns the all the styles from classDef statements in the graph definition. - * - * @param text - * @param diagObj - * @returns {Map} ClassDef styles - */ -export const getClasses = function (text, diagObj) { - log.info('Extracting classes'); - return diagObj.db.getClasses(); -}; - -/** - * Draws a flowchart in the tag with id: id based on the graph definition in text. - * - * @param text - * @param id - * @param _version - * @param diagObj - */ -export const draw = async function (text, id, _version, diagObj) { - log.info('Drawing flowchart'); - const { securityLevel, flowchart: conf } = getConfig(); - let sandboxElement; - if (securityLevel === 'sandbox') { - sandboxElement = select('#i' + id); - } - const root = - securityLevel === 'sandbox' - ? select(sandboxElement.nodes()[0].contentDocument.body) - : select('body'); - const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - - // Fetch the default direction, use TD if none was found - let dir = diagObj.db.getDirection(); - if (dir === undefined) { - dir = 'TD'; - } - const nodeSpacing = conf.nodeSpacing ?? 50; - const rankSpacing = conf.rankSpacing ?? 50; - - // Create the input mermaid.graph - const g = new graphlib.Graph({ - multigraph: true, - compound: true, - }) - .setGraph({ - rankdir: dir, - nodesep: nodeSpacing, - ranksep: rankSpacing, - marginx: 8, - marginy: 8, - }) - .setDefaultEdgeLabel(function () { - return {}; - }); - - let subG; - const subGraphs = diagObj.db.getSubGraphs(); - for (let i = subGraphs.length - 1; i >= 0; i--) { - subG = subGraphs[i]; - diagObj.db.addVertex(subG.id, subG.title, 'group', undefined, subG.classes); - } - - // Fetch the vertices/nodes and edges/links from the parsed graph definition - const vert = diagObj.db.getVertices(); - log.warn('Get vertices', vert); - - const edges = diagObj.db.getEdges(); - - let i = 0; - for (i = subGraphs.length - 1; i >= 0; i--) { - subG = subGraphs[i]; - - selectAll('cluster').append('text'); - - for (const node of subG.nodes) { - log.warn( - 'Setting subgraph', - node, - diagObj.db.lookUpDomId(node), - diagObj.db.lookUpDomId(subG.id) - ); - g.setParent(diagObj.db.lookUpDomId(node), diagObj.db.lookUpDomId(subG.id)); - } - } - await addVertices(vert, g, id, root, doc, diagObj); - await addEdges(edges, g, diagObj); - - // Create the renderer - const render = new Render(); - - // Add custom shapes - flowChartShapes.addToRender(render); - - // Add our custom arrow - an empty arrowhead - render.arrows().none = function normal(parent, id, edge, type) { - const marker = parent - .append('marker') - .attr('id', id) - .attr('viewBox', '0 0 10 10') - .attr('refX', 9) - .attr('refY', 5) - .attr('markerUnits', 'strokeWidth') - .attr('markerWidth', 8) - .attr('markerHeight', 6) - .attr('orient', 'auto'); - - const path = marker.append('path').attr('d', 'M 0 0 L 0 0 L 0 0 z'); - applyStyle(path, edge[type + 'Style']); - }; - - // Override normal arrowhead defined in d3. Remove style & add class to allow css styling. - render.arrows().normal = function normal(parent, id) { - const marker = parent - .append('marker') - .attr('id', id) - .attr('viewBox', '0 0 10 10') - .attr('refX', 9) - .attr('refY', 5) - .attr('markerUnits', 'strokeWidth') - .attr('markerWidth', 8) - .attr('markerHeight', 6) - .attr('orient', 'auto'); - - marker - .append('path') - .attr('d', 'M 0 0 L 10 5 L 0 10 z') - .attr('class', 'arrowheadPath') - .style('stroke-width', 1) - .style('stroke-dasharray', '1,0'); - }; - - // Set up an SVG group so that we can translate the final graph. - const svg = root.select(`[id="${id}"]`); - - // Run the renderer. This is what draws the final graph. - const element = root.select('#' + id + ' g'); - render(element, g); - - element.selectAll('g.node').attr('title', function () { - return diagObj.db.getTooltip(this.id); - }); - - // Index nodes - diagObj.db.indexNodes('subGraph' + i); - - // reposition labels - for (i = 0; i < subGraphs.length; i++) { - subG = subGraphs[i]; - if (subG.title !== 'undefined') { - const clusterRects = doc.querySelectorAll( - '#' + id + ' [id="' + diagObj.db.lookUpDomId(subG.id) + '"] rect' - ); - const clusterEl = doc.querySelectorAll( - '#' + id + ' [id="' + diagObj.db.lookUpDomId(subG.id) + '"]' - ); - - const xPos = clusterRects[0].x.baseVal.value; - const yPos = clusterRects[0].y.baseVal.value; - const _width = clusterRects[0].width.baseVal.value; - const cluster = select(clusterEl[0]); - const te = cluster.select('.label'); - te.attr('transform', `translate(${xPos + _width / 2}, ${yPos + 14})`); - te.attr('id', id + 'Text'); - - for (const className of subG.classes) { - clusterEl[0].classList.add(className); - } - } - } - - // Add label rects for non html labels - if (!conf.htmlLabels) { - const labels = doc.querySelectorAll('[id="' + id + '"] .edgeLabel .label'); - for (const label of labels) { - // Get dimensions of label - const dim = label.getBBox(); - - const rect = doc.createElementNS('http://www.w3.org/2000/svg', 'rect'); - rect.setAttribute('rx', 0); - rect.setAttribute('ry', 0); - rect.setAttribute('width', dim.width); - rect.setAttribute('height', dim.height); - // rect.setAttribute('style', 'fill:#e8e8e8;'); - - label.insertBefore(rect, label.firstChild); - } - } - setupGraphViewbox(g, svg, conf.diagramPadding, conf.useMaxWidth); - - // If node has a link, wrap it in an anchor SVG object. - const keys = [...vert.keys()]; - keys.forEach(function (key) { - const vertex = vert.get(key); - - if (vertex.link) { - const node = root.select('#' + id + ' [id="' + diagObj.db.lookUpDomId(key) + '"]'); - if (node) { - const link = doc.createElementNS('http://www.w3.org/2000/svg', 'a'); - link.setAttributeNS('http://www.w3.org/2000/svg', 'class', vertex.classes.join(' ')); - link.setAttributeNS('http://www.w3.org/2000/svg', 'href', vertex.link); - link.setAttributeNS('http://www.w3.org/2000/svg', 'rel', 'noopener'); - if (securityLevel === 'sandbox') { - link.setAttributeNS('http://www.w3.org/2000/svg', 'target', '_top'); - } else if (vertex.linkTarget) { - link.setAttributeNS('http://www.w3.org/2000/svg', 'target', vertex.linkTarget); - } - - const linkNode = node.insert(function () { - return link; - }, ':first-child'); - - const shape = node.select('.label-container'); - if (shape) { - linkNode.append(function () { - return shape.node(); - }); - } - - const label = node.select('.label'); - if (label) { - linkNode.append(function () { - return label.node(); - }); - } - } - } - }); -}; - -export default { - setConf, - addVertices, - addEdges, - getClasses, - draw, -}; diff --git a/packages/mermaid/src/diagrams/flowchart/flowRenderer.spec.js b/packages/mermaid/src/diagrams/flowchart/flowRenderer.spec.js deleted file mode 100644 index 79bf75453c..0000000000 --- a/packages/mermaid/src/diagrams/flowchart/flowRenderer.spec.js +++ /dev/null @@ -1,277 +0,0 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -import { addVertices, addEdges } from './flowRenderer.js'; -import { setConfig } from '../../diagram-api/diagramAPI.js'; - -setConfig({ - flowchart: { - htmlLabels: false, - }, -}); - -describe('the flowchart renderer', function () { - describe('when adding vertices to a graph', function () { - [ - ['round', 'rect', 5], - ['square', 'rect'], - ['diamond', 'question'], - ['hexagon', 'hexagon'], - ['odd', 'rect_left_inv_arrow'], - ['lean_right', 'lean_right'], - ['lean_left', 'lean_left'], - ['trapezoid', 'trapezoid'], - ['inv_trapezoid', 'inv_trapezoid'], - ['odd_right', 'rect_left_inv_arrow'], - ['circle', 'circle'], - ['ellipse', 'ellipse'], - ['stadium', 'stadium'], - ['subroutine', 'subroutine'], - ['cylinder', 'cylinder'], - ['group', 'rect'], - ].forEach(function ([type, expectedShape, expectedRadios = 0]) { - it(`should add the correct shaped node to the graph for vertex type ${type}`, async function () { - const fakeDiag = { - db: { - lookUpDomId: () => { - return 'my-node-id'; - }, - }, - }; - const addedNodes = []; - const mockG = { - setNode: function (id, object) { - addedNodes.push([id, object]); - }, - }; - await addVertices( - { - v1: { - type, - id: 'my-node-id', - classes: [], - styles: [], - text: 'my vertex text', - }, - }, - mockG, - 'svg-id', - undefined, - undefined, - fakeDiag - ); - expect(addedNodes).toHaveLength(1); - expect(addedNodes[0][0]).toEqual('my-node-id'); - expect(addedNodes[0][1]).toHaveProperty('id', 'my-node-id'); - expect(addedNodes[0][1]).toHaveProperty('labelType', 'svg'); - expect(addedNodes[0][1]).toHaveProperty('shape', expectedShape); - expect(addedNodes[0][1]).toHaveProperty('rx', expectedRadios); - expect(addedNodes[0][1]).toHaveProperty('ry', expectedRadios); - }); - }); - - ['Multi
Line', 'Multi
Line', 'Multi
Line', 'MultiLine'].forEach( - function (labelText) { - it('should handle multiline texts with different line breaks', async function () { - const addedNodes = []; - const fakeDiag = { - db: { - lookUpDomId: () => { - return 'my-node-id'; - }, - }, - }; - const mockG = { - setNode: function (id, object) { - addedNodes.push([id, object]); - }, - }; - await addVertices( - { - v1: { - type: 'rect', - id: 'my-node-id', - classes: [], - styles: [], - text: 'Multi
Line', - }, - }, - mockG, - 'svg-id', - false, - document, - fakeDiag - ); - expect(addedNodes).toHaveLength(1); - expect(addedNodes[0][0]).toEqual('my-node-id'); - expect(addedNodes[0][1]).toHaveProperty('id', 'my-node-id'); - expect(addedNodes[0][1]).toHaveProperty('labelType', 'svg'); - expect(addedNodes[0][1].label).toBeDefined(); - expect(addedNodes[0][1].label).toBeDefined(); // node - expect(addedNodes[0][1].label.firstChild.innerHTML).toEqual('Multi'); // node, line 1 - expect(addedNodes[0][1].label.lastChild.innerHTML).toEqual('Line'); // node, line 2 - }); - } - ); - - [ - [['fill:#fff'], 'fill:#fff;', ''], - [['color:#ccc'], '', 'color:#ccc;'], - [['fill:#fff', 'color:#ccc'], 'fill:#fff;', 'color:#ccc;'], - [ - ['fill:#fff', 'color:#ccc', 'text-align:center'], - 'fill:#fff;', - 'color:#ccc;text-align:center;', - ], - ].forEach(function ([style, expectedStyle, expectedLabelStyle]) { - it(`should add the styles to style and/or labelStyle for style ${style}`, async function () { - const addedNodes = []; - const fakeDiag = { - db: { - lookUpDomId: () => { - return 'my-node-id'; - }, - }, - }; - const mockG = { - setNode: function (id, object) { - addedNodes.push([id, object]); - }, - }; - await addVertices( - { - v1: { - type: 'rect', - id: 'my-node-id', - classes: [], - styles: style, - text: 'my vertex text', - }, - }, - mockG, - 'svg-id', - undefined, - undefined, - fakeDiag - ); - expect(addedNodes).toHaveLength(1); - expect(addedNodes[0][0]).toEqual('my-node-id'); - expect(addedNodes[0][1]).toHaveProperty('id', 'my-node-id'); - expect(addedNodes[0][1]).toHaveProperty('labelType', 'svg'); - expect(addedNodes[0][1]).toHaveProperty('style', expectedStyle); - expect(addedNodes[0][1]).toHaveProperty('labelStyle', expectedLabelStyle); - }); - }); - - it(`should add default class to all nodes which do not have another class assigned`, async function () { - const addedNodes = []; - const mockG = { - setNode: function (id, object) { - addedNodes.push([id, object]); - }, - }; - const fakeDiag = { - db: { - lookUpDomId: () => { - return 'my-node-id'; - }, - }, - }; - await addVertices( - { - v1: { - type: 'rect', - id: 'my-node-id', - classes: [], - styles: [], - text: 'my vertex text', - }, - v2: { - type: 'rect', - id: 'myNode', - classes: ['myClass'], - styles: [], - text: 'my vertex text', - }, - }, - mockG, - 'svg-id', - undefined, - undefined, - fakeDiag - ); - expect(addedNodes).toHaveLength(2); - expect(addedNodes[0][0]).toEqual('my-node-id'); - expect(addedNodes[0][1]).toHaveProperty('class', 'default'); - expect(addedNodes[1][0]).toEqual('my-node-id'); - expect(addedNodes[1][1]).toHaveProperty('class', 'myClass'); - }); - }); - - describe('when adding edges to a graph', function () { - it('should handle multiline texts and set centered label position', async function () { - const addedEdges = []; - const fakeDiag = { - db: { - lookUpDomId: () => { - return 'my-node-id'; - }, - }, - }; - const mockG = { - setEdge: function (s, e, data, c) { - addedEdges.push(data); - }, - }; - await addEdges( - [ - { text: 'Multi
Line' }, - { text: 'Multi
Line' }, - { text: 'Multi
Line' }, - { text: 'MultiLine' }, - { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi
Line' }, - { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi
Line' }, - { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'Multi
Line' }, - { style: ['stroke:DarkGray', 'stroke-width:2px'], text: 'MultiLine' }, - ], - mockG, - fakeDiag - ); - - addedEdges.forEach(function (edge) { - expect(edge).toHaveProperty('label', 'Multi\nLine'); - expect(edge).toHaveProperty('labelpos', 'c'); - }); - }); - - [ - [['stroke:DarkGray'], 'stroke:DarkGray;', ''], - [['color:red'], '', 'fill:red;'], - [['stroke:DarkGray', 'color:red'], 'stroke:DarkGray;', 'fill:red;'], - [ - ['stroke:DarkGray', 'color:red', 'stroke-width:2px'], - 'stroke:DarkGray;stroke-width:2px;', - 'fill:red;', - ], - ].forEach(function ([style, expectedStyle, expectedLabelStyle]) { - it(`should add the styles to style and/or labelStyle for style ${style}`, async function () { - const addedEdges = []; - const fakeDiag = { - db: { - lookUpDomId: () => { - return 'my-node-id'; - }, - }, - }; - const mockG = { - setEdge: function (s, e, data, c) { - addedEdges.push(data); - }, - }; - await addEdges([{ style: style, text: 'styling' }], mockG, fakeDiag); - - expect(addedEdges).toHaveLength(1); - expect(addedEdges[0]).toHaveProperty('style', expectedStyle); - expect(addedEdges[0]).toHaveProperty('labelStyle', expectedLabelStyle); - }); - }); - }); -});