diff --git a/docs/config/setup/mermaid/functions/clearLayoutRenderState.md b/docs/config/setup/mermaid/functions/clearLayoutRenderState.md index bd43a132fa0..4b3074ad135 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:197](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L197) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:207](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L207) ## Returns diff --git a/docs/config/setup/mermaid/functions/defaultMeasureLayout.md b/docs/config/setup/mermaid/functions/defaultMeasureLayout.md index 67887ad5c74..7b000c68209 100644 --- a/docs/config/setup/mermaid/functions/defaultMeasureLayout.md +++ b/docs/config/setup/mermaid/functions/defaultMeasureLayout.md @@ -12,7 +12,7 @@ > **defaultMeasureLayout**(`data4Layout`, `__namedParameters`, `options?`): `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:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L204) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L214) ## Parameters diff --git a/docs/config/setup/mermaid/functions/paintLayoutData.md b/docs/config/setup/mermaid/functions/paintLayoutData.md index 211004cd2f7..a530379b9b1 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:212](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L212) +Defined in: [packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts#L222) ## Parameters 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/docs/syntax/c4.md b/docs/syntax/c4.md index 69d9eef30b0..4d246e7399f 100644 --- a/docs/syntax/c4.md +++ b/docs/syntax/c4.md @@ -212,14 +212,15 @@ 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: +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. ```yaml --- config: - wrap: true c4: - width: 216 + wrap: false --- ``` diff --git a/e2e/rendering/class/classDiagram-neo.spec.js b/e2e/rendering/class/classDiagram-neo.spec.js index e842a069bb8..c0043ff4267 100644 --- a/e2e/rendering/class/classDiagram-neo.spec.js +++ b/e2e/rendering/class/classDiagram-neo.spec.js @@ -194,3 +194,33 @@ test.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. +test.describe('Class diagram — Classic look with new themes', () => { + themes.forEach(({ theme, label }) => { + 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, + }); + }); + }); + + themes.forEach(({ theme, label }) => { + test(`CLASSIC-2 [${label}]: should render cardinality with classic look`, async ({ + page, + }, testInfo) => { + await imgSnapshotTest(page, testInfo, diagrams.cardinality, { + logLevel: 1, + htmlLabels: true, + theme, + }); + }); + }); +}); 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` diff --git a/package.json b/package.json index 1980fe25038..4f9284148fc 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/packages/mermaid/CHANGELOG.md b/packages/mermaid/CHANGELOG.md index fac90d2b166..9b11f32cd11 100644 --- a/packages/mermaid/CHANGELOG.md +++ b/packages/mermaid/CHANGELOG.md @@ -1,5 +1,23 @@ # 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 + +- [#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 ff58ef6e51a..8569fe06042 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -1,6 +1,6 @@ { "name": "mermaid", - "version": "11.17.0", + "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/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; } 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/docs/syntax/c4.md b/packages/mermaid/src/docs/syntax/c4.md index dd62a4ae7c0..e75b205a377 100644 --- a/packages/mermaid/src/docs/syntax/c4.md +++ b/packages/mermaid/src/docs/syntax/c4.md @@ -156,14 +156,15 @@ 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: +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. ```yaml --- config: - wrap: true c4: - width: 216 + wrap: false --- ``` diff --git a/packages/mermaid/src/rendering-util/createGraph.ts b/packages/mermaid/src/rendering-util/createGraph.ts index 6a666507fab..519fd40f314 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'); 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 a1324caad5d..a78e9456052 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts +++ b/packages/mermaid/src/rendering-util/layout-algorithms/common/index.ts @@ -134,7 +134,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 diff --git a/packages/mermaid/src/rendering-util/rendering-elements/markers.js b/packages/mermaid/src/rendering-util/rendering-elements/markers.js index 25728549ca0..fe8d25e40c8 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([]); + }); +}); 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) => { diff --git a/packages/tiny/CHANGELOG.md b/packages/tiny/CHANGELOG.md index fac90d2b166..9b11f32cd11 100644 --- a/packages/tiny/CHANGELOG.md +++ b/packages/tiny/CHANGELOG.md @@ -1,5 +1,23 @@ # 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 + +- [#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..bdb0bad0162 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.2", "description": "Tiny version of mermaid", "type": "commonjs", "main": "./dist/mermaid.tiny.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d20bc0c7c01..603f886af37 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,8 +43,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 @@ -1645,9 +1645,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==} @@ -12211,17 +12208,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..1ea214ceef1 --- /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); + }); +});