diff --git a/cypress/platform/knsv2.html b/cypress/platform/knsv2.html index 1c7bda8e7..dfcd8aa4f 100644 --- a/cypress/platform/knsv2.html +++ b/cypress/platform/knsv2.html @@ -84,29 +84,66 @@ /* tspan { font-size: 6px !important; } */ + /* .flowchart-link { + stroke-dasharray: 4, 4 !important; + animation: flow 1s linear infinite; + animation: dashdraw 4.93282s linear infinite; + stroke-width: 2px !important; + } */ + + @keyframes dashdraw { + from { + stroke-dashoffset: 0; + } + } + + /*stroke-width:2;stroke-dasharray:10.000000,9.865639;stroke-dashoffset:-198.656393;animation: 4.932820s linear infinite;*/ + /* stroke-width:2;stroke-dasharray:10.000000,9.865639;stroke-dashoffset:-198.656393;animation: dashdraw 4.932820s linear infinite;*/
----- -config: - layout: elk ---- +flowchart LR - subgraph S2 - subgraph s1["APA"] - D{"Use the editor"} - end - - - D -- Mermaid js --> I{"fa:fa-code Text"} - D --> I - D --> I - - end + A --> B+ flowchart LR + A e1@==> B + e1@{ animate: true} +++flowchart LR + A e1@--> B + classDef animate stroke-width:2,stroke-dasharray:10\,8,stroke-dashoffset:-180,animation: edge-animation-frame 6s linear infinite, stroke-linecap: round + class e1 animate ++infinite
++flowchart LR + A e1@--> B + classDef animate stroke-dasharray: 9\,5,stroke-dashoffset: 900,animation: dash 25s linear infinite; + class e1 animate ++Mermaid - edge-animation-slow
++flowchart LR + A e1@--> B +e1@{ animation: fast} ++Mermaid - edge-animation-fast
++flowchart LR + A e1@--> B + classDef animate stroke-dasharray: 1000,stroke-dashoffset: 1000,animation: dash 10s linear; + class e1 edge-animation-fast ++ ++ +info+--- config: layout: elk @@ -131,7 +168,7 @@ config: end end-+--- config: layout: elk @@ -144,7 +181,7 @@ config: D-->I D-->I-+--- config: layout: elk @@ -183,7 +220,7 @@ flowchart LR n8@{ shape: rect}-+--- config: layout: elk @@ -199,7 +236,7 @@ flowchart LR-+--- config: layout: elk @@ -208,7 +245,7 @@ flowchart LR A{A} --> B & C-+--- config: layout: elk @@ -220,7 +257,7 @@ flowchart LR end-+--- config: layout: elk diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts index 1dbc789c9..ccb8a8e94 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts @@ -24,7 +24,7 @@ import type { FlowLink, FlowVertexTypeParam, } from './types.js'; -import type { NodeMetaData } from '../../types.js'; +import type { NodeMetaData, EdgeMetaData } from '../../types.js'; const MERMAID_DOM_ID_PREFIX = 'flowchart-'; let vertexCounter = 0; @@ -71,12 +71,38 @@ export const addVertex = function ( classes: string[], dir: string, props = {}, - shapeData: any + metadata: any ) { - // console.log('addVertex', id, shapeData); if (!id || id.trim().length === 0) { return; } + // Extract the metadata from the shapeData, the syntax for adding metadata for nodes and edges is the same + // so at this point we don't know if it's a node or an edge, but we can still extract the metadata + let doc; + if (metadata !== undefined) { + let yamlData; + // detect if shapeData contains a newline character + if (!metadata.includes('\n')) { + yamlData = '{\n' + metadata + '\n}'; + } else { + yamlData = metadata + '\n'; + } + doc = yaml.load(yamlData, { schema: yaml.JSON_SCHEMA }) as NodeMetaData; + } + + // Check if this is an edge + const edge = edges.find((e) => e.id === id); + if (edge) { + const edgeDoc = doc as EdgeMetaData; + if (edgeDoc?.animate) { + edge.animate = edgeDoc.animate; + } + if (edgeDoc?.animation) { + edge.animation = edgeDoc.animation; + } + return; + } + let txt; let vertex = vertices.get(id); @@ -128,19 +154,7 @@ export const addVertex = function ( 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 !== undefined) { 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.`); @@ -187,11 +201,18 @@ export const addVertex = function ( * Function called by parser when a link/edge definition has been found * */ -export const addSingleLink = function (_start: string, _end: string, type: any) { +export const addSingleLink = function (_start: string, _end: string, type: any, id?: string) { const start = _start; const end = _end; - const edge: FlowEdge = { start: start, end: end, type: undefined, text: '', labelType: 'text' }; + const edge: FlowEdge = { + start: start, + end: end, + type: undefined, + text: '', + labelType: 'text', + classes: [], + }; log.info('abc78 Got edge...', edge); const linkTextObj = type.text; @@ -210,6 +231,9 @@ export const addSingleLink = function (_start: string, _end: string, type: any) edge.stroke = type.stroke; edge.length = type.length > 10 ? 10 : type.length; } + if (id) { + edge.id = id; + } if (edges.length < (config.maxEdges ?? 500)) { log.info('Pushing edge...'); @@ -225,11 +249,17 @@ You have to call mermaid.initialize.` } }; -export const addLink = function (_start: string[], _end: string[], type: unknown) { - log.info('addLink', _start, _end, type); +export const addLink = function (_start: string[], _end: string[], linkData: unknown) { + const id = + linkData && typeof linkData === 'object' && 'id' in linkData + ? linkData.id?.replace('@', '') + : undefined; + + log.info('addLink', _start, _end, id); + for (const start of _start) { for (const end of _end) { - addSingleLink(start, end, type); + addSingleLink(start, end, linkData, id); } } }; @@ -282,7 +312,13 @@ export const updateLink = function (positions: ('default' | number)[], style: st }); }; -export const addClass = function (ids: string, style: string[]) { +export const addClass = function (ids: string, _style: string[]) { + const style = _style + .join() + .replace(/\\,/g, '§§§') + .replace(/,/g, ';') + .replace(/§§§/g, ',') + .split(';'); ids.split(',').forEach(function (id) { let classNode = classes.get(id); if (classNode === undefined) { @@ -337,6 +373,10 @@ export const setClass = function (ids: string, className: string) { if (vertex) { vertex.classes.push(className); } + const edge = edges.find((e) => e.id === id); + if (edge) { + edge.classes.push(className); + } const subGraph = subGraphLookup.get(id); if (subGraph) { subGraph.classes.push(className); @@ -997,7 +1037,7 @@ export const getData = () => { styles.push(...rawEdge.style); } const edge: Edge = { - id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }), + id: getEdgeId(rawEdge.start, rawEdge.end, { counter: index, prefix: 'L' }, rawEdge.id), start: rawEdge.start, end: rawEdge.end, type: rawEdge.type ?? 'normal', @@ -1009,14 +1049,20 @@ export const getData = () => { rawEdge?.stroke === 'invisible' ? '' : 'edge-thickness-normal edge-pattern-solid flowchart-link', - arrowTypeStart: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeStart, - arrowTypeEnd: rawEdge?.stroke === 'invisible' ? 'none' : arrowTypeEnd, + arrowTypeStart: + rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open' ? 'none' : arrowTypeStart, + arrowTypeEnd: + rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open' ? 'none' : arrowTypeEnd, arrowheadStyle: 'fill: #333', + cssCompiledStyles: getCompiledStyles(rawEdge.classes), labelStyle: styles, style: styles, pattern: rawEdge.stroke, look: config.look, + animate: rawEdge.animate, + animation: rawEdge.animation, }; + edges.push(edge); }); 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..5682c9bed 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-edges.spec.js @@ -39,6 +39,27 @@ const doubleEndedEdges = [ { edgeStart: '<==', edgeEnd: '==>', stroke: 'thick', type: 'double_arrow_point' }, { edgeStart: '<-.', edgeEnd: '.->', stroke: 'dotted', type: 'double_arrow_point' }, ]; +const regularEdges = [ + { edgeStart: '--', edgeEnd: '--x', stroke: 'normal', type: 'arrow_cross' }, + { edgeStart: '==', edgeEnd: '==x', stroke: 'thick', type: 'arrow_cross' }, + { edgeStart: '-.', edgeEnd: '.-x', stroke: 'dotted', type: 'arrow_cross' }, + { edgeStart: '--', edgeEnd: '--o', stroke: 'normal', type: 'arrow_circle' }, + { edgeStart: '==', edgeEnd: '==o', stroke: 'thick', type: 'arrow_circle' }, + { edgeStart: '-.', edgeEnd: '.-o', stroke: 'dotted', type: 'arrow_circle' }, + { edgeStart: '--', edgeEnd: '-->', stroke: 'normal', type: 'arrow_point' }, + { edgeStart: '==', edgeEnd: '==>', stroke: 'thick', type: 'arrow_point' }, + { edgeStart: '-.', edgeEnd: '.->', stroke: 'dotted', type: 'arrow_point' }, + + { edgeStart: '--', edgeEnd: '----x', stroke: 'normal', type: 'arrow_cross' }, + { edgeStart: '==', edgeEnd: '====x', stroke: 'thick', type: 'arrow_cross' }, + { edgeStart: '-.', edgeEnd: '...-x', stroke: 'dotted', type: 'arrow_cross' }, + { edgeStart: '--', edgeEnd: '----o', stroke: 'normal', type: 'arrow_circle' }, + { edgeStart: '==', edgeEnd: '====o', stroke: 'thick', type: 'arrow_circle' }, + { edgeStart: '-.', edgeEnd: '...-o', stroke: 'dotted', type: 'arrow_circle' }, + { edgeStart: '--', edgeEnd: '---->', stroke: 'normal', type: 'arrow_point' }, + { edgeStart: '==', edgeEnd: '====>', stroke: 'thick', type: 'arrow_point' }, + { edgeStart: '-.', edgeEnd: '...->', stroke: 'dotted', type: 'arrow_point' }, +]; describe('[Edges] when parsing', () => { beforeEach(function () { @@ -67,6 +88,74 @@ describe('[Edges] when parsing', () => { expect(edges[0].type).toBe('arrow_circle'); }); + describe('edges with ids', function () { + describe('open ended edges with ids and labels', function () { + regularEdges.forEach((edgeType) => { + it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () { + const res = flow.parser.parse( + `flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;` + ); + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + expect(vert.get('A').id).toBe('A'); + expect(vert.get('B').id).toBe('B'); + expect(edges.length).toBe(1); + expect(edges[0].id).toBe('e1'); + expect(edges[0].start).toBe('A'); + expect(edges[0].end).toBe('B'); + expect(edges[0].type).toBe(`${edgeType.type}`); + expect(edges[0].text).toBe(''); + expect(edges[0].stroke).toBe(`${edgeType.stroke}`); + }); + it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () { + const res = flow.parser.parse( + `flowchart TD;\nA e1@${edgeType.edgeStart}${edgeType.edgeEnd} B;` + ); + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + expect(vert.get('A').id).toBe('A'); + expect(vert.get('B').id).toBe('B'); + expect(edges.length).toBe(1); + expect(edges[0].id).toBe('e1'); + expect(edges[0].start).toBe('A'); + expect(edges[0].end).toBe('B'); + expect(edges[0].type).toBe(`${edgeType.type}`); + expect(edges[0].text).toBe(''); + expect(edges[0].stroke).toBe(`${edgeType.stroke}`); + }); + }); + it('should handle normal edges where you also have a node with metadata', function () { + const res = flow.parser.parse(`flowchart LR +A id1@-->B +A@{ shape: 'rect' } +`); + const edges = flow.parser.yy.getEdges(); + + expect(edges[0].id).toBe('id1'); + }); + }); + describe('double ended edges with ids and labels', function () { + doubleEndedEdges.forEach((edgeType) => { + it(`should handle ${edgeType.stroke} ${edgeType.type} with text`, function () { + const res = flow.parser.parse( + `flowchart TD;\nA e1@${edgeType.edgeStart} label ${edgeType.edgeEnd} B;` + ); + const vert = flow.parser.yy.getVertices(); + const edges = flow.parser.yy.getEdges(); + expect(vert.get('A').id).toBe('A'); + expect(vert.get('B').id).toBe('B'); + expect(edges.length).toBe(1); + expect(edges[0].id).toBe('e1'); + expect(edges[0].start).toBe('A'); + expect(edges[0].end).toBe('B'); + expect(edges[0].type).toBe(`${edgeType.type}`); + expect(edges[0].text).toBe('label'); + expect(edges[0].stroke).toBe(`${edgeType.stroke}`); + }); + }); + }); + }); + describe('edges', function () { doubleEndedEdges.forEach((edgeType) => { it(`should handle ${edgeType.stroke} ${edgeType.type} with no text`, function () { diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison index b3df82fa5..fbd30fa9e 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison @@ -141,6 +141,7 @@ that id. .*direction\s+RL[^\n]* return 'direction_rl'; .*direction\s+LR[^\n]* return 'direction_lr'; +[^\s]+\@(?=[^\{]) { return 'LINK_ID'; } [0-9]+ return 'NUM'; \# return 'BRKT'; ":::" return 'STYLE_SEPARATOR'; @@ -201,7 +202,9 @@ that id. "*" return 'MULT'; "#" return 'BRKT'; "&" return 'AMP'; -([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ return 'NODE_STRING'; +([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|\-(?=[^\>\-\.])|=(?!=))+ { + return 'NODE_STRING'; +} "-" return 'MINUS' [\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]| [\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]| @@ -361,7 +364,7 @@ spaceList statement : vertexStatement separator - { /* console.warn('finat vs', $vertexStatement.nodes); */ $$=$vertexStatement.nodes} + { $$=$vertexStatement.nodes} | styleStatement separator {$$=[];} | linkStyleStatement separator @@ -472,6 +475,8 @@ link: linkStatement arrowText {$$ = $linkStatement;} | START_LINK edgeText LINK {var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText};} + | LINK_ID START_LINK edgeText LINK + {var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText, "id": $LINK_ID};} ; edgeText: edgeTextToken @@ -487,6 +492,8 @@ edgeText: edgeTextToken linkStatement: LINK {var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length};} + | LINK_ID LINK + {var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length, "id": $LINK_ID};} ; arrowText: diff --git a/packages/mermaid/src/diagrams/flowchart/types.ts b/packages/mermaid/src/diagrams/flowchart/types.ts index b2c5cf620..00acb6751 100644 --- a/packages/mermaid/src/diagrams/flowchart/types.ts +++ b/packages/mermaid/src/diagrams/flowchart/types.ts @@ -62,6 +62,10 @@ export interface FlowEdge { length?: number; text: string; labelType: 'text'; + classes: string[]; + id?: string; + animation?: 'fast' | 'slow'; + animate?: boolean; } export interface FlowClass { diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index a6a7a55f7..2581d342f 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -9,6 +9,7 @@ import { curveBasis, line, select } from 'd3'; import rough from 'roughjs'; import createLabel from './createLabel.js'; import { addEdgeMarkers } from './edgeMarker.ts'; +import { isLabelStyle } from './shapes/handDrawnShapeStyles.js'; const edgeLabels = new Map(); const terminalLabels = new Map(); @@ -429,6 +430,14 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod const tail = startNode; var head = endNode; + const edgeClassStyles = []; + for (const key in edge.cssCompiledStyles) { + if (isLabelStyle(key)) { + continue; + } + edgeClassStyles.push(edge.cssCompiledStyles[key]); + } + if (head.intersect && tail.intersect) { points = points.slice(1, edge.points.length - 1); points.unshift(tail.intersect(points[0])); @@ -521,12 +530,27 @@ export const insertEdge = function (elem, edge, clusterDb, diagramType, startNod svgPath.attr('d', d); elem.node().appendChild(svgPath.node()); } else { + const stylesFromClasses = edgeClassStyles.join(';'); + const styles = edge.edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : ''; + let animationClass = ''; + if (edge.animate) { + animationClass = ' edge-animation-fast'; + } + if (edge.animation) { + animationClass = ' edge-animation-' + edge.animation; + } svgPath = elem .append('path') .attr('d', linePath) .attr('id', edge.id) - .attr('class', ' ' + strokeClasses + (edge.classes ? ' ' + edge.classes : '')) - .attr('style', edgeStyles ? edgeStyles.reduce((acc, style) => acc + ';' + style, '') : ''); + .attr( + 'class', + ' ' + + strokeClasses + + (edge.classes ? ' ' + edge.classes : '') + + (animationClass ? animationClass : '') + ) + .attr('style', stylesFromClasses + ';' + styles); } // DEBUG code, DO NOT REMOVE diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts index 80e2a4423..4ac6b2ddd 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/handDrawnShapeStyles.ts @@ -32,7 +32,28 @@ export const styles2Map = (styles: string[]) => { }); return styleMap; }; - +export const isLabelStyle = (key: string) => { + return ( + key === 'color' || + key === 'font-size' || + key === 'font-family' || + key === 'font-weight' || + key === 'font-style' || + key === 'text-decoration' || + key === 'text-align' || + key === 'text-transform' || + key === 'line-height' || + key === 'letter-spacing' || + key === 'word-spacing' || + key === 'text-shadow' || + key === 'text-overflow' || + key === 'white-space' || + key === 'word-wrap' || + key === 'word-break' || + key === 'overflow-wrap' || + key === 'hyphens' + ); +}; export const styles2String = (node: Node) => { const { stylesArray } = compileStyles(node); const labelStyles: string[] = []; @@ -42,26 +63,7 @@ export const styles2String = (node: Node) => { stylesArray.forEach((style) => { const key = style[0]; - if ( - key === 'color' || - key === 'font-size' || - key === 'font-family' || - key === 'font-weight' || - key === 'font-style' || - key === 'text-decoration' || - key === 'text-align' || - key === 'text-transform' || - key === 'line-height' || - key === 'letter-spacing' || - key === 'word-spacing' || - key === 'text-shadow' || - key === 'text-overflow' || - key === 'white-space' || - key === 'word-wrap' || - key === 'word-break' || - key === 'overflow-wrap' || - key === 'hyphens' - ) { + if (isLabelStyle(key)) { labelStyles.push(style.join(':') + ' !important'); } else { nodeStyles.push(style.join(':') + ' !important'); diff --git a/packages/mermaid/src/rendering-util/types.ts b/packages/mermaid/src/rendering-util/types.ts index 86cfd50b3..d64594218 100644 --- a/packages/mermaid/src/rendering-util/types.ts +++ b/packages/mermaid/src/rendering-util/types.ts @@ -101,6 +101,7 @@ export interface Edge { arrowheadStyle?: string; arrowTypeEnd?: string; arrowTypeStart?: string; + cssCompiledStyles?: string[]; // Flowchart specific properties defaultInterpolate?: string; end?: string; diff --git a/packages/mermaid/src/styles.ts b/packages/mermaid/src/styles.ts index 78b514c40..2cb11f146 100644 --- a/packages/mermaid/src/styles.ts +++ b/packages/mermaid/src/styles.ts @@ -27,7 +27,28 @@ const getStyles = ( font-size: ${options.fontSize}; fill: ${options.textColor} } - + @keyframes edge-animation-frame { + from { + stroke-dashoffset: 0; + } + } + @keyframes dash { + to { + stroke-dashoffset: 0; + } + } + & .edge-animation-slow { + stroke-dasharray: 9,5 !important; + stroke-dashoffset: 900; + animation: dash 50s linear infinite; + stroke-linecap: round; + } + & .edge-animation-fast { + stroke-dasharray: 9,5 !important; + stroke-dashoffset: 900; + animation: dash 20s linear infinite; + stroke-linecap: round; + } /* Classes common for multiple diagrams */ & .error-icon { diff --git a/packages/mermaid/src/types.ts b/packages/mermaid/src/types.ts index 5587ca3f4..fdccae677 100644 --- a/packages/mermaid/src/types.ts +++ b/packages/mermaid/src/types.ts @@ -12,6 +12,11 @@ export interface NodeMetaData { assigned?: string; ticket?: string; } + +export interface EdgeMetaData { + animation?: 'fast' | 'slow'; + animate?: boolean; +} import type { MermaidConfig } from './config.type.js'; export interface Point { diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index c1d674834..68b5e2889 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -937,8 +937,12 @@ export const getEdgeId = ( counter?: number; prefix?: string; suffix?: string; - } + }, + id?: string ) => { + if (id) { + return id; + } return `${prefix ? `${prefix}_` : ''}${from}_${to}_${counter}${suffix ? `_${suffix}` : ''}`; };