Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/state-note-edge-neo-dashes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'mermaid': patch
---

fix(state): draw the edge between a state and its note dashed under the `neo` look. It was rendering solid.

The dashes were expressed only as CSS — `.note-edge { stroke-dasharray: 5 }` — which is enough under `classic` but not under `neo`, where `insertEdge` writes an inline `stroke-dasharray` on every edge, computed from the path length so the arrow markers keep their gaps. An inline style outranks a stylesheet rule, so the note edge took the solid pattern and the `.note-edge` rule simply lost. The note edge now declares `pattern: 'dashed'`, which is what `insertEdge` reads to choose its dash generator. `classic` is unchanged.
12 changes: 12 additions & 0 deletions packages/mermaid/src/diagrams/state/dataFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,18 @@ export const dataFetcher = (
style: G_EDGE_STYLE,
labelStyle: '',
classes: CSS_EDGE_NOTE_EDGE,
// The dashes have to be declared on the edge, not only through the `note-edge`
// class. Under `look: neo`, `insertEdge` writes an *inline* `stroke-dasharray`
// computed from the path length -- a solid run trimmed at both ends so the arrow
// markers get their gaps -- and it picks that pattern from `edge.pattern`. An
// inline style outranks the stylesheet, so a note edge that only carried the class
// was drawn solid: the `.note-edge` rule was still there and simply lost.
//
// Naming the pattern here routes it through the same dash generator every other
// dashed edge uses, so the marker gaps survive. `classic` is untouched: it writes
// no inline dasharray, and `.note-edge` still wins over `edge-pattern-dashed`
// because it is emitted later in the sheet at equal specificity.
pattern: 'dashed',
arrowheadStyle: G_EDGE_ARROWHEADSTYLE,
labelpos: G_EDGE_LABELPOS,
labelType: G_EDGE_LABELTYPE,
Expand Down
5 changes: 5 additions & 0 deletions packages/mermaid/src/diagrams/state/stateDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ export interface Edge {
thickness: string;
classes: string;
look: MermaidConfig['look'];
/**
* Stroke pattern, read by `insertEdge`. The note edge is the only state edge that sets
* it; see the note-edge push in `dataFetcher.ts` for why the CSS class is not enough.
*/
pattern?: 'solid' | 'dotted' | 'dashed';
}

/**
Expand Down
66 changes: 66 additions & 0 deletions packages/mermaid/src/diagrams/state/stateNoteEdge.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* The edge joining a state to its note is dashed. That used to be expressed only as CSS --
* `.note-edge { stroke-dasharray: 5 }` in `state/styles.js` -- which is enough under the
* `classic` look and not enough under `neo`.
*
* Under `neo`, `insertEdge` writes an *inline* `stroke-dasharray` on every edge, computed
* from the path length: for a solid edge, one long run trimmed at both ends so the arrow
* markers have their gaps. An inline style outranks a stylesheet rule, so the note edge
* came out solid with the `.note-edge` rule still present and simply losing. `neo` became
* the default look, which is what made a long-standing quirk everyone's problem.
*
* `insertEdge` chooses between the two dash generators on `edge.pattern`, so that is where
* the dashing has to be declared. This pins it there.
*/
// @ts-expect-error No types available for JISON
import stateDiagram, { parser } from './parser/stateDiagram.jison';
import { beforeEach, describe, expect, it } from 'vitest';
import { StateDB } from './stateDb.js';

describe('state note edges', () => {
let stateDb: StateDB;

beforeEach(() => {
stateDb = new StateDB(2);
parser.yy = stateDb;
stateDiagram.parser.yy = stateDb;
stateDiagram.parser.yy.clear();
});

const edges = (diagram: string) => {
parser.parse(diagram);
return stateDb.getData().edges;
};

const diagram = `stateDiagram-v2
[*] --> Active
Active --> Idle
note right of Active
A note
end note
`;

it('declares the note edge as dashed on the edge, not only in CSS', () => {
const noteEdge = edges(diagram).find((edge) => edge.classes.includes('note-edge'));
expect(noteEdge).toBeDefined();
// `insertEdge` reads this to pick its dash generator under the neo look. Without it the
// edge takes the solid generator and the inline result hides `.note-edge`.
expect(noteEdge?.pattern).toBe('dashed');
});

it('leaves ordinary transitions solid', () => {
// The fix must not dash every edge: `pattern` is absent on transitions, which is what
// sends them down the solid branch.
const transitions = edges(diagram).filter((edge) => !edge.classes.includes('note-edge'));
expect(transitions.length).toBeGreaterThan(0);
expect(transitions.every((edge) => edge.pattern === undefined)).toBe(true);
});

it('keeps the note-edge class, which is what the classic look still styles', () => {
// `classic` writes no inline dasharray, so it is the `.note-edge` rule that dashes the
// line there -- and that rule beats `edge-pattern-dashed` on source order. Dropping the
// class in favour of the pattern alone would change the classic look's dash length.
const noteEdge = edges(diagram).find((edge) => edge.classes.includes('note-edge'));
expect(noteEdge?.classes).toContain('note-edge');
});
});
Loading