diff --git a/.changeset/flowchart-edge-classdef.md b/.changeset/flowchart-edge-classdef.md new file mode 100644 index 00000000000..f96401d9a9a --- /dev/null +++ b/.changeset/flowchart-edge-classdef.md @@ -0,0 +1,14 @@ +--- +'mermaid': minor +--- + +feat(flowchart): add classDef styling support for edges via @::: inline syntax + +Introduces a new `@:::className` inline syntax for applying `classDef` styles directly to flowchart edges, mirroring the existing `:::className` syntax for nodes. + +Supported forms: + +- `edgeId@:::className` — edge with an explicit ID and a class +- `@:::className` — class only, no explicit ID + +Works on all edge types: bare arrows (`-->`), pipe-labeled (`-->|text|`), and inline-text (`-- text -->`). The existing `class ` statement syntax is unchanged. diff --git a/demos/flowchart.html b/demos/flowchart.html index 0c71a2bf84a..64f567a7beb 100644 --- a/demos/flowchart.html +++ b/demos/flowchart.html @@ -1615,6 +1615,21 @@

flowchart

B D end + +
+ +

Edge classDef styling with @::: syntax

+
+flowchart TD
+    Start:::nodeRed --> Process
+    Process okLink@:::edgeRed-->|OK| Approve
+    Process @:::edgeRed-->|Error| Retry
+    Approve @:::edgeRed--> End
+    Retry --> Process
+
+    classDef nodeRed fill:green,stroke:red
+    classDef edgeRed stroke:red,stroke-width:3px
     

diff --git a/docs/syntax/flowchart.md b/docs/syntax/flowchart.md index 323cd43584c..e5319829079 100644 --- a/docs/syntax/flowchart.md +++ b/docs/syntax/flowchart.md @@ -1281,7 +1281,67 @@ In this snippet: - `class e1 animate` applies the `animate` class to the edge `e1`. **Note on Escaping Commas:** -When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaid’s style definitions. +When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaid's style definitions. + +### Applying classDef styles inline with @::: + +You can apply a `classDef` class directly on an edge using the `@:::className` inline syntax, without needing a separate `class` statement. This mirrors how `:::className` works for nodes. + +**Class only (no edge ID):** + +```mermaid-example +flowchart LR + A @:::myEdgeClass--> B + classDef myEdgeClass stroke:red,stroke-width:3px +``` + +```mermaid +flowchart LR + A @:::myEdgeClass--> B + classDef myEdgeClass stroke:red,stroke-width:3px +``` + +**With an edge ID and a class:** + +```mermaid-example +flowchart LR + A myLink@:::myEdgeClass--> B + classDef myEdgeClass stroke:red,stroke-width:3px +``` + +```mermaid +flowchart LR + A myLink@:::myEdgeClass--> B + classDef myEdgeClass stroke:red,stroke-width:3px +``` + +Both forms also work with labeled edges: + +```mermaid-example +flowchart TD + Start:::nodeStyle --> Process + Process okLink@:::edgeStyle-->|OK| Approve + Process @:::edgeStyle-->|Error| Retry + Approve @:::edgeStyle--> End + Retry --> Process + + classDef nodeStyle fill:green,stroke:red + classDef edgeStyle stroke:red,stroke-width:3px +``` + +```mermaid +flowchart TD + Start:::nodeStyle --> Process + Process okLink@:::edgeStyle-->|OK| Approve + Process @:::edgeStyle-->|Error| Retry + Approve @:::edgeStyle--> End + Retry --> Process + + classDef nodeStyle fill:green,stroke:red + classDef edgeStyle stroke:red,stroke-width:3px +``` + +The `@:::className` token must appear between the source node and the arrow. The class is applied to the edge path, label, and arrowhead. ## New arrow types diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts index 3e4034e3d7e..460cb90e795 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts @@ -166,6 +166,34 @@ describe('flow db getData', () => { }); }); +describe('flow db getData with edge classDef', () => { + let flowDb: FlowDB; + beforeEach(() => { + flowDb = new FlowDB(); + }); + + it('should include edge classDef class name in SVG classes string', () => { + flowDb.addVertex('A', { text: 'A', type: 'text' }, undefined, [], [], '', {}, undefined); + flowDb.addVertex('B', { text: 'B', type: 'text' }, undefined, [], [], '', {}, undefined); + flowDb.addLink(['A'], ['B'], { classes: ['myEdgeClass'] }); + flowDb.addClass('myEdgeClass', ['stroke:red', 'stroke-width:3px']); + + const { edges } = flowDb.getData(); + expect(edges[0].classes).toContain('myEdgeClass'); + }); + + it('should compile classDef styles into cssCompiledStyles for edges', () => { + flowDb.addVertex('A', { text: 'A', type: 'text' }, undefined, [], [], '', {}, undefined); + flowDb.addVertex('B', { text: 'B', type: 'text' }, undefined, [], [], '', {}, undefined); + flowDb.addLink(['A'], ['B'], { classes: ['myEdgeClass'] }); + flowDb.addClass('myEdgeClass', ['stroke:red', 'stroke-width:3px']); + + const { edges } = flowDb.getData(); + expect(edges[0].cssCompiledStyles).toBeDefined(); + expect(edges[0].cssCompiledStyles!.length).toBeGreaterThan(0); + }); +}); + describe('flow db direction', () => { let flowDb: FlowDB; beforeEach(() => { diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.ts b/packages/mermaid/src/diagrams/flowchart/flowDb.ts index fa3db3af681..bba9817a7d9 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.ts @@ -303,6 +303,9 @@ export class FlowDB implements DiagramDB { edge.stroke = type.stroke; edge.length = type.length > 10 ? 10 : type.length; } + if (type?.classes) { + edge.classes.push(...type.classes); + } if (id && !this.edges.some((e) => e.id === id)) { edge.id = id; edge.isUserDefinedId = true; @@ -1150,7 +1153,8 @@ You have to call mermaid.initialize.` classes: rawEdge?.stroke === 'invisible' ? '' - : 'edge-thickness-normal edge-pattern-solid flowchart-link', + : 'edge-thickness-normal edge-pattern-solid flowchart-link' + + (rawEdge.classes.length > 0 ? ' ' + rawEdge.classes.join(' ') : ''), arrowTypeStart: rawEdge?.stroke === 'invisible' || rawEdge?.type === 'arrow_open' ? 'none' 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 7b8f71be439..5bb8d217ac2 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js @@ -386,4 +386,132 @@ describe('[Style] when parsing', () => { expect(vert.get('D').classes[0]).toBe('C1'); expect(vert.get('E').classes[0]).toBe('C2'); }); + + describe('classDef on edges', function () { + it('should apply a class to an edge using @::: inline syntax (no edge ID)', function () { + flow.parser.parse(` + flowchart TD + A @:::red--> B + classDef red stroke:red + `); + const edges = flow.parser.yy.getEdges(); + expect(edges.length).toBe(1); + expect(edges[0].classes).toContain('red'); + }); + + it('should apply a class to an edge with an ID using @::: inline syntax', function () { + flow.parser.parse(` + flowchart TD + A okLink@:::red--> B + classDef red stroke:red + `); + const edges = flow.parser.yy.getEdges(); + expect(edges.length).toBe(1); + expect(edges[0].id).toBe('okLink'); + expect(edges[0].classes).toContain('red'); + }); + + it('should apply a class to a labeled edge using @::: inline syntax (pipe label)', function () { + flow.parser.parse(` + flowchart TD + A @:::red-->|OK| B + classDef red stroke:red + `); + const edges = flow.parser.yy.getEdges(); + expect(edges.length).toBe(1); + expect(edges[0].classes).toContain('red'); + expect(edges[0].text).toBe('OK'); + }); + + it('should apply a class to a labeled edge with ID using @::: inline syntax (pipe label)', function () { + flow.parser.parse(` + flowchart TD + A okLink@:::red-->|OK| B + classDef red stroke:red + `); + const edges = flow.parser.yy.getEdges(); + expect(edges.length).toBe(1); + expect(edges[0].id).toBe('okLink'); + expect(edges[0].classes).toContain('red'); + expect(edges[0].text).toBe('OK'); + }); + + it('should apply a class to an edge using inline text label with @::: syntax (no ID)', function () { + flow.parser.parse(` + flowchart TD + A @:::red-- label --> B + classDef red stroke:red + `); + const edges = flow.parser.yy.getEdges(); + expect(edges.length).toBe(1); + expect(edges[0].classes).toContain('red'); + expect(edges[0].text).toBe('label'); + }); + + it('should apply a class to an edge using inline text label with @::: syntax (with ID)', function () { + flow.parser.parse(` + flowchart TD + A okLink@:::red-- label --> B + classDef red stroke:red + `); + const edges = flow.parser.yy.getEdges(); + expect(edges.length).toBe(1); + expect(edges[0].id).toBe('okLink'); + expect(edges[0].classes).toContain('red'); + expect(edges[0].text).toBe('label'); + }); + + it('should apply a class to an edge via the class statement using a user-defined edge ID', function () { + flow.parser.parse(` + flowchart TD + A okLink@--> B + class okLink red + classDef red stroke:red + `); + const edges = flow.parser.yy.getEdges(); + expect(edges.length).toBe(1); + expect(edges[0].id).toBe('okLink'); + expect(edges[0].classes).toContain('red'); + }); + + it('should compile classDef styles for an edge with @::: syntax', function () { + flow.parser.parse(` + flowchart TD + A @:::red--> B + classDef red stroke:red,stroke-width:3px + `); + const edges = flow.parser.yy.getEdges(); + const classes = flow.parser.yy.getClasses(); + expect(edges[0].classes).toContain('red'); + expect(classes.get('red').styles).toContain('stroke:red'); + expect(classes.get('red').styles).toContain('stroke-width:3px'); + }); + + it('should support the example from the feature request', function () { + flow.parser.parse(` + flowchart TD + Start:::nodeRed --> Process + Process okLink@:::edgeRed-->|OK| Approve + Process @:::edgeRed-->|Error| Retry + Approve @:::edgeRed--> End + Retry --> Process + classDef nodeRed fill:green,stroke:red + classDef edgeRed stroke:red + `); + const edges = flow.parser.yy.getEdges(); + // Edge from Start to Process: no class + expect(edges[0].classes).toEqual([]); + // Process okLink@:::edgeRed-->|OK| Approve + expect(edges[1].id).toBe('okLink'); + expect(edges[1].classes).toContain('edgeRed'); + expect(edges[1].text).toBe('OK'); + // Process @:::edgeRed-->|Error| Retry + expect(edges[2].classes).toContain('edgeRed'); + expect(edges[2].text).toBe('Error'); + // Approve @:::edgeRed--> End + expect(edges[3].classes).toContain('edgeRed'); + // Retry --> Process: no class + expect(edges[4].classes).toEqual([]); + }); + }); }); diff --git a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison index 7340cf8d3aa..ed590802502 100644 --- a/packages/mermaid/src/diagrams/flowchart/parser/flow.jison +++ b/packages/mermaid/src/diagrams/flowchart/parser/flow.jison @@ -142,6 +142,8 @@ that id. .*direction\s+LR[^\n]* return 'direction_lr'; .*direction\s+TD[^\n]* return 'direction_td'; +[^\s\"]+\@":::"[^\s\-=.~]+ { return 'LINK_ID_CLASS'; } +\@":::"[^\s\-=.~]+ { return 'LINK_CLASS'; } [^\s\"]+\@(?=[^\{\"]) { return 'LINK_ID'; } [0-9]+ return 'NUM'; \# return 'BRKT'; @@ -478,6 +480,12 @@ link: linkStatement arrowText {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};} + | LINK_ID_CLASS START_LINK edgeText LINK + {var parts = $LINK_ID_CLASS.split('@:::'); var id = parts[0]+'@'; var cls = parts[1]; + var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText, "id": id, "classes": [cls]};} + | LINK_CLASS START_LINK edgeText LINK + {var cls = $LINK_CLASS.substring(4); + var inf = yy.destructLink($LINK, $START_LINK); $$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length,"text":$edgeText, "classes": [cls]};} ; edgeText: edgeTextToken @@ -495,6 +503,12 @@ 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};} + | LINK_ID_CLASS LINK + {var parts = $LINK_ID_CLASS.split('@:::'); var id = parts[0]+'@'; var cls = parts[1]; + var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length, "id": id, "classes": [cls]};} + | LINK_CLASS LINK + {var cls = $LINK_CLASS.substring(4); + var inf = yy.destructLink($LINK);$$ = {"type":inf.type,"stroke":inf.stroke,"length":inf.length, "classes": [cls]};} ; arrowText: diff --git a/packages/mermaid/src/docs/syntax/flowchart.md b/packages/mermaid/src/docs/syntax/flowchart.md index 8eb24b70b47..7c7398b5869 100644 --- a/packages/mermaid/src/docs/syntax/flowchart.md +++ b/packages/mermaid/src/docs/syntax/flowchart.md @@ -779,7 +779,43 @@ In this snippet: - `class e1 animate` applies the `animate` class to the edge `e1`. **Note on Escaping Commas:** -When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaid’s style definitions. +When setting the `stroke-dasharray` property, remember to escape commas as `\,` since commas are used as delimiters in Mermaid's style definitions. + +### Applying classDef styles inline with @::: + +You can apply a `classDef` class directly on an edge using the `@:::className` inline syntax, without needing a separate `class` statement. This mirrors how `:::className` works for nodes. + +**Class only (no edge ID):** + +```mermaid-example +flowchart LR + A @:::myEdgeClass--> B + classDef myEdgeClass stroke:red,stroke-width:3px +``` + +**With an edge ID and a class:** + +```mermaid-example +flowchart LR + A myLink@:::myEdgeClass--> B + classDef myEdgeClass stroke:red,stroke-width:3px +``` + +Both forms also work with labeled edges: + +```mermaid-example +flowchart TD + Start:::nodeStyle --> Process + Process okLink@:::edgeStyle-->|OK| Approve + Process @:::edgeStyle-->|Error| Retry + Approve @:::edgeStyle--> End + Retry --> Process + + classDef nodeStyle fill:green,stroke:red + classDef edgeStyle stroke:red,stroke-width:3px +``` + +The `@:::className` token must appear between the source node and the arrow. The class is applied to the edge path, label, and arrowhead. ## New arrow types diff --git a/packages/mermaid/src/rendering-util/rendering-elements/edges.js b/packages/mermaid/src/rendering-util/rendering-elements/edges.js index 5bad2e061a0..a97b00cf232 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/edges.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/edges.js @@ -566,11 +566,12 @@ export const insertEdge = function ( const tail = startNode; var head = endNode; const edgeClassStyles = []; - for (const key in edge.cssCompiledStyles) { + for (const style of edge.cssCompiledStyles ?? []) { + const key = style.split(':')[0]?.trim(); if (isLabelStyle(key)) { continue; } - edgeClassStyles.push(edge.cssCompiledStyles[key]); + edgeClassStyles.push(style); } log.debug('UIO intersect check', edge.points, head.x, tail.x);