Skip to content
Closed
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d31993f
fix: pie: Don't sort, keep order. Keep color order constant.
Jan 6, 2026
9bd3a45
fix: align branch label background with text for multi-line labels in…
veeceey Feb 14, 2026
002ce07
Merge branch 'develop' into fix/issue-7362-gitgraph-branch-label-alig…
veeceey Feb 19, 2026
cf76f7e
refactor(types): correct types for `createText.ts`
aloisklink Mar 1, 2026
2ef4c37
docs: add a TSDoc comment to `createText()`
aloisklink Mar 1, 2026
c541af4
Merge branch 'develop' into fix/issue-7362-gitgraph-branch-label-alig…
knsv Mar 2, 2026
220f9d7
docs: add small TSDoc comment to decodeEntities()
aloisklink Mar 2, 2026
1108184
refactor: `decodeEntities` if `htmlLabels: false`
aloisklink Mar 2, 2026
57b70b3
fix: prevent escaping `<` and `&` when `htmlLabels: false`
aloisklink Mar 2, 2026
9767066
refactor: handle `node.padding==undefined`
aloisklink Mar 2, 2026
026e65d
refactor(types): assert `.textContent` is non-null
aloisklink Mar 2, 2026
dafc6dd
refactor(types): type known `<div>` elements
aloisklink Mar 2, 2026
1f1484a
fix(sequence): add catch-all rule for ID lexer state to prevent hang
ashishjain0512 Mar 2, 2026
461d2ee
test: fix duplicate `createText` test case
aloisklink Mar 2, 2026
4037351
refactor: remove unused requirement box unescaping
aloisklink Mar 2, 2026
9a365fc
Merge pull request #7436 from aloisklink/fix/prevent-escaping-<-&-whe…
knsv Mar 3, 2026
84e7a3a
Merge pull request #7393 from veeceey/fix/issue-7362-gitgraph-branch-…
knsv Mar 3, 2026
975ce08
Merge pull request #7295 from daym/bug/5899_pie_retain_slice_order
knsv Mar 3, 2026
f37d579
Merge pull request #7437 from mermaid-js/bug/6399_sequence_hang_missi…
knsv Mar 3, 2026
35c2729
Update gitignore
sidharthv96 Mar 4, 2026
4380da4
Update gitignore
sidharthv96 Mar 4, 2026
63fd29f
fix(elk): scope rounded edge curve to ELK layout only
Mar 5, 2026
c5d73fc
fix(elk): set edge.curve on the correct object before insertEdge
Mar 5, 2026
ee0ade3
fix(elk): add curve property to edge type annotation
Mar 5, 2026
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
5 changes: 5 additions & 0 deletions .changeset/fix-gitgraph-branch-label-alignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mermaid': patch
---

fix: Align branch label background with text for multi-line labels in LR GitGraph layout
5 changes: 5 additions & 0 deletions .changeset/fix-sequence-hang-missing-space-after-as.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'mermaid': patch
---

fix: prevent sequence diagram hang when "as" is used without a trailing space in participant declarations
9 changes: 4 additions & 5 deletions .changeset/rounded-edge-curves.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
---
'mermaid': minor
'mermaid': patch
'@mermaid-js/mermaid-layout-elk': patch
---

fix: replace smooth curve edges with rounded right-angle edges
fix: use rounded right-angle edges for ELK layout

The default flowchart edge curve changes from `basis` (smooth splines) to `rounded` (right-angle segments with rounded corners). This fixes ELK layout edges that were curving instead of routing at right angles (#7213) and applies consistently across all diagram types using the shared rendering pipeline.

To restore the previous smooth curve behavior, set `flowchart.curve: 'basis'` in your config.
ELK layout edges now default to `rounded` curve (right-angle segments with rounded corners) instead of inheriting the global `basis` default. This fixes ELK edges that were curving instead of routing at right angles (#7213). Non-ELK layouts are unaffected and keep their existing `basis` default.
8 changes: 8 additions & 0 deletions .changeset/tough-baths-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'mermaid': patch
---

fix: prevent escaping `<` and `&` when `htmlLabels: false`

user: @aloisklink
user: @BambioGaming
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,6 @@ generated/
/.claude/skills/
packages/mermaid/src/diagrams/CLAUDE.md
packages/parser/CLAUDE.md
AGENTS.mermaid.md
CLAUDE.mermaid.md
.envrc
9 changes: 9 additions & 0 deletions cypress/integration/rendering/flowchart-v2.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ describe('Flowchart v2', () => {
{ htmlLabels: false, flowchart: { htmlLabels: false } }
);
});
it('5a: angle brackets should be work without html labels', () => {
imgSnapshotTest(
`flowchart TD
a["**Plain text**:\n 5 > 3 && 2 < 4"]
b["\`**Markdown**:<br> 5 > 3 && 2 < 4\`"]
`,
{ htmlLabels: false }
);
});
it('6: should render non-escaped with html labels', () => {
imgSnapshotTest(
`flowchart TD
Expand Down
15 changes: 15 additions & 0 deletions cypress/integration/rendering/gitGraph.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2164,4 +2164,19 @@ gitGraph TB:
);
});
});

it('97: should render branch labels with multi-line text aligned with background in LR layout', () => {
imgSnapshotTest(
`gitGraph LR:
commit id: "1"
branch "Feature A\n(ongoing)"
commit id: "2"
commit id: "3"
checkout main
commit id: "4"
commit id: "5"
`,
{}
);
});
});
11 changes: 8 additions & 3 deletions packages/mermaid-layout-elk/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
}: InternalHelpers,
{ algorithm }: RenderOptions
) => {
const nodeDb: Record<string, any> = {};

Check warning on line 55 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
const clusterDb: Record<string, any> = {};

Check warning on line 56 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type

const addVertex = async (
nodeEl: SVGGroup,
Expand Down Expand Up @@ -145,16 +145,16 @@
const drawNodes = async (
relX: number,
relY: number,
nodeArray: any[],

Check warning on line 148 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
svg: any,

Check warning on line 149 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
subgraphsEl: SVGGroup,
depth: number
) => {
await Promise.all(
nodeArray.map(async function (node: {
id: string | number;
x: any;

Check warning on line 156 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
y: any;

Check warning on line 157 in packages/mermaid-layout-elk/src/render.ts

View workflow job for this annotation

GitHub Actions / autofix

Unexpected any. Specify a different type
width: number;
labels: { width: any }[];
height: number;
Expand Down Expand Up @@ -347,9 +347,14 @@
edgeData.style = (edgeData.style || '') + (strokeRes.style || '');
edgeData.labelStyle = (edgeData.labelStyle || '') + (strokeRes.labelStyle || '');

// Curve
// @ts-ignore - defaults.confCurve is present at runtime but missing in type
edgeData.curve = getCurve(edge.interpolate, defaults.defaultInterpolate, defaults.confCurve);
// Curve – ELK produces orthogonal edge routes, so default to 'rounded' (right-angle
// segments with rounded corners) unless the user explicitly set an interpolation.
if (edge.interpolate !== undefined || defaults.defaultInterpolate !== undefined) {
// @ts-ignore - defaults.confCurve is present at runtime but missing in type
edgeData.curve = getCurve(edge.interpolate, defaults.defaultInterpolate, defaults.confCurve);
} else {
edgeData.curve = 'rounded';
}

// Arrowhead style + labelpos when we have label text
const hasText = (edge?.text ?? '') !== '';
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/src/diagrams/class/shapeUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ async function addText<T extends SVGGraphicsElement>(
numberOfLines = text.children.length;

const textChild = text.children[0];
if (text.textContent === '' || text.textContent.includes('&gt')) {
if (text.textContent === '' || text.textContent!.includes('&gt')) {
textChild.textContent =
textContent[0] +
textContent.substring(1).replaceAll('&gt;', '>').replaceAll('&lt;', '<').trim();
Expand Down
2 changes: 1 addition & 1 deletion packages/mermaid/src/diagrams/git/gitGraphRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ const drawBranches = (
bkg.attr('x', pos - bbox.width / 2 - 10).attr('y', maxPos);
label.attr('transform', 'translate(' + (pos - bbox.width / 2 - 5) + ', ' + maxPos + ')');
} else {
bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - bbox.height / 2) + ')');
bkg.attr('transform', 'translate(' + -19 + ', ' + (pos - 11) + ')');
}
});
};
Expand Down
63 changes: 63 additions & 0 deletions packages/mermaid/src/diagrams/pie/pieRenderer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { draw } from './pieRenderer.js';
import { parser } from './pieParser.js';
import { db } from './pieDb.js';

describe('pieRenderer', () => {
beforeEach(() => {
document.body.innerHTML = '<svg id="my-svg"></svg>';
db.clear();
});

it('should render slices in input order (not sorted by size)', async () => {
const text = `pie
"A" : 10
"B" : 100
"C" : 50`;

await parser.parse(text);

const diagObj = { db };
await draw(text, 'my-svg', '1.0.0', diagObj as any);

const slices = document.querySelectorAll('.pieCircle');
// d3 binds data to the property __data__
const sliceData = [...slices].map((el: any) => el.__data__.data.label);

expect(sliceData).toEqual(['A', 'B', 'C']);
});

it('should maintain color consistency when slices are hidden', async () => {
// A (10), B (100), C (<1 hidden), D (50)
// Colors: A->0, B->1, C->2, D->3
// Visible: A, B, D.
// D should have color 3, not 2.
const text = `pie
"A" : 10
"B" : 100
"C" : 0.1
"D" : 50`;

await parser.parse(text);

const diagObj = { db };
await draw(text, 'my-svg', '1.0.0', diagObj as any);

const slices = document.querySelectorAll('.pieCircle');
const sliceData = [...slices].map((el: any) => ({
label: el.__data__.data.label,
fill: el.getAttribute('fill'),
}));

// We verify that D has a different color than it would if C wasn't there.
// Since we can't easily predict exact d3 colors string without mocking theme,
// we can check if D's color matches what we expect from the ordinal scale index.
// But checking indices is harder on DOM.
// However, we know D should be the 4th color.
// A -> Color 1
// B -> Color 2
// D -> Color 4

// Let's at least verify D is present and A, B are present.
expect(sliceData.map((d) => d.label)).toEqual(['A', 'B', 'D']);
});
});
11 changes: 7 additions & 4 deletions packages/mermaid/src/diagrams/pie/pieRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ const createPieArcs = (sections: Sections): d3.PieArcDatum<D3Section>[] => {

const pieData: D3Section[] = [...sections.entries()]
.map(([label, value]) => ({ label, value }))
.filter((d) => (d.value / sum) * 100 >= 1) // Remove values < 1%
.sort((a, b) => b.value - a.value);
.filter((d) => (d.value / sum) * 100 >= 1); // Remove values < 1%

const pie: d3.Pie<unknown, D3Section> = d3pie<D3Section>().value((d) => d.value);
const pie: d3.Pie<unknown, D3Section> = d3pie<D3Section>()
.value((d) => d.value)
.sort(null);
return pie(pieData);
};

Expand Down Expand Up @@ -92,7 +93,9 @@ export const draw: DrawDefinition = (text, id, _version, diagObj) => {
const filteredArcs = arcs.filter((datum) => ((datum.data.value / sum) * 100).toFixed(0) !== '0');

// Set the color scale
const color: d3.ScaleOrdinal<string, 12, never> = scaleOrdinal(myGeneratedColors);
const color: d3.ScaleOrdinal<string, 12, never> = scaleOrdinal(myGeneratedColors).domain([
...sections.keys(),
]);

// Build the pie chart: each part of the pie is a path that we build using the arc function.
group
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<ID>[^<>:\n,;@\s]+(?=\s+as\s) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
<ID>[^<>:\n,;@]+(?=\s*[\n;#]|$) { yytext = yytext.trim(); this.popState(); return 'ACTOR'; }
<ID>[^<>:\n,;@]*\<[^\n]* { this.popState(); return 'INVALID'; }
<ID>[^\n]+ { yytext = yytext.trim(); this.popState(); return 'INVALID'; }
"box" { this.begin('LINE'); return 'box'; }
"participant" { this.begin('ID'); return 'participant'; }
"actor" { this.begin('ID'); return 'participant_actor'; }
Expand Down
14 changes: 14 additions & 0 deletions packages/mermaid/src/diagrams/sequence/sequenceDiagram.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2622,6 +2622,20 @@ Bob->>Alice:Got it!
expect(error).toBe(true);
});

it('should not hang when "as" is used without a space before the alias text', async () => {
let errorMessage = '';
try {
await Diagram.fromText(`
sequenceDiagram
participant X_AutoPublishable asAAAAAAAAAAAAA:AAAAAAAAAAAAA
`);
} catch (e) {
errorMessage = e instanceof Error ? e.message : String(e);
}
expect(errorMessage).not.toBe('');
expect(errorMessage).not.toContain('Lexical error');
}, 5000);

it('should parse participant with stereotype and alias', async () => {
const diagram = await Diagram.fromText(`
sequenceDiagram
Expand Down
41 changes: 40 additions & 1 deletion packages/mermaid/src/rendering-util/createText.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { select } from 'd3';
import { describe, expect, it } from 'vitest';
import { sanitizeText } from '../diagram-api/diagramAPI.js';
import mermaid from '../mermaid.js';
import { replaceIconSubstring } from './createText.js';
import { createText, replaceIconSubstring } from './createText.js';

describe('replaceIconSubstring', () => {
it('converts FontAwesome icon notations to HTML tags', async () => {
Expand Down Expand Up @@ -61,3 +62,41 @@ describe('replaceIconSubstring', () => {
expect(output).toContain(expected);
});
});

describe('createText', () => {
beforeEach(() => {
// JSDom has no SVGTSpanElement, so we need to mock getComputedTextLength to avoid errors in createText
const mock = vi.mockObject(window.SVGElement.prototype);
(mock as unknown as SVGTSpanElement).getComputedTextLength = vi.fn(() => 123.456);
});

it.for([
{
useHtmlLabels: false,
markdown: true,
},
{
useHtmlLabels: true,
markdown: true,
},
{
useHtmlLabels: false,
markdown: false,
},
{
useHtmlLabels: true,
markdown: false,
},
])(
'decodes HTML entities in text when useHtmlLabels is $useHtmlLabels and markdown is $markdown',
async ({ useHtmlLabels, markdown }) => {
const input = '5 &gt; 3 &amp;&amp; 2 &lt; 4';
const expected = '5 > 3 && 2 < 4';

const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const svgGroup = svg.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'g'));
const output = await createText(select(svgGroup), input, { useHtmlLabels, markdown });
expect(output.textContent).toEqual(expected);
}
);
});
Loading
Loading