From 281cd7b0705a7cdf4295bfd5e3171647dc809dfb Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Wed, 19 Aug 2026 14:29:37 +0200 Subject: [PATCH 01/18] fix: class diagram markers no longer scale with edge stroke-width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class diagram relation markers (composition, aggregation, extension, dependency, lollipop) never set `markerUnits`, so they fell back to the SVG default of `strokeWidth` and were scaled by the referencing edge's stroke width. `class/styles.js` applies `stroke-width: ${options.strokeWidth}` to `path.relation` unconditionally, so themes that set `themeVariables.strokeWidth` to 2 — redux, redux-dark, redux-color, redux-dark-color, neo, neo-dark — drew these markers at twice their size. At 2x they overshoot the line-end offset from `markerOffsets` and end up hidden behind the class box. Only the default `classic` look was affected: `look: 'neo'` uses the `-margin` marker variants, which already set `markerUnits="userSpaceOnUse"`, as does `extensionStart` — which is why some markers looked correct while others did not. Set `markerUnits="userSpaceOnUse"` on the nine markers that lacked it, matching what the `-margin` variants already did. These five marker families are requested only by the class renderer, so no other diagram type is affected. The existing neo e2e suite only exercised `look: 'neo'` and therefore never covered the plain markers; CLASSIC-1/CLASSIC-2 close that gap. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/tidy-markers-userspaceonuse.md | 5 +++ .../rendering/class/classDiagram-neo.spec.js | 26 +++++++++++++++ .../rendering-elements/markers.js | 9 +++++ .../rendering-elements/markers.spec.ts | 33 +++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 .changeset/tidy-markers-userspaceonuse.md create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/markers.spec.ts diff --git a/.changeset/tidy-markers-userspaceonuse.md b/.changeset/tidy-markers-userspaceonuse.md new file mode 100644 index 00000000000..071c693de10 --- /dev/null +++ b/.changeset/tidy-markers-userspaceonuse.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: class diagram relation markers (composition, aggregation, extension, dependency, lollipop) no longer scale with the edge stroke width, so they stay outside the class box boundary in themes that set `strokeWidth: 2` (`redux`, `redux-dark`, `redux-color`, `redux-dark-color`, `neo`, `neo-dark`) with the default `classic` look. diff --git a/cypress/integration/rendering/class/classDiagram-neo.spec.js b/cypress/integration/rendering/class/classDiagram-neo.spec.js index e4b769abfc3..3fa07f17019 100644 --- a/cypress/integration/rendering/class/classDiagram-neo.spec.js +++ b/cypress/integration/rendering/class/classDiagram-neo.spec.js @@ -183,3 +183,29 @@ describe('Class diagram — Neo look with new themes', () => { }); }); }); + +// The neo/redux themes set `themeVariables.strokeWidth` to 2, which lands on `path.relation`. +// Relation markers must not scale with it, otherwise they overshoot the line-end offset and end +// up drawn behind the class box. The `look: 'neo'` cases above use the `-margin` marker variants, +// so only the default `classic` look exercises the plain markers. +describe('Class diagram — Classic look with new themes', () => { + themes.forEach(({ theme, label }) => { + it(`CLASSIC-1 [${label}]: should render relation markers outside the class box`, () => { + imgSnapshotTest(diagrams.allRelationships, { + logLevel: 1, + htmlLabels: true, + theme, + }); + }); + }); + + themes.forEach(({ theme, label }) => { + it(`CLASSIC-2 [${label}]: should render cardinality with classic look`, () => { + imgSnapshotTest(diagrams.cardinality, { + logLevel: 1, + htmlLabels: true, + theme, + }); + }); + }); +}); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/markers.js b/packages/mermaid/src/rendering-util/rendering-elements/markers.js index 0f84756edb0..95942347840 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/markers.js +++ b/packages/mermaid/src/rendering-util/rendering-elements/markers.js @@ -35,6 +35,7 @@ const extension = (elem, type, id) => { .attr('markerWidth', 20) .attr('markerHeight', 28) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('path') .attr('d', 'M 1,1 V 13 L18,7 Z'); // this is actual shape for arrowhead @@ -83,6 +84,7 @@ const composition = (elem, type, id) => { .attr('markerWidth', 190) .attr('markerHeight', 240) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); @@ -96,6 +98,7 @@ const composition = (elem, type, id) => { .attr('markerWidth', 20) .attr('markerHeight', 28) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); @@ -141,6 +144,7 @@ const aggregation = (elem, type, id) => { .attr('markerWidth', 190) .attr('markerHeight', 240) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); @@ -154,6 +158,7 @@ const aggregation = (elem, type, id) => { .attr('markerWidth', 20) .attr('markerHeight', 28) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('path') .attr('d', 'M 18,7 L9,13 L1,7 L9,1 Z'); @@ -198,6 +203,7 @@ const dependency = (elem, type, id) => { .attr('markerWidth', 190) .attr('markerHeight', 240) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('path') .attr('d', 'M 5,7 L9,13 L1,7 L9,1 Z'); @@ -211,6 +217,7 @@ const dependency = (elem, type, id) => { .attr('markerWidth', 20) .attr('markerHeight', 28) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('path') .attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z'); elem @@ -254,6 +261,7 @@ const lollipop = (elem, type, id) => { .attr('markerWidth', 190) .attr('markerHeight', 240) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('circle') .attr('fill', 'transparent') .attr('cx', 7) @@ -270,6 +278,7 @@ const lollipop = (elem, type, id) => { .attr('markerWidth', 190) .attr('markerHeight', 240) .attr('orient', 'auto') + .attr('markerUnits', 'userSpaceOnUse') .append('circle') .attr('fill', 'transparent') .attr('cx', 7) diff --git a/packages/mermaid/src/rendering-util/rendering-elements/markers.spec.ts b/packages/mermaid/src/rendering-util/rendering-elements/markers.spec.ts new file mode 100644 index 00000000000..73b3fdd2645 --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/markers.spec.ts @@ -0,0 +1,33 @@ +import { select } from 'd3'; +import { describe, expect, it } from 'vitest'; +import insertMarkers from './markers.js'; + +/** + * The class diagram relation markers must not scale with the edge's stroke-width. + * + * Themes such as `redux`/`neo` set `themeVariables.strokeWidth` to 2, which becomes the + * `stroke-width` of `path.relation`. Markers default to `markerUnits="strokeWidth"`, so any + * marker missing an explicit `markerUnits` gets drawn at twice its size and overshoots the + * line-end offset from `markerOffsets`, ending up hidden behind the class box. + */ +const classDiagramMarkers = ['aggregation', 'extension', 'composition', 'dependency', 'lollipop']; + +const renderMarkers = () => { + const svg = select(document.body).append('svg'); + insertMarkers(svg, classDiagramMarkers, 'classDiagram', 'test'); + return svg; +}; + +describe('class diagram markers', () => { + it('sets markerUnits="userSpaceOnUse" on every marker so size is independent of stroke-width', () => { + const svg = renderMarkers(); + + const offenders = svg + .selectAll('marker') + .nodes() + .filter((marker) => (marker as Element).getAttribute('markerUnits') !== 'userSpaceOnUse') + .map((marker) => (marker as Element).id); + + expect(offenders).toEqual([]); + }); +}); From c66200bc2302006c908f77819c584109f50c06e7 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Thu, 20 Aug 2026 09:43:20 +0200 Subject: [PATCH 02/18] fix(elk): build markers with the host instance's config With `look: 'neo'`, the first diagram rendered on a page with `layout: 'elk'` got its markers built from default theme variables: state diagram arrowheads stayed dark on dark themes, and the ER / requirement crow's-foot markers were drawn at the default stroke width. `@mermaid-js/layout-elk` is bundled with its own copy of mermaid, so it runs its own `config.ts` - one that never sees `mermaid.initialize()`. That copy is populated by `syncElkPackageConfig()`, which runs from `prepareLayout`, but `createCommonLayoutRenderer` calls `insertMarkers` before `prepareLayout`, so the first render reads defaults. Only the `*_neo` markers are affected, since they bake theme values into presentation attributes at creation time while the classic markers leave colour to the diagram stylesheet. Prefer the `insertMarkers` helper the host instance already passes in, as `mermaid-layout-tidy-tree` already does, so markers are always created against the initialized config. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/elk-neo-marker-theme-config.md | 5 +++++ .../rendering-util/layout-algorithms/common/index.ts | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changeset/elk-neo-marker-theme-config.md diff --git a/.changeset/elk-neo-marker-theme-config.md b/.changeset/elk-neo-marker-theme-config.md new file mode 100644 index 00000000000..ed66fba8917 --- /dev/null +++ b/.changeset/elk-neo-marker-theme-config.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: neo-look arrowheads and crow's-foot markers no longer fall back to default theme colours/stroke widths on the first render with `layout: elk`. State diagram arrowheads stayed dark on dark themes, and ER / requirement markers were drawn at the default stroke width, because markers were created from the layout package's own bundled copy of mermaid, whose config had not been initialized yet. diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts b/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts index 17403fe4bc7..c744babc939 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts +++ b/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts @@ -133,7 +133,17 @@ export function createCommonLayoutRenderer< options?: RenderOptions ): Promise { const element = svg.select('g') as unknown as D3Selection; - insertMarkers(element, data4Layout.markers, data4Layout.type, data4Layout.diagramId); + // Use the helper handed over by the host mermaid instance when there is one. + // External layout packages (elk, tidy-tree) are bundled with their own copy of + // these modules, and that copy's config module never sees `mermaid.initialize()`, + // so markers created through the statically imported `insertMarkers` read default + // theme variables instead of the diagram's. + (helpers?.insertMarkers ?? insertMarkers)( + element, + data4Layout.markers, + data4Layout.type, + data4Layout.diagramId + ); clearLayoutRenderState(); // Convenience struct containing everything you need to render From 8265aa514a4b937c23b0cf47cb1eb8c722888176 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 07:49:48 +0000 Subject: [PATCH 03/18] [autofix.ci] apply automated fixes --- docs/config/setup/mermaid/functions/clearLayoutRenderState.md | 2 +- docs/config/setup/mermaid/functions/defaultMeasureLayout.md | 2 +- docs/config/setup/mermaid/functions/paintLayoutData.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/config/setup/mermaid/functions/clearLayoutRenderState.md b/docs/config/setup/mermaid/functions/clearLayoutRenderState.md index 50423361baf..43063787df0 100644 --- a/docs/config/setup/mermaid/functions/clearLayoutRenderState.md +++ b/docs/config/setup/mermaid/functions/clearLayoutRenderState.md @@ -12,7 +12,7 @@ > **clearLayoutRenderState**(): `void` -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:196](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L196) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:206](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L206) ## Returns diff --git a/docs/config/setup/mermaid/functions/defaultMeasureLayout.md b/docs/config/setup/mermaid/functions/defaultMeasureLayout.md index ef1642dff52..99ee73946b7 100644 --- a/docs/config/setup/mermaid/functions/defaultMeasureLayout.md +++ b/docs/config/setup/mermaid/functions/defaultMeasureLayout.md @@ -12,7 +12,7 @@ > **defaultMeasureLayout**(`data4Layout`, `__namedParameters`): `Promise`<{ `graph`: `Graph`; `groups`: { `clusters`: `D3Selection`<`SVGGElement`>; `edgeLabels`: `D3Selection`<`SVGGElement`>; `edgePaths`: `D3Selection`<`SVGGElement`>; `nodes`: `D3Selection`<`SVGGElement`>; `rootGroups`: `D3Selection`<`SVGGElement`>; }; `nodeElements`: `Map`<`string`, `D3Selection`<`SVGElement` | `SVGGElement`>>; }> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L203) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L213) ## Parameters diff --git a/docs/config/setup/mermaid/functions/paintLayoutData.md b/docs/config/setup/mermaid/functions/paintLayoutData.md index 47e3da79f29..4420840feac 100644 --- a/docs/config/setup/mermaid/functions/paintLayoutData.md +++ b/docs/config/setup/mermaid/functions/paintLayoutData.md @@ -12,7 +12,7 @@ > **paintLayoutData**(`data4Layout`, `context`, `options`): `Promise`<`void`> -Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:210](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L210) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:220](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L220) ## Parameters From 31ce60a596746c76dc932ab540d910a6c7fff8be Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Thu, 20 Aug 2026 10:37:53 +0200 Subject: [PATCH 04/18] fix(c4): wrap element labels to c4.width again C4 element labels stopped wrapping in 11.17.0. The unified-shapes label helper gated wrapping on the root-level `wrap` option, which has no schema default and is therefore undefined, so the wrap width was always Infinity and long descriptions rendered on one unbroken line, growing the shape well past the configured c4.width. Gate on `c4.wrap` (schema default true) instead, which is what the legacy renderer used via getConfig().c4. The helper already computes the same wrap width the legacy path applied (216 - 2 * 20 = 176), and drawRect's Math.max already treats node.width as a minimum, so the box settles back at c4.width once the label wraps -- no sizing change is needed. Boundaries and relationships are still drawn by the legacy path, where the per-shape wrap flag is frozen at parse time, so they remain unwrapped (#7949). --- .changeset/c4-wrap-element-labels.md | 7 +++ .../shapes/c4LabelHelper.spec.ts | 62 +++++++++++++++++++ .../shapes/c4LabelHelper.ts | 8 ++- 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 .changeset/c4-wrap-element-labels.md create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.spec.ts diff --git a/.changeset/c4-wrap-element-labels.md b/.changeset/c4-wrap-element-labels.md new file mode 100644 index 00000000000..a36557177d7 --- /dev/null +++ b/.changeset/c4-wrap-element-labels.md @@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix(c4): wrap element labels to `c4.width` again + +C4 element labels (`System`, `Container`, `Component`, `Person` and their `_Ext` variants) stopped wrapping in 11.17.0, so long descriptions rendered on one unbroken line and the shape grew sideways well past the configured `c4.width`. The unified-shapes label helper gated wrapping on the root-level `wrap` option, which has no schema default and is therefore `undefined`; it now gates on `c4.wrap` (default `true`), which is what the legacy renderer used. diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.spec.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.spec.ts new file mode 100644 index 00000000000..b63a8e6ac7b --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.spec.ts @@ -0,0 +1,62 @@ +import { select } from 'd3'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { reset, setConfig } from '../../../config.js'; +import type { Node } from '../../types.js'; +import { c4LabelHelper } from './c4LabelHelper.js'; + +// jsdom implements neither of these SVG measurement APIs. A flat per-character +// metric is enough: the assertions below count wrapped lines, not pixels. +const PX_PER_CHAR = 7; + +beforeAll(() => { + // @ts-expect-error -- jsdom's SVGElement has no getComputedTextLength + SVGElement.prototype.getComputedTextLength = function () { + return (this.textContent ?? '').length * PX_PER_CHAR; + }; + // @ts-expect-error -- jsdom's SVGElement has no getBBox + SVGElement.prototype.getBBox = function () { + return { x: 0, y: 0, width: (this.textContent ?? '').length * PX_PER_CHAR, height: 20 }; + }; +}); + +afterEach(() => { + reset(); +}); + +/** A `System(...)` element as `buildC4Node` hands it over: `c4.width` and `c4ShapePadding`. */ +const c4Node = (): Node => + ({ + id: 'SystemAA', + label: 'Internet Banking System', + stereotype: '[Software System]', + description: ['Allows customers to view information about their bank accounts'], + width: 216, + padding: 20, + }) as unknown as Node; + +const renderLabel = async (node: Node) => { + const svg = select(document.body).append('svg'); + await c4LabelHelper(svg as never, node); + return svg; +}; + +const outerTspans = (svg: ReturnType>) => + svg.selectAll('tspan.text-outer-tspan').size(); + +describe('c4LabelHelper', () => { + it('wraps element labels by default, so the label stays within c4.width', async () => { + const svg = await renderLabel(c4Node()); + + // Three sections (name, stereotype, description) produce exactly 3 outer + // tspan elements when nothing wraps; the long description has to break further. + expect(outerTspans(svg)).toBeGreaterThan(3); + }); + + it('does not wrap when c4.wrap is disabled', async () => { + setConfig({ c4: { wrap: false } }); + const svg = await renderLabel(c4Node()); + + // One line per section, none of them broken. + expect(outerTspans(svg)).toBe(3); + }); +}); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.ts index f3100d0cebb..4a1b78de27a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.ts @@ -46,9 +46,11 @@ export const c4LabelHelper = async ( const wrapWidth = node.width ? Math.max(node.width - 2 * (node.padding ?? 0), MIN_WRAP_WIDTH) : (getConfig().flowchart?.wrappingWidth ?? 200); - // Wrapping is opt-in via the root `wrap` config, matching the legacy C4 - // renderer; the (currently ignored) c4.wrap option is tracked in #7949. - const width = config.wrap ? wrapWidth : Number.POSITIVE_INFINITY; + // `c4.wrap` (schema default true) is this diagram's own auto-wrap flag, and is what + // the legacy renderer gated on. The root-level `wrap` has no schema default, so + // reading it here left every C4 label unwrapped. + const shouldWrap = config.c4?.wrap ?? true; + const width = shouldWrap ? wrapWidth : Number.POSITIVE_INFINITY; const rendered = await Promise.all( sections.map(async (section) => { From 876e5dd747e22f05f42c915d613168e55d613d8c Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Thu, 20 Aug 2026 10:37:53 +0200 Subject: [PATCH 05/18] fix(c4): wrap element labels to c4.width again C4 element labels stopped wrapping in 11.17.0. The unified-shapes label helper gated wrapping on the root-level `wrap` option, which has no schema default and is therefore undefined, so the wrap width was always Infinity and long descriptions rendered on one unbroken line, growing the shape well past the configured c4.width. Gate on `c4.wrap` (schema default true) instead, which is what the legacy renderer used via getConfig().c4. The helper already computes the same wrap width the legacy path applied (216 - 2 * 20 = 176), and drawRect's Math.max already treats node.width as a minimum, so the box settles back at c4.width once the label wraps -- no sizing change is needed. Boundaries and relationships are still drawn by the legacy path, where the per-shape wrap flag is frozen at parse time, so they remain unwrapped (#7949). --- .changeset/c4-wrap-element-labels.md | 7 +++ .../shapes/c4LabelHelper.spec.ts | 62 +++++++++++++++++++ .../shapes/c4LabelHelper.ts | 8 ++- 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 .changeset/c4-wrap-element-labels.md create mode 100644 packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.spec.ts diff --git a/.changeset/c4-wrap-element-labels.md b/.changeset/c4-wrap-element-labels.md new file mode 100644 index 00000000000..a36557177d7 --- /dev/null +++ b/.changeset/c4-wrap-element-labels.md @@ -0,0 +1,7 @@ +--- +'mermaid': patch +--- + +fix(c4): wrap element labels to `c4.width` again + +C4 element labels (`System`, `Container`, `Component`, `Person` and their `_Ext` variants) stopped wrapping in 11.17.0, so long descriptions rendered on one unbroken line and the shape grew sideways well past the configured `c4.width`. The unified-shapes label helper gated wrapping on the root-level `wrap` option, which has no schema default and is therefore `undefined`; it now gates on `c4.wrap` (default `true`), which is what the legacy renderer used. diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.spec.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.spec.ts new file mode 100644 index 00000000000..b63a8e6ac7b --- /dev/null +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.spec.ts @@ -0,0 +1,62 @@ +import { select } from 'd3'; +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { reset, setConfig } from '../../../config.js'; +import type { Node } from '../../types.js'; +import { c4LabelHelper } from './c4LabelHelper.js'; + +// jsdom implements neither of these SVG measurement APIs. A flat per-character +// metric is enough: the assertions below count wrapped lines, not pixels. +const PX_PER_CHAR = 7; + +beforeAll(() => { + // @ts-expect-error -- jsdom's SVGElement has no getComputedTextLength + SVGElement.prototype.getComputedTextLength = function () { + return (this.textContent ?? '').length * PX_PER_CHAR; + }; + // @ts-expect-error -- jsdom's SVGElement has no getBBox + SVGElement.prototype.getBBox = function () { + return { x: 0, y: 0, width: (this.textContent ?? '').length * PX_PER_CHAR, height: 20 }; + }; +}); + +afterEach(() => { + reset(); +}); + +/** A `System(...)` element as `buildC4Node` hands it over: `c4.width` and `c4ShapePadding`. */ +const c4Node = (): Node => + ({ + id: 'SystemAA', + label: 'Internet Banking System', + stereotype: '[Software System]', + description: ['Allows customers to view information about their bank accounts'], + width: 216, + padding: 20, + }) as unknown as Node; + +const renderLabel = async (node: Node) => { + const svg = select(document.body).append('svg'); + await c4LabelHelper(svg as never, node); + return svg; +}; + +const outerTspans = (svg: ReturnType>) => + svg.selectAll('tspan.text-outer-tspan').size(); + +describe('c4LabelHelper', () => { + it('wraps element labels by default, so the label stays within c4.width', async () => { + const svg = await renderLabel(c4Node()); + + // Three sections (name, stereotype, description) produce exactly 3 outer + // tspan elements when nothing wraps; the long description has to break further. + expect(outerTspans(svg)).toBeGreaterThan(3); + }); + + it('does not wrap when c4.wrap is disabled', async () => { + setConfig({ c4: { wrap: false } }); + const svg = await renderLabel(c4Node()); + + // One line per section, none of them broken. + expect(outerTspans(svg)).toBe(3); + }); +}); diff --git a/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.ts b/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.ts index f3100d0cebb..4a1b78de27a 100644 --- a/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.ts +++ b/packages/mermaid/src/rendering-util/rendering-elements/shapes/c4LabelHelper.ts @@ -46,9 +46,11 @@ export const c4LabelHelper = async ( const wrapWidth = node.width ? Math.max(node.width - 2 * (node.padding ?? 0), MIN_WRAP_WIDTH) : (getConfig().flowchart?.wrappingWidth ?? 200); - // Wrapping is opt-in via the root `wrap` config, matching the legacy C4 - // renderer; the (currently ignored) c4.wrap option is tracked in #7949. - const width = config.wrap ? wrapWidth : Number.POSITIVE_INFINITY; + // `c4.wrap` (schema default true) is this diagram's own auto-wrap flag, and is what + // the legacy renderer gated on. The root-level `wrap` has no schema default, so + // reading it here left every C4 label unwrapped. + const shouldWrap = config.c4?.wrap ?? true; + const width = shouldWrap ? wrapWidth : Number.POSITIVE_INFINITY; const rendered = await Promise.all( sections.map(async (section) => { From 1a96110882852cda29418ca4c659c4d4ac1111ca Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Thu, 20 Aug 2026 11:18:52 +0200 Subject: [PATCH 06/18] fix(dev): dedupe @codemirror/state so the dev explorer editor loads The dev-explorer code editor failed to mount, throwing "Unrecognized extension value in extension set" from EditorState.create. Two copies of @codemirror/state were being bundled: @codemirror/language resolved to 6.6.0 alongside the root pin, while @codemirror/view and @codemirror/commands require ^6.7.0 and got 6.7.1. CodeMirror gives facets and state fields per-module identity, so the extensions built by @codemirror/language -- syntaxHighlighting() and the mermaid() language support -- were unrecognisable to the Configuration resolver that came from the other copy, and the editor never mounted. Raise the root pin to ^6.7.1. @codemirror/language declares ^6.0.0, so everything now collapses onto a single instance and the bundle drops the duplicate module. Add a lockfile guard test: nothing in lint, types, or the unit suite notices a duplicated CodeMirror instance, and it would silently come back on any lockfile churn. --- package.json | 2 +- pnpm-lock.yaml | 13 +++---------- scripts/codemirror-dedupe.spec.ts | 32 +++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 scripts/codemirror-dedupe.spec.ts diff --git a/package.json b/package.json index a41846c645d..5718ea85329 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "@changesets/cli": "^2.29.8", "@codemirror/commands": "^6.10.4", "@codemirror/language": "^6.12.4", - "@codemirror/state": "^6.6.0", + "@codemirror/state": "^6.7.1", "@codemirror/view": "^6.43.7", "@cspell/eslint-plugin": "^9.3.2", "@eslint/js": "^9.26.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e609af5c2d0..02c60342247 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,8 +41,8 @@ importers: specifier: ^6.12.4 version: 6.12.4 '@codemirror/state': - specifier: ^6.6.0 - version: 6.6.0 + specifier: ^6.7.1 + version: 6.7.1 '@codemirror/view': specifier: ^6.43.7 version: 6.43.7 @@ -1640,9 +1640,6 @@ packages: '@codemirror/language@6.12.4': resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} - '@codemirror/state@6.6.0': - resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} - '@codemirror/state@6.7.1': resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} @@ -12206,17 +12203,13 @@ snapshots: '@codemirror/language@6.12.4': dependencies: - '@codemirror/state': 6.6.0 + '@codemirror/state': 6.7.1 '@codemirror/view': 6.43.7 '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 '@lezer/lr': 1.4.10 style-mod: 4.1.3 - '@codemirror/state@6.6.0': - dependencies: - '@marijn/find-cluster-break': 1.0.2 - '@codemirror/state@6.7.1': dependencies: '@marijn/find-cluster-break': 1.0.2 diff --git a/scripts/codemirror-dedupe.spec.ts b/scripts/codemirror-dedupe.spec.ts new file mode 100644 index 00000000000..20f735f63e0 --- /dev/null +++ b/scripts/codemirror-dedupe.spec.ts @@ -0,0 +1,32 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +/** + * CodeMirror keeps extension identity (facets, state fields) per module instance. + * If two copies of `@codemirror/state` end up in the dev-explorer bundle, extensions + * created by one copy are rejected by the other with: + * + * Uncaught Error: Unrecognized extension value in extension set ([object Object]). + * + * That breaks the dev-explorer code editor entirely, and only at runtime — nothing + * in lint, types, or unit tests notices. Guard the invariant at the lockfile level. + */ +const readLockfile = () => readFileSync(resolve(process.cwd(), 'pnpm-lock.yaml'), 'utf8'); + +const resolvedVersionsOf = (lockfile: string, packageName: string) => { + const escaped = packageName.replace(/[/@]/g, '\\$&'); + const matches = lockfile.matchAll(new RegExp(`^ '?${escaped}@([^':]+)'?:`, 'gm')); + return [...new Set([...matches].map((m) => m[1]))]; +}; + +describe('CodeMirror dependency deduplication', () => { + it('resolves @codemirror/state to exactly one version', () => { + expect(resolvedVersionsOf(readLockfile(), '@codemirror/state')).toHaveLength(1); + }); + + it('detects the duplicate-instance regression', () => { + const duplicated = [" '@codemirror/state@6.6.0':", " '@codemirror/state@6.7.1':"].join('\n'); + expect(resolvedVersionsOf(duplicated, '@codemirror/state')).toHaveLength(2); + }); +}); From 6bae15eb59d2836faf45e6642b24f4a039526d62 Mon Sep 17 00:00:00 2001 From: Per Brolin Date: Thu, 20 Aug 2026 14:35:22 +0200 Subject: [PATCH 07/18] Updated tests to Playwright API --- e2e/rendering/class/classDiagram-neo.spec.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/e2e/rendering/class/classDiagram-neo.spec.js b/e2e/rendering/class/classDiagram-neo.spec.js index ed4dcaccecc..c0043ff4267 100644 --- a/e2e/rendering/class/classDiagram-neo.spec.js +++ b/e2e/rendering/class/classDiagram-neo.spec.js @@ -199,10 +199,12 @@ test.describe('Class diagram — Neo look with new themes', () => { // Relation markers must not scale with it, otherwise they overshoot the line-end offset and end // up drawn behind the class box. The `look: 'neo'` cases above use the `-margin` marker variants, // so only the default `classic` look exercises the plain markers. -describe('Class diagram — Classic look with new themes', () => { +test.describe('Class diagram — Classic look with new themes', () => { themes.forEach(({ theme, label }) => { - it(`CLASSIC-1 [${label}]: should render relation markers outside the class box`, () => { - imgSnapshotTest(diagrams.allRelationships, { + test(`CLASSIC-1 [${label}]: should render relation markers outside the class box`, async ({ + page, + }, testInfo) => { + await imgSnapshotTest(page, testInfo, diagrams.allRelationships, { logLevel: 1, htmlLabels: true, theme, @@ -211,8 +213,10 @@ describe('Class diagram — Classic look with new themes', () => { }); themes.forEach(({ theme, label }) => { - it(`CLASSIC-2 [${label}]: should render cardinality with classic look`, () => { - imgSnapshotTest(diagrams.cardinality, { + test(`CLASSIC-2 [${label}]: should render cardinality with classic look`, async ({ + page, + }, testInfo) => { + await imgSnapshotTest(page, testInfo, diagrams.cardinality, { logLevel: 1, htmlLabels: true, theme, From b433a9aad549f650d268309b4ac1af180502cea3 Mon Sep 17 00:00:00 2001 From: Per Brolin Date: Thu, 20 Aug 2026 15:58:33 +0200 Subject: [PATCH 08/18] Updated changeset to describe specifik diagram affected --- .changeset/tidy-markers-userspaceonuse.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/tidy-markers-userspaceonuse.md b/.changeset/tidy-markers-userspaceonuse.md index 071c693de10..37a2e601829 100644 --- a/.changeset/tidy-markers-userspaceonuse.md +++ b/.changeset/tidy-markers-userspaceonuse.md @@ -2,4 +2,4 @@ 'mermaid': patch --- -fix: class diagram relation markers (composition, aggregation, extension, dependency, lollipop) no longer scale with the edge stroke width, so they stay outside the class box boundary in themes that set `strokeWidth: 2` (`redux`, `redux-dark`, `redux-color`, `redux-dark-color`, `neo`, `neo-dark`) with the default `classic` look. +fix(class): class diagram relation markers (composition, aggregation, extension, dependency, lollipop) no longer scale with the edge stroke width, so they stay outside the class box boundary in themes that set `strokeWidth: 2` (`redux`, `redux-dark`, `redux-color`, `redux-dark-color`, `neo`, `neo-dark`) with the default `classic` look. From 412c80abb749da9844322855378140fc2b2a6877 Mon Sep 17 00:00:00 2001 From: Per Brolin Date: Fri, 21 Aug 2026 14:50:54 +0200 Subject: [PATCH 09/18] Update doc and consistent handlig in c4 as for seq diags --- docs/syntax/c4.md | 4 ++-- packages/mermaid/src/diagrams/c4/c4Diagram.ts | 14 +++++++++++--- packages/mermaid/src/docs/syntax/c4.md | 4 ++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/syntax/c4.md b/docs/syntax/c4.md index 69d9eef30b0..aa2269bb3cc 100644 --- a/docs/syntax/c4.md +++ b/docs/syntax/c4.md @@ -212,14 +212,14 @@ UpdateRelStyle(customerA, bankA, $offsetY="60") ## Element text wrapping -Element text (name, type and description) stays on one line by default; the element sizes itself to the longest line. Set the root `wrap` config value to wrap text to the element width instead, which is set by the [`c4.width`](/config/schema-docs/config-defs-c4-diagram-config.html#width) config value: +Element text (name, type and description) is wrapped by default by wrap: true. Wrapping of line will occur if line length exceeds [`c4.width`](/config/schema-docs/config-defs-c4-diagram-config.html#width). To disable the default wrapping, set c4.wrap to false ```yaml --- config: - wrap: true c4: width: 216 + wrap: false --- ``` diff --git a/packages/mermaid/src/diagrams/c4/c4Diagram.ts b/packages/mermaid/src/diagrams/c4/c4Diagram.ts index 9557a0f708d..def33147af6 100644 --- a/packages/mermaid/src/diagrams/c4/c4Diagram.ts +++ b/packages/mermaid/src/diagrams/c4/c4Diagram.ts @@ -3,6 +3,7 @@ import parser from './parser/c4Diagram.jison'; import db from './c4Db.js'; import renderer from './c4Renderer.js'; import styles from './styles.js'; +import { setConfig } from '../../diagram-api/diagramAPI.js'; import type { MermaidConfig } from '../../config.type.js'; import type { DiagramDefinition } from '../../diagram-api/types.js'; @@ -11,8 +12,15 @@ export const diagram: DiagramDefinition = { db, renderer, styles, - init: ({ c4, wrap }: MermaidConfig) => { - renderer.setConf(c4); - db.setWrap(wrap); + init: (cnf: MermaidConfig) => { + if (!cnf.c4) { + cnf.c4 = {}; + } + if (cnf.wrap !== undefined) { + cnf.c4.wrap = cnf.wrap; + setConfig({ c4: { wrap: cnf.wrap } }); + } + renderer.setConf(cnf.c4); + db.setWrap(cnf.c4.wrap); }, }; diff --git a/packages/mermaid/src/docs/syntax/c4.md b/packages/mermaid/src/docs/syntax/c4.md index dd62a4ae7c0..cbefabbf04e 100644 --- a/packages/mermaid/src/docs/syntax/c4.md +++ b/packages/mermaid/src/docs/syntax/c4.md @@ -156,14 +156,14 @@ UpdateRelStyle(customerA, bankA, $offsetY="60") ## Element text wrapping -Element text (name, type and description) stays on one line by default; the element sizes itself to the longest line. Set the root `wrap` config value to wrap text to the element width instead, which is set by the [`c4.width`](/config/schema-docs/config-defs-c4-diagram-config.html#width) config value: +Element text (name, type and description) is wrapped by default by wrap: true. Wrapping of line will occur if line length exceeds [`c4.width`](/config/schema-docs/config-defs-c4-diagram-config.html#width). To disable the default wrapping, set c4.wrap to false ```yaml --- config: - wrap: true c4: width: 216 + wrap: false --- ``` From 8a2348020038cd908a819685f555b16694f3d490 Mon Sep 17 00:00:00 2001 From: Per Brolin Date: Fri, 21 Aug 2026 16:14:25 +0200 Subject: [PATCH 10/18] Updated doc from feedback --- docs/syntax/c4.md | 5 +++-- packages/mermaid/src/docs/syntax/c4.md | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/syntax/c4.md b/docs/syntax/c4.md index aa2269bb3cc..37f245a99e0 100644 --- a/docs/syntax/c4.md +++ b/docs/syntax/c4.md @@ -212,13 +212,14 @@ UpdateRelStyle(customerA, bankA, $offsetY="60") ## Element text wrapping -Element text (name, type and description) is wrapped by default by wrap: true. Wrapping of line will occur if line length exceeds [`c4.width`](/config/schema-docs/config-defs-c4-diagram-config.html#width). To disable the default wrapping, set c4.wrap to false +Since v\, C4 diagrams wrap by default and can be disabled by either setting wrap or c4.wrap to false. Before v\, wrapping was disabled by default, and could be enabled by setting wrap to true (but not c4.wrap). + +Example below illustrates disabling of default wrapping by use of c4.wrap. ```yaml --- config: c4: - width: 216 wrap: false --- ``` diff --git a/packages/mermaid/src/docs/syntax/c4.md b/packages/mermaid/src/docs/syntax/c4.md index cbefabbf04e..e9282613013 100644 --- a/packages/mermaid/src/docs/syntax/c4.md +++ b/packages/mermaid/src/docs/syntax/c4.md @@ -156,13 +156,14 @@ UpdateRelStyle(customerA, bankA, $offsetY="60") ## Element text wrapping -Element text (name, type and description) is wrapped by default by wrap: true. Wrapping of line will occur if line length exceeds [`c4.width`](/config/schema-docs/config-defs-c4-diagram-config.html#width). To disable the default wrapping, set c4.wrap to false +Since v, C4 diagrams wrap by default and can be disabled by either setting wrap or c4.wrap to false. Before v, wrapping was disabled by default, and could be enabled by setting wrap to true (but not c4.wrap). + +Example below illustrates disabling of default wrapping by use of c4.wrap. ```yaml --- config: c4: - width: 216 wrap: false --- ``` From 655211a0657add5136f8358756dd5294c6dcd821 Mon Sep 17 00:00:00 2001 From: Per Brolin Date: Mon, 24 Aug 2026 13:07:39 +0200 Subject: [PATCH 11/18] Reverted change of wrap-options --- packages/mermaid/src/diagrams/c4/c4Diagram.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/mermaid/src/diagrams/c4/c4Diagram.ts b/packages/mermaid/src/diagrams/c4/c4Diagram.ts index def33147af6..9557a0f708d 100644 --- a/packages/mermaid/src/diagrams/c4/c4Diagram.ts +++ b/packages/mermaid/src/diagrams/c4/c4Diagram.ts @@ -3,7 +3,6 @@ import parser from './parser/c4Diagram.jison'; import db from './c4Db.js'; import renderer from './c4Renderer.js'; import styles from './styles.js'; -import { setConfig } from '../../diagram-api/diagramAPI.js'; import type { MermaidConfig } from '../../config.type.js'; import type { DiagramDefinition } from '../../diagram-api/types.js'; @@ -12,15 +11,8 @@ export const diagram: DiagramDefinition = { db, renderer, styles, - init: (cnf: MermaidConfig) => { - if (!cnf.c4) { - cnf.c4 = {}; - } - if (cnf.wrap !== undefined) { - cnf.c4.wrap = cnf.wrap; - setConfig({ c4: { wrap: cnf.wrap } }); - } - renderer.setConf(cnf.c4); - db.setWrap(cnf.c4.wrap); + init: ({ c4, wrap }: MermaidConfig) => { + renderer.setConf(c4); + db.setWrap(wrap); }, }; From 3054836688bfa5cef4abca757548e5da6da962b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:53:34 +0000 Subject: [PATCH 12/18] Version Packages --- .changeset/c4-wrap-element-labels.md | 7 ------- .changeset/elk-neo-marker-theme-config.md | 5 ----- .changeset/tidy-markers-userspaceonuse.md | 5 ----- docs/syntax/c4.md | 2 +- packages/mermaid/CHANGELOG.md | 12 ++++++++++++ packages/mermaid/package.json | 2 +- packages/mermaid/src/docs/syntax/c4.md | 2 +- packages/tiny/CHANGELOG.md | 12 ++++++++++++ packages/tiny/package.json | 2 +- 9 files changed, 28 insertions(+), 21 deletions(-) delete mode 100644 .changeset/c4-wrap-element-labels.md delete mode 100644 .changeset/elk-neo-marker-theme-config.md delete mode 100644 .changeset/tidy-markers-userspaceonuse.md diff --git a/.changeset/c4-wrap-element-labels.md b/.changeset/c4-wrap-element-labels.md deleted file mode 100644 index a36557177d7..00000000000 --- a/.changeset/c4-wrap-element-labels.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'mermaid': patch ---- - -fix(c4): wrap element labels to `c4.width` again - -C4 element labels (`System`, `Container`, `Component`, `Person` and their `_Ext` variants) stopped wrapping in 11.17.0, so long descriptions rendered on one unbroken line and the shape grew sideways well past the configured `c4.width`. The unified-shapes label helper gated wrapping on the root-level `wrap` option, which has no schema default and is therefore `undefined`; it now gates on `c4.wrap` (default `true`), which is what the legacy renderer used. diff --git a/.changeset/elk-neo-marker-theme-config.md b/.changeset/elk-neo-marker-theme-config.md deleted file mode 100644 index ed66fba8917..00000000000 --- a/.changeset/elk-neo-marker-theme-config.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: neo-look arrowheads and crow's-foot markers no longer fall back to default theme colours/stroke widths on the first render with `layout: elk`. State diagram arrowheads stayed dark on dark themes, and ER / requirement markers were drawn at the default stroke width, because markers were created from the layout package's own bundled copy of mermaid, whose config had not been initialized yet. diff --git a/.changeset/tidy-markers-userspaceonuse.md b/.changeset/tidy-markers-userspaceonuse.md deleted file mode 100644 index 37a2e601829..00000000000 --- a/.changeset/tidy-markers-userspaceonuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix(class): class diagram relation markers (composition, aggregation, extension, dependency, lollipop) no longer scale with the edge stroke width, so they stay outside the class box boundary in themes that set `strokeWidth: 2` (`redux`, `redux-dark`, `redux-color`, `redux-dark-color`, `neo`, `neo-dark`) with the default `classic` look. diff --git a/docs/syntax/c4.md b/docs/syntax/c4.md index 37f245a99e0..4d246e7399f 100644 --- a/docs/syntax/c4.md +++ b/docs/syntax/c4.md @@ -212,7 +212,7 @@ UpdateRelStyle(customerA, bankA, $offsetY="60") ## Element text wrapping -Since v\, C4 diagrams wrap by default and can be disabled by either setting wrap or c4.wrap to false. Before v\, wrapping was disabled by default, and could be enabled by setting wrap to true (but not c4.wrap). +Since v11.17.1, C4 diagrams wrap by default and can be disabled by either setting wrap or c4.wrap to false. Before v11.17.1, wrapping was disabled by default, and could be enabled by setting wrap to true (but not c4.wrap). Example below illustrates disabling of default wrapping by use of c4.wrap. diff --git a/packages/mermaid/CHANGELOG.md b/packages/mermaid/CHANGELOG.md index fac90d2b166..5b082c0a124 100644 --- a/packages/mermaid/CHANGELOG.md +++ b/packages/mermaid/CHANGELOG.md @@ -1,5 +1,17 @@ # mermaid +## 11.17.1 + +### Patch Changes + +- [#8092](https://github.com/mermaid-js/mermaid/pull/8092) [`31ce60a`](https://github.com/mermaid-js/mermaid/commit/31ce60a596746c76dc932ab540d910a6c7fff8be) Thanks [@pbrolin47](https://github.com/pbrolin47)! - fix(c4): wrap element labels to `c4.width` again + + C4 element labels (`System`, `Container`, `Component`, `Person` and their `_Ext` variants) stopped wrapping in 11.17.0, so long descriptions rendered on one unbroken line and the shape grew sideways well past the configured `c4.width`. The unified-shapes label helper gated wrapping on the root-level `wrap` option, which has no schema default and is therefore `undefined`; it now gates on `c4.wrap` (default `true`), which is what the legacy renderer used. + +- [#8088](https://github.com/mermaid-js/mermaid/pull/8088) [`c66200b`](https://github.com/mermaid-js/mermaid/commit/c66200bc2302006c908f77819c584109f50c06e7) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix: neo-look arrowheads and crow's-foot markers no longer fall back to default theme colours/stroke widths on the first render with `layout: elk`. State diagram arrowheads stayed dark on dark themes, and ER / requirement markers were drawn at the default stroke width, because markers were created from the layout package's own bundled copy of mermaid, whose config had not been initialized yet. + +- [#8079](https://github.com/mermaid-js/mermaid/pull/8079) [`281cd7b`](https://github.com/mermaid-js/mermaid/commit/281cd7b0705a7cdf4295bfd5e3171647dc809dfb) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix(class): class diagram relation markers (composition, aggregation, extension, dependency, lollipop) no longer scale with the edge stroke width, so they stay outside the class box boundary in themes that set `strokeWidth: 2` (`redux`, `redux-dark`, `redux-color`, `redux-dark-color`, `neo`, `neo-dark`) with the default `classic` look. + ## 11.17.0 ### Minor Changes diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index d4bfa12b3fc..71a924d4d73 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "11.17.0", + "version": "11.17.1", "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.", "type": "module", "module": "./dist/mermaid.core.mjs", diff --git a/packages/mermaid/src/docs/syntax/c4.md b/packages/mermaid/src/docs/syntax/c4.md index e9282613013..e75b205a377 100644 --- a/packages/mermaid/src/docs/syntax/c4.md +++ b/packages/mermaid/src/docs/syntax/c4.md @@ -156,7 +156,7 @@ UpdateRelStyle(customerA, bankA, $offsetY="60") ## Element text wrapping -Since v, C4 diagrams wrap by default and can be disabled by either setting wrap or c4.wrap to false. Before v, wrapping was disabled by default, and could be enabled by setting wrap to true (but not c4.wrap). +Since v11.17.1, C4 diagrams wrap by default and can be disabled by either setting wrap or c4.wrap to false. Before v11.17.1, wrapping was disabled by default, and could be enabled by setting wrap to true (but not c4.wrap). Example below illustrates disabling of default wrapping by use of c4.wrap. diff --git a/packages/tiny/CHANGELOG.md b/packages/tiny/CHANGELOG.md index fac90d2b166..5b082c0a124 100644 --- a/packages/tiny/CHANGELOG.md +++ b/packages/tiny/CHANGELOG.md @@ -1,5 +1,17 @@ # mermaid +## 11.17.1 + +### Patch Changes + +- [#8092](https://github.com/mermaid-js/mermaid/pull/8092) [`31ce60a`](https://github.com/mermaid-js/mermaid/commit/31ce60a596746c76dc932ab540d910a6c7fff8be) Thanks [@pbrolin47](https://github.com/pbrolin47)! - fix(c4): wrap element labels to `c4.width` again + + C4 element labels (`System`, `Container`, `Component`, `Person` and their `_Ext` variants) stopped wrapping in 11.17.0, so long descriptions rendered on one unbroken line and the shape grew sideways well past the configured `c4.width`. The unified-shapes label helper gated wrapping on the root-level `wrap` option, which has no schema default and is therefore `undefined`; it now gates on `c4.wrap` (default `true`), which is what the legacy renderer used. + +- [#8088](https://github.com/mermaid-js/mermaid/pull/8088) [`c66200b`](https://github.com/mermaid-js/mermaid/commit/c66200bc2302006c908f77819c584109f50c06e7) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix: neo-look arrowheads and crow's-foot markers no longer fall back to default theme colours/stroke widths on the first render with `layout: elk`. State diagram arrowheads stayed dark on dark themes, and ER / requirement markers were drawn at the default stroke width, because markers were created from the layout package's own bundled copy of mermaid, whose config had not been initialized yet. + +- [#8079](https://github.com/mermaid-js/mermaid/pull/8079) [`281cd7b`](https://github.com/mermaid-js/mermaid/commit/281cd7b0705a7cdf4295bfd5e3171647dc809dfb) Thanks [@ashishjain0512](https://github.com/ashishjain0512)! - fix(class): class diagram relation markers (composition, aggregation, extension, dependency, lollipop) no longer scale with the edge stroke width, so they stay outside the class box boundary in themes that set `strokeWidth: 2` (`redux`, `redux-dark`, `redux-color`, `redux-dark-color`, `neo`, `neo-dark`) with the default `classic` look. + ## 11.17.0 ### Minor Changes diff --git a/packages/tiny/package.json b/packages/tiny/package.json index 576742a5973..06c023008fb 100644 --- a/packages/tiny/package.json +++ b/packages/tiny/package.json @@ -1,6 +1,6 @@ { "name": "@mermaid-js/tiny", - "version": "11.17.0", + "version": "11.17.1", "description": "Tiny version of mermaid", "type": "commonjs", "main": "./dist/mermaid.tiny.js", From 178d7c79fcbafcf0662b822ec34ed989372ee5c2 Mon Sep 17 00:00:00 2001 From: Knut Bot Date: Tue, 25 Aug 2026 12:11:05 +0200 Subject: [PATCH 13/18] fix: restore edgePaths class on the edge group (#8125) The shared createLayoutElementGroups helper defaulted to `edges edgePath` (singular), so layouts going through createGraphWithElements emitted a different class than dagre, which passes `edgePaths` explicitly. The edge group class is part of the rendered SVG contract that downstream integrations style and query, and renaming it fails silently: no error, rules just stop matching. Restore `edgePaths` as the default, and point the flowchart, block and user journey stylesheets at `.edgePaths .path`. Those rules still used the flowchart-v1 `.edgePath` selector, which matches nothing in the v2 DOM. Resolves #8124 Co-authored-by: Knut Sveidqvist --- .changeset/tall-moons-shave.md | 5 +++++ docs/diagrams/flowchart-code-flow.mmd | 2 +- packages/mermaid/src/diagrams/block/styles.ts | 2 +- packages/mermaid/src/diagrams/flowchart/styles.ts | 2 +- packages/mermaid/src/diagrams/user-journey/styles.js | 2 +- packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd | 2 +- packages/mermaid/src/rendering-util/createGraph.ts | 2 +- 7 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/tall-moons-shave.md diff --git a/.changeset/tall-moons-shave.md b/.changeset/tall-moons-shave.md new file mode 100644 index 00000000000..5e89a1a5663 --- /dev/null +++ b/.changeset/tall-moons-shave.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: restore the `edgePaths` class on the edge group in rendered SVG, and point the flowchart, block and user journey stylesheets at it diff --git a/docs/diagrams/flowchart-code-flow.mmd b/docs/diagrams/flowchart-code-flow.mmd index d306dac7b27..13e539a32c5 100644 --- a/docs/diagrams/flowchart-code-flow.mmd +++ b/docs/diagrams/flowchart-code-flow.mmd @@ -128,7 +128,7 @@ flowchart TD Styles --> stylesTS["styles.ts
getStyles(options)"] stylesTS --> StyleOptions["FlowChartStyleOptions
- arrowheadColor, border2
- clusterBkg, mainBkg
- fontFamily, textColor"] - StyleOptions --> GenerateCSS["Generate CSS styles
- .label, .cluster-label
- .node, .edgePath
- .flowchart-link, .edgeLabel"] + StyleOptions --> GenerateCSS["Generate CSS styles
- .label, .cluster-label
- .node, .edgePaths
- .flowchart-link, .edgeLabel"] GenerateCSS --> GetIconStyles["getIconStyles()"] %% Type System diff --git a/packages/mermaid/src/diagrams/block/styles.ts b/packages/mermaid/src/diagrams/block/styles.ts index 0aa11f7066e..1b0d09246cf 100644 --- a/packages/mermaid/src/diagrams/block/styles.ts +++ b/packages/mermaid/src/diagrams/block/styles.ts @@ -79,7 +79,7 @@ const getStyles = (options: BlockChartStyleOptions) => fill: ${options.arrowheadColor}; } - .edgePath .path { + .edgePaths .path { stroke: ${options.lineColor}; stroke-width: 2.0px; } diff --git a/packages/mermaid/src/diagrams/flowchart/styles.ts b/packages/mermaid/src/diagrams/flowchart/styles.ts index 3475b765f12..d54a79a6738 100644 --- a/packages/mermaid/src/diagrams/flowchart/styles.ts +++ b/packages/mermaid/src/diagrams/flowchart/styles.ts @@ -89,7 +89,7 @@ const getStyles = (options: FlowChartStyleOptions) => fill: ${options.arrowheadColor}; } - .edgePath .path { + .edgePaths .path { stroke: ${options.lineColor}; stroke-width: ${options.strokeWidth ?? 2}px; } diff --git a/packages/mermaid/src/diagrams/user-journey/styles.js b/packages/mermaid/src/diagrams/user-journey/styles.js index ebfb5658d82..8d2a5d72891 100644 --- a/packages/mermaid/src/diagrams/user-journey/styles.js +++ b/packages/mermaid/src/diagrams/user-journey/styles.js @@ -51,7 +51,7 @@ const getStyles = (options) => fill: ${options.arrowheadColor}; } - .edgePath .path { + .edgePaths .path { stroke: ${options.lineColor}; stroke-width: 1.5px; } diff --git a/packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd b/packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd index d306dac7b27..13e539a32c5 100644 --- a/packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd +++ b/packages/mermaid/src/docs/diagrams/flowchart-code-flow.mmd @@ -128,7 +128,7 @@ flowchart TD Styles --> stylesTS["styles.ts
getStyles(options)"] stylesTS --> StyleOptions["FlowChartStyleOptions
- arrowheadColor, border2
- clusterBkg, mainBkg
- fontFamily, textColor"] - StyleOptions --> GenerateCSS["Generate CSS styles
- .label, .cluster-label
- .node, .edgePath
- .flowchart-link, .edgeLabel"] + StyleOptions --> GenerateCSS["Generate CSS styles
- .label, .cluster-label
- .node, .edgePaths
- .flowchart-link, .edgeLabel"] GenerateCSS --> GetIconStyles["getIconStyles()"] %% Type System diff --git a/packages/mermaid/src/rendering-util/createGraph.ts b/packages/mermaid/src/rendering-util/createGraph.ts index f0723fcc878..4d5b0a8add1 100644 --- a/packages/mermaid/src/rendering-util/createGraph.ts +++ b/packages/mermaid/src/rendering-util/createGraph.ts @@ -28,7 +28,7 @@ export interface CreateLayoutElementGroupsOptions { export function createLayoutElementGroups( element: D3Selection, - { edgePathsClass = 'edges edgePath' }: CreateLayoutElementGroupsOptions = {} + { edgePathsClass = 'edges edgePaths' }: CreateLayoutElementGroupsOptions = {} ): LayoutElementGroups { const rootGroups = element.insert('g').attr('class', 'root'); const clusters = rootGroups.insert('g').attr('class', 'clusters'); From dcb694ddb58dc5ad3502e7e903cac05fd812eac3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 13:36:01 +0200 Subject: [PATCH 14/18] Version Packages (#8130) --- .changeset/tall-moons-shave.md | 5 ----- packages/mermaid/CHANGELOG.md | 6 ++++++ packages/mermaid/package.json | 2 +- packages/tiny/CHANGELOG.md | 6 ++++++ packages/tiny/package.json | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) delete mode 100644 .changeset/tall-moons-shave.md diff --git a/.changeset/tall-moons-shave.md b/.changeset/tall-moons-shave.md deleted file mode 100644 index 5e89a1a5663..00000000000 --- a/.changeset/tall-moons-shave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'mermaid': patch ---- - -fix: restore the `edgePaths` class on the edge group in rendered SVG, and point the flowchart, block and user journey stylesheets at it diff --git a/packages/mermaid/CHANGELOG.md b/packages/mermaid/CHANGELOG.md index 5b082c0a124..9b11f32cd11 100644 --- a/packages/mermaid/CHANGELOG.md +++ b/packages/mermaid/CHANGELOG.md @@ -1,5 +1,11 @@ # mermaid +## 11.17.2 + +### Patch Changes + +- [#8125](https://github.com/mermaid-js/mermaid/pull/8125) [`178d7c7`](https://github.com/mermaid-js/mermaid/commit/178d7c79fcbafcf0662b822ec34ed989372ee5c2) Thanks [@knsv-bot](https://github.com/knsv-bot)! - fix: restore the `edgePaths` class on the edge group in rendered SVG, and point the flowchart, block and user journey stylesheets at it + ## 11.17.1 ### Patch Changes diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index 71a924d4d73..570ad4b13dc 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "11.17.1", + "version": "11.17.2", "description": "Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.", "type": "module", "module": "./dist/mermaid.core.mjs", diff --git a/packages/tiny/CHANGELOG.md b/packages/tiny/CHANGELOG.md index 5b082c0a124..9b11f32cd11 100644 --- a/packages/tiny/CHANGELOG.md +++ b/packages/tiny/CHANGELOG.md @@ -1,5 +1,11 @@ # mermaid +## 11.17.2 + +### Patch Changes + +- [#8125](https://github.com/mermaid-js/mermaid/pull/8125) [`178d7c7`](https://github.com/mermaid-js/mermaid/commit/178d7c79fcbafcf0662b822ec34ed989372ee5c2) Thanks [@knsv-bot](https://github.com/knsv-bot)! - fix: restore the `edgePaths` class on the edge group in rendered SVG, and point the flowchart, block and user journey stylesheets at it + ## 11.17.1 ### Patch Changes diff --git a/packages/tiny/package.json b/packages/tiny/package.json index 06c023008fb..bdb0bad0162 100644 --- a/packages/tiny/package.json +++ b/packages/tiny/package.json @@ -1,6 +1,6 @@ { "name": "@mermaid-js/tiny", - "version": "11.17.1", + "version": "11.17.2", "description": "Tiny version of mermaid", "type": "commonjs", "main": "./dist/mermaid.tiny.js", From 0677067633d724fd7b4b72e4e1dbdc7276cba681 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Tue, 25 Aug 2026 13:55:14 +0200 Subject: [PATCH 15/18] test(e2e): point swimlanes edge selector at g.edgePaths Swimlanes renders through createCommonLayoutRenderer, which builds its element groups with the default edgePathsClass from createLayoutElementGroups. #8125 restored that default from `edges edgePath` to `edges edgePaths`, so the spec's `g.edgePath` selector stopped matching and the theme/linkStyle assertions failed with "element(s) not found". Also drop the `path.path` alternative: v2 edge paths are classed `edge-thickness-* edge-pattern-* flowchart-link`, so there is no `.path` class to match and the union only made the selector look like it had a fallback. --- e2e/rendering/swimlanes/swimlanes.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e/rendering/swimlanes/swimlanes.spec.ts b/e2e/rendering/swimlanes/swimlanes.spec.ts index 6af0a3fe6c9..860ea5df078 100644 --- a/e2e/rendering/swimlanes/swimlanes.spec.ts +++ b/e2e/rendering/swimlanes/swimlanes.spec.ts @@ -23,7 +23,10 @@ const HANDDRAWN_FIXTURES = [ ]; const shapeSelector = 'rect, polygon, ellipse, circle, path'; -const edgePathSelector = 'g.edgePath path.path, g.edgePath path'; +// The shared layout renderer emits the edge group as `g.edges.edgePaths` +// (see createLayoutElementGroups). Edge paths themselves carry +// `edge-thickness-* edge-pattern-* flowchart-link`, not a `.path` class. +const edgePathSelector = 'g.edgePaths path'; const asStandaloneSwimlanes = (source: string): string => { // Every swimlanes layout-test fixture declares the standalone `swimlanes` From b82c00c241ed6732dc72cb771dd0a0a0530a0f14 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Tue, 25 Aug 2026 14:04:41 +0200 Subject: [PATCH 16/18] fix(agentflow): point the edge stroke rule at .edgePaths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agentflow renders through the shared layout pipeline, whose edge group is `g.edges.edgePaths` — dagre has always passed that class explicitly, and #8125 restored it as the createLayoutElementGroups default. The `.edgePath` selector here matched nothing, so `strokeWidth` never reached the edge. Brings agentflow in line with the flowchart, block and user journey stylesheets #8125 already corrected. --- packages/mermaid/src/diagrams/agentflow/styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/agentflow/styles.ts b/packages/mermaid/src/diagrams/agentflow/styles.ts index 4f62610e66b..385dce25aae 100644 --- a/packages/mermaid/src/diagrams/agentflow/styles.ts +++ b/packages/mermaid/src/diagrams/agentflow/styles.ts @@ -87,7 +87,7 @@ const getStyles = (options: AgentflowStyleOptions) => fill: ${options.arrowheadColor}; } - .edgePath .path { + .edgePaths .path { stroke: ${options.lineColor}; stroke-width: ${options.strokeWidth ?? 2}px; } From 96a4e352a03f3f019496a4efeae7c0468c2e7206 Mon Sep 17 00:00:00 2001 From: Ashish Jain Date: Tue, 25 Aug 2026 14:14:38 +0200 Subject: [PATCH 17/18] Potential fix for pull request finding 'CodeQL / Incomplete string escaping or encoding' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- scripts/codemirror-dedupe.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/codemirror-dedupe.spec.ts b/scripts/codemirror-dedupe.spec.ts index 20f735f63e0..dd51495b686 100644 --- a/scripts/codemirror-dedupe.spec.ts +++ b/scripts/codemirror-dedupe.spec.ts @@ -15,7 +15,7 @@ import { describe, expect, it } from 'vitest'; const readLockfile = () => readFileSync(resolve(process.cwd(), 'pnpm-lock.yaml'), 'utf8'); const resolvedVersionsOf = (lockfile: string, packageName: string) => { - const escaped = packageName.replace(/[/@]/g, '\\$&'); + const escaped = packageName.replace(/[\\^$.*+?()[\]{}|/]/g, '\\$&'); const matches = lockfile.matchAll(new RegExp(`^ '?${escaped}@([^':]+)'?:`, 'gm')); return [...new Set([...matches].map((m) => m[1]))]; }; From b7653bfb32f87d3a83a96f4212af30b7f5698a48 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:19:31 +0000 Subject: [PATCH 18/18] [autofix.ci] apply automated fixes --- scripts/codemirror-dedupe.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/codemirror-dedupe.spec.ts b/scripts/codemirror-dedupe.spec.ts index dd51495b686..1ea214ceef1 100644 --- a/scripts/codemirror-dedupe.spec.ts +++ b/scripts/codemirror-dedupe.spec.ts @@ -15,7 +15,7 @@ import { describe, expect, it } from 'vitest'; const readLockfile = () => readFileSync(resolve(process.cwd(), 'pnpm-lock.yaml'), 'utf8'); const resolvedVersionsOf = (lockfile: string, packageName: string) => { - const escaped = packageName.replace(/[\\^$.*+?()[\]{}|/]/g, '\\$&'); + const escaped = packageName.replace(/[$()*+./?[\\\]^{|}]/g, '\\$&'); const matches = lockfile.matchAll(new RegExp(`^ '?${escaped}@([^':]+)'?:`, 'gm')); return [...new Set([...matches].map((m) => m[1]))]; };