diff --git a/cypress/platform/cyto.html b/cypress/platform/cyto.html new file mode 100644 index 000000000..f58036fc5 --- /dev/null +++ b/cypress/platform/cyto.html @@ -0,0 +1,86 @@ + +
+ + + + + + + + ++flowchart TD +id1 --> id2--> id3[Iam number 3]--> id4--> id5 --> id1 ++ + + + + diff --git a/cypress/platform/knsv.html b/cypress/platform/knsv.html index 6ce545082..1167defbe 100644 --- a/cypress/platform/knsv.html +++ b/cypress/platform/knsv.html @@ -38,7 +38,7 @@ -
+flowchart LR classDef aPID stroke:#4e4403,fill:#fdde29,color:#4e4403,rx:5px,ry:5px; classDef crm stroke:#333333,fill:#DCDCDC,color:#333333,rx:5px,ry:5px; @@ -71,10 +71,12 @@ flowchart TD end end+ --> id2 --> id3(I have a long text) --> id4 --> id5 --> id1flowchart TD -id +id1+flowchart LR a["Haiya"]===>b @@ -99,7 +101,7 @@ flowchart TD class A someclass; class C someclass;-+sequenceDiagram title: My Sequence Diagram Title accTitle: My Acc Sequence Diagram @@ -109,14 +111,14 @@ flowchart TD John-->>Alice: Great! Alice-)John: See you later!-+graph TD A -->|000| B B -->|111| C linkStyle 1 stroke:#ff3,stroke-width:4px,color:red;-+journey accTitle: My User Journey Diagram accDescr: My User Journey Diagram Description @@ -130,10 +132,10 @@ graph TD Go downstairs: 5: Me Sit down: 5: Me-+info-+requirementDiagram accTitle: My req Diagram accDescr: My req Diagram Description @@ -174,7 +176,7 @@ requirementDiagram test_req - contains -> test_req3 test_req <- copies - test_entity2-+gantt dateFormat YYYY-MM-DD title Adding GANTT diagram functionality to mermaid @@ -206,7 +208,7 @@ gantt Add gantt diagram to demo page :20h Add another diagram to demo page :48h-+stateDiagram state Active { Idle @@ -234,7 +236,7 @@ stateDiagram end B ->> A: Return-+classDiagram accTitle: My class diagram accDescr: My class diagram Description @@ -259,7 +261,7 @@ class Class10 { A->>Bob: Hola Bob-->A: Pasten !-+gitGraph commit id: "ZERO" branch develop @@ -288,7 +290,7 @@ flowchart TD C -->|Two| E[iPhone] C -->|Three| F[fa:fa-car Car]-+classDiagram Animal "1" <|-- Duck Animal <|-- Fish @@ -311,7 +313,7 @@ flowchart TD +run() }-+erDiagram CAR ||--o{ NAMED-DRIVER : allows CAR { @@ -343,6 +345,8 @@ flowchart TD useMaxWidth: true, htmlLabels: false, fontFamily: 'courier', + defaultRenderer: 'cytoscape', + // defaultRenderer: 'dagre-wrapper', }, }); function callback() { diff --git a/src/dagre-wrapper/nodes.js b/src/dagre-wrapper/nodes.js index 344210e93..6c4b1d785 100644 --- a/src/dagre-wrapper/nodes.js +++ b/src/dagre-wrapper/nodes.js @@ -8,8 +8,6 @@ import note from './shapes/note'; import { parseMember } from '../diagrams/class/svgDraw'; import { evaluate, sanitizeText as sanitize } from '../diagrams/common/common'; -const sanitizeText = (txt) => sanitize(txt, getConfig()); - const question = (parent, node) => { const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); @@ -1064,6 +1062,7 @@ export const insertNode = (elem, node, dir) => { if (node.haveCallback) { nodeElems[node.id].attr('class', nodeElems[node.id].attr('class') + ' clickable'); } + return newEl; }; export const setNodeElem = (elem, node) => { nodeElems[node.id] = elem; diff --git a/src/diagram-api/diagram-orchestration.ts b/src/diagram-api/diagram-orchestration.ts index b9b0520c9..fbe8f4955 100644 --- a/src/diagram-api/diagram-orchestration.ts +++ b/src/diagram-api/diagram-orchestration.ts @@ -41,9 +41,11 @@ import erStyles from '../diagrams/er/styles'; import flowParser from '../diagrams/flowchart/parser/flow'; import { flowDetector } from '../diagrams/flowchart/flowDetector'; import { flowDetectorV2 } from '../diagrams/flowchart/flowDetector-v2'; +import { flowDetectorV3 } from '../diagrams/flowchart/flowDetector-v3'; import flowDb from '../diagrams/flowchart/flowDb'; import flowRenderer from '../diagrams/flowchart/flowRenderer'; import flowRendererV2 from '../diagrams/flowchart/flowRenderer-v2'; +import flowRendererV3 from '../diagrams/flowchart/flowRenderer-v3'; import flowStyles from '../diagrams/flowchart/styles'; // @ts-ignore: TODO Fix ts errors @@ -305,6 +307,20 @@ export const addDiagrams = () => { }, flowDetectorV2 ); + registerDiagram( + 'flowchart-v3', + { + parser: flowParser, + db: flowDb, + renderer: flowRendererV3, + styles: flowStyles, + init: () => { + flowDb.clear(); + flowDb.setGen('gen-3'); + }, + }, + flowDetectorV3 + ); registerDiagram( 'gitGraph', { parser: gitGraphParser, db: gitGraphDb, renderer: gitGraphRenderer, styles: gitGraphStyles }, diff --git a/src/diagrams/flowchart/flowDetector-v2.ts b/src/diagrams/flowchart/flowDetector-v2.ts index f73748c79..71673e779 100644 --- a/src/diagrams/flowchart/flowDetector-v2.ts +++ b/src/diagrams/flowchart/flowDetector-v2.ts @@ -1,8 +1,23 @@ import type { DiagramDetector } from '../../diagram-api/detectType'; - +import { log } from '../../diagram-api/diagramAPI'; export const flowDetectorV2: DiagramDetector = (txt, config) => { + log.info('Config in flowDetector-v2 defaultRenderer: ', config?.flowchart?.defaultRenderer); + + // If we have confgured to use cytoscape then we should always return false here + if (config?.flowchart?.defaultRenderer === 'cytoscape') { + log.info('flowDetector-v2 returning false'); + return false; + } else { + log.info('flowDetector-v2 not cytoscape'); + } + // If we have confgured to use dagre-wrapper then we should return true in this function for graph code thus making it use the new flowchart diagram if (config?.flowchart?.defaultRenderer === 'dagre-wrapper' && txt.match(/^\s*graph/) !== null) return true; + log.info('Config in flowDetector-v2 returning', txt.match(/^\s*flowchart/) !== null); return txt.match(/^\s*flowchart/) !== null; }; + +/* +if (((_a = config === null || config === void 0 ? void 0 : config.flowchart) === null || _a === void 0 ? void 0 : _a.defaultRenderer) === 'dagre-wrapper1' && txt.match(/^\s*graph/) !== null) +*/ diff --git a/src/diagrams/flowchart/flowDetector-v3.ts b/src/diagrams/flowchart/flowDetector-v3.ts new file mode 100644 index 000000000..f5622855c --- /dev/null +++ b/src/diagrams/flowchart/flowDetector-v3.ts @@ -0,0 +1,8 @@ +import type { DiagramDetector } from '../../diagram-api/detectType'; + +export const flowDetectorV3: DiagramDetector = (txt, config) => { + // If we have confgured to use dagre-wrapper then we should return true in this function for graph code thus making it use the new flowchart diagram + if (config?.flowchart?.defaultRenderer === 'cytoscape' && txt.match(/^\s*graph/) !== null) + return true; + return txt.match(/^\s*flowchart/) !== null; +}; diff --git a/src/diagrams/flowchart/flowRenderer-v2.js b/src/diagrams/flowchart/flowRenderer-v2.js index 6b7c4c1bf..2837b2939 100644 --- a/src/diagrams/flowchart/flowRenderer-v2.js +++ b/src/diagrams/flowchart/flowRenderer-v2.js @@ -12,7 +12,7 @@ import { interpolateToCurve, getStylesFromArray } from '../../utils'; import { setupGraphViewbox } from '../../setupGraphViewbox'; import addSVGAccessibilityFields from '../../accessibility'; -const conf = {}; +let conf = {}; export const setConf = function (cnf) { const keys = Object.keys(cnf); for (let i = 0; i < keys.length; i++) { @@ -351,6 +351,7 @@ export const getClasses = function (text, diagObj) { */ export const draw = function (text, id, _version, diagObj) { + // const conf = config.getConfig(_version); log.info('Drawing flowchart'); diagObj.db.clear(); flowDb.setGen('gen-2'); diff --git a/src/diagrams/flowchart/flowRenderer-v3.js b/src/diagrams/flowchart/flowRenderer-v3.js new file mode 100644 index 000000000..fe5c5cc62 --- /dev/null +++ b/src/diagrams/flowchart/flowRenderer-v3.js @@ -0,0 +1,548 @@ +import graphlib from 'graphlib'; +import { select, curveLinear, selectAll } from 'd3'; + +import flowDb from './flowDb'; +import { getConfig } from '../../config'; +import { insertNode } from '../../dagre-wrapper/nodes.js'; +import { render } from '../../dagre-wrapper/index.js'; +import addHtmlLabel from 'dagre-d3/lib/label/add-html-label.js'; +import { log } from '../../logger'; +import common, { evaluate } from '../common/common'; +import { interpolateToCurve, getStylesFromArray } from '../../utils'; +import { setupGraphViewbox } from '../../setupGraphViewbox'; +import addSVGAccessibilityFields from '../../accessibility'; + +import cytoscape from 'cytoscape'; + +const conf = {}; +export const setConf = function (cnf) { + const keys = Object.keys(cnf); + for (let i = 0; i < keys.length; i++) { + conf[keys[i]] = cnf[keys[i]]; + } +}; + +// /** +// * 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 = function (vert, cy, svgId, root, doc, diagObj) { + const svg = root.select(`[id="${svgId}"]`); + const nodes = svg.insert('g').attr('class', 'nodes'); + const keys = Object.keys(vert); + + // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition + keys.forEach(function (id) { + 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 node = { + label: vertexText.replace( + /fa[lrsb]?:fa-[\w-]+/g, + (s) => `` + ), + }; + 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 (let j = 0; j < rows.length; j++) { + 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 = rows[j]; + svgLabel.appendChild(tspan); + } + vertexNode = svgLabel; + } + + let radious = 0; + let _shape = ''; + // Set the shape based parameters + switch (vertex.type) { + case 'round': + radious = 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'; + } + // // Add the node + const node = { + labelStyle: styles.labelStyle, + shape: _shape, + labelText: vertexText, + rx: radious, + ry: radious, + 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, + }; + const nodeEl = insertNode(nodes, node, vertex.dir); + const boundingBox = nodeEl.node().getBBox(); + cy.add({ + group: 'nodes', + data: { + id: vertex.id, + labelStyle: styles.labelStyle, + shape: _shape, + labelText: vertexText, + rx: radious, + ry: radious, + class: classStr, + style: styles.style, + 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, + boundingBox, + el: nodeEl, + }, + }); + + log.trace('setNode', { + labelStyle: styles.labelStyle, + shape: _shape, + labelText: vertexText, + rx: radious, + ry: radious, + 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 + * @param cy + * @param diagObj + */ +export const addEdges = function (edges, cy, diagObj) { + // log.info('abc78 edges = ', edges); + let cnt = 0; + let linkIdCnt = {}; + + let defaultStyle; + let defaultLabelStyle; + + if (typeof edges.defaultStyle !== 'undefined') { + const defaultStyles = getStylesFromArray(edges.defaultStyle); + defaultStyle = defaultStyles.style; + defaultLabelStyle = defaultStyles.labelStyle; + } + + edges.forEach(function (edge) { + cnt++; + + // Identify Link + var linkIdBase = 'L-' + edge.start + '-' + edge.end; + // count the links from+to the same node to give unique id + if (typeof 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]); + var linkNameStart = 'LS-' + edge.start; + var 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 (typeof defaultStyle !== 'undefined') { + style = defaultStyle; + } + if (typeof 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; + } + if (typeof 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 (typeof edge.interpolate !== 'undefined') { + edgeData.curve = interpolateToCurve(edge.interpolate, curveLinear); + } else if (typeof edges.defaultInterpolate !== 'undefined') { + edgeData.curve = interpolateToCurve(edges.defaultInterpolate, curveLinear); + } else { + edgeData.curve = interpolateToCurve(conf.curve, curveLinear); + } + + if (typeof edge.text === 'undefined') { + if (typeof edge.style !== 'undefined') { + edgeData.arrowheadStyle = 'fill: #333'; + } + } else { + edgeData.arrowheadStyle = 'fill: #333'; + edgeData.labelpos = 'c'; + } + + edgeData.labelType = 'text'; + edgeData.label = edge.text.replace(common.lineBreakRegex, '\n'); + + if (typeof 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 + cy.add({ group: 'edges', data: { source: edge.start, target: edge.end, edgeData, id: cnt } }); + }); +}; + +/** + * Returns the all the styles from classDef statements in the graph definition. + * + * @param text + * @param diagObj + * @returns {object} ClassDef styles + */ +export const getClasses = function (text, diagObj) { + log.info('Extracting classes'); + diagObj.db.clear(); + try { + // Parse the graph definition + diagObj.parse(text); + return diagObj.db.getClasses(); + } catch (e) { + return; + } +}; + +/** + * Draws a flowchart in the tag with id: id based on the graph definition in text. + * + * @param text + * @param id + */ + +export const draw = function (text, id, _version, diagObj) { + const cy = cytoscape({ + // styleEnabled: false, + // animate: false, + // ready: function () { + // log.info('Ready', this); + // }, + container: document.getElementById('cy'), // container to render in + + elements: [ + // list of graph elements to start with + // { // node a + // data: { id: 'a' } + // }, + // { // node b + // data: { id: 'b' } + // }, + // { // edge ab + // data: { id: 'ab', source: 'a', target: 'b' } + // } + ], + + style: [ + // the stylesheet for the graph + { + selector: 'node', + style: { + 'background-color': '#666', + label: 'data(labelText)', + }, + }, + + { + selector: 'edge', + style: { + width: 3, + 'line-color': '#ccc', + 'target-arrow-color': '#ccc', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + label: 'data(id)', + }, + }, + ], + + layout: { + name: 'breadthfirst', + rows: 1, + }, + }); + log.info('Drawing flowchart using v3 renderer'); + // Fetch the default direction, use TD if none was found + let dir = diagObj.db.getDirection(); + if (typeof 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; + + const c = cytoscape({ + // name: 'cose', + name: 'cose', + container: null, + layout: { + boundingBox: { + x1: 0, + y1: 0, + w: 200, + h: 200, + }, + }, + headless: true, + styleEnabled: false, + animate: false, + ready: function () { + log.info('Ready', this); + }, + }); + + const svg = root.select(`[id="${id}"]`); + const edgesEl = svg.insert('g').attr('class', 'edges edgePath'); + + // Fetch the vertices/nodes and edges/links from the parsed graph definition + const vert = diagObj.db.getVertices(); + addVertices(vert, cy, id, root, doc, diagObj); + // c.style(); + // Make cytoscape care about the dimensisions of the nodes + cy.nodes().forEach(function (n) { + n.layoutDimensions = () => { + const boundingBox = n.data().boundingBox; + return { w: boundingBox.width, h: boundingBox.height }; + }; + }); + + const edges = diagObj.db.getEdges(); + addEdges(edges, cy, diagObj); + + cy.layout({ + // name: 'grid', + name: 'circle', + // name: 'cose' + }).run(); + cy.nodes().map((node, id) => { + const data = node.data(); + log.info( + 'Position: (', + node.position().x, + ', ', + node.position().y, + ')', + node.layoutDimensions() + ); + data.el.attr('transform', `translate(${node.position().x}, ${node.position().y})`); + }); + + cy.edges().map((edge, id) => { + const data = edge.data(); + if (edge[0]._private.bodyBounds) { + const bounds = edge[0]._private.bodyBounds; + log.info( + id, + // 'x:', + // edge.controlPoints(), + // 'y:', + edge[0]._private + // 'w:', + // edge.boundingbox().w, + // 'h:', + // edge.boundingbox().h, + // edge.midPoint() + ); + // data.el.attr('transform', `translate(${node.position().x}, ${node.position().y})`); + edgesEl + .insert('line') + .attr('x1', bounds.x1) + .attr('y1', bounds.y1) + .attr('x2', bounds.x2) + .attr('y2', bounds.y2) + .attr('class', 'path'); + } + }); + log.info(cy.json()); + setupGraphViewbox({}, svg, conf.diagramPadding, conf.useMaxWidth); +}; + +export default { + // setConf, + // addVertices, + // addEdges, + getClasses, + draw, +};