Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/flowchart-edge-classdef.md
Original file line number Diff line number Diff line change
@@ -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 <edgeId> <className>` statement syntax is unchanged.
15 changes: 15 additions & 0 deletions demos/flowchart.html
Original file line number Diff line number Diff line change
Expand Up @@ -1615,6 +1615,21 @@ <h3>flowchart</h3>
B
D
end
</pre
>
<hr />

<h2>Edge classDef styling with @::: syntax</h2>
<pre class="mermaid">
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
</pre>
<hr />

Expand Down
62 changes: 61 additions & 1 deletion docs/syntax/flowchart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions packages/mermaid/src/diagrams/flowchart/flowDb.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
6 changes: 5 additions & 1 deletion packages/mermaid/src/diagrams/flowchart/flowDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'
Expand Down
128 changes: 128 additions & 0 deletions packages/mermaid/src/diagrams/flowchart/parser/flow-style.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});
});
});
14 changes: 14 additions & 0 deletions packages/mermaid/src/diagrams/flowchart/parser/flow.jison
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
38 changes: 37 additions & 1 deletion packages/mermaid/src/docs/syntax/flowchart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading