Compare commits

..

6 Commits

Author SHA1 Message Date
darshanr0107
c16d20c855 fix:prioritize flowchart.htmlLabels over global htmlLabels
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-18 17:47:41 +05:30
darshanr0107
270f05d409 chore: add visual tests for html labels rendering
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-18 15:01:40 +05:30
darshanr0107
c6134a05d2 chore: add changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-18 13:58:49 +05:30
darshanr0107
bf4a57fd04 fix: htmlLabels resolution for flowchart diagrams
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-09-18 13:10:07 +05:30
Sidharth Vinod
994f7df29a Merge pull request #6949 from tklever/examples-license
docs: add MIT license to `examples` metadata
2025-09-16 12:40:40 +05:30
Tim Klever
dc11b8645c docs: add MIT license to examples metadata
Adding the MIT license documentation so the published package metadata accurately reflects the license. The MIT license is currently included in the package distribution https://www.npmjs.com/package/@mermaid-js/examples?activeTab=code, but not documented in the metadata. This causes npmjs.com to display the license as "none"
2025-09-15 17:35:44 -07:00
14 changed files with 101 additions and 108 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Ensure flowchart htmlLabels resolution respects both global and flowchart config

View File

@@ -122,46 +122,6 @@ describe('Flowchart v2', () => {
expect(svg).to.not.have.attr('style'); expect(svg).to.not.have.attr('style');
}); });
}); });
it('renders only pure SVG labels (no <foreignObject>) when flowchart.htmlLabels=false', () => {
renderGraph(
`---
config:
flowchart:
htmlLabels: false
---
flowchart LR
subgraph \`**One**\`
a["\`**The cat**
in the hat\`"] -- "\`**edge label**\`" --> b{{"\`**The dog** in the hog\`"}}
end
subgraph \`**Two**\`
c["\`**The cat**
in the hat\`"] -- "\`**Bold edge label**\`" --> d["\`The dog in the hog\`"]
end
`
);
cy.get('svg').find('foreignObject').should('not.exist');
});
it('renders only pure SVG labels (no <foreignObject>) when global htmlLabels=false', () => {
renderGraph(
`---
config:
htmlLabels: false
---
flowchart LR
subgraph \`**One**\`
a["\`**The cat**
in the hat\`"] -- "\`**edge label**\`" --> b{{"\`**The dog** in the hog\`"}}
end
subgraph \`**Two**\`
c["\`**The cat**
in the hat\`"] -- "\`**Bold edge label**\`" --> d["\`The dog in the hog\`"]
end
`
);
cy.get('svg').find('foreignObject').should('not.exist');
});
it('V2 - 16: Render Stadium shape', () => { it('V2 - 16: Render Stadium shape', () => {
imgSnapshotTest( imgSnapshotTest(
@@ -1239,4 +1199,61 @@ class link myClass
` `
); );
}); });
describe('htmlLabels rendering', () => {
it('should not render with htmlLabels when disabled via flowchart config', () => {
imgSnapshotTest(
`flowchart LR
A["HTML label <br> with breaks"] --> B["Another label"]
C --> D
`,
{ flowchart: { htmlLabels: false } }
);
});
it('should not render with htmlLabels when disabled via global config', () => {
imgSnapshotTest(
`flowchart LR
A["HTML label <br> with breaks"] --> B["Another label"]
C --> D
`,
{ htmlLabels: false }
);
});
it('should render with htmlLabels when enabled', () => {
imgSnapshotTest(
`flowchart LR
A["HTML label <br> with breaks"] --> B["Another label"]
C --> D
`,
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
);
});
it('should not render with htmlLabels when disabled via flowchart config, even when enabled in global config', () => {
imgSnapshotTest(
`flowchart LR
A["HTML label <br> with breaks"] --> B["Another label"]
C --> D
`,
{ htmlLabels: true, flowchart: { htmlLabels: false } },
undefined,
($svg) => {
expect($svg.find('foreignObject').length).to.equal(0);
}
);
});
it('should create foreignObject elements when htmlLabels enabled', () => {
renderGraph(
`flowchart TD
A["Node with <br> HTML"] -- "edge <br> label" --> B["Another node"]
C --> D
`,
{ htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' }
);
cy.get('svg foreignObject').should('exist');
});
});
}); });

View File

@@ -10,7 +10,7 @@
# Interface: DetailedError # Interface: DetailedError
Defined in: [packages/mermaid/src/utils.ts:784](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L784) Defined in: [packages/mermaid/src/utils.ts:783](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L783)
## Properties ## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/utils.ts:784](https://github.com/mermaid-js/me
> `optional` **error**: `any` > `optional` **error**: `any`
Defined in: [packages/mermaid/src/utils.ts:789](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L789) Defined in: [packages/mermaid/src/utils.ts:788](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L788)
--- ---
@@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/utils.ts:789](https://github.com/mermaid-js/me
> **hash**: `any` > **hash**: `any`
Defined in: [packages/mermaid/src/utils.ts:787](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L787) Defined in: [packages/mermaid/src/utils.ts:786](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L786)
--- ---
@@ -34,7 +34,7 @@ Defined in: [packages/mermaid/src/utils.ts:787](https://github.com/mermaid-js/me
> `optional` **message**: `string` > `optional` **message**: `string`
Defined in: [packages/mermaid/src/utils.ts:790](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L790) Defined in: [packages/mermaid/src/utils.ts:789](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L789)
--- ---
@@ -42,4 +42,4 @@ Defined in: [packages/mermaid/src/utils.ts:790](https://github.com/mermaid-js/me
> **str**: `string` > **str**: `string`
Defined in: [packages/mermaid/src/utils.ts:785](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L785) Defined in: [packages/mermaid/src/utils.ts:784](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/utils.ts#L784)

View File

@@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"description": "Mermaid examples package", "description": "Mermaid examples package",
"author": "Sidharth Vinod", "author": "Sidharth Vinod",
"license": "MIT",
"type": "module", "type": "module",
"module": "./dist/mermaid-examples.core.mjs", "module": "./dist/mermaid-examples.core.mjs",
"types": "./dist/mermaid.d.ts", "types": "./dist/mermaid.d.ts",

View File

@@ -33,7 +33,10 @@ const getStyles = (options: FlowChartStyleOptions) =>
background-color: ${fade(options.tertiaryColor, 0.5)}; background-color: ${fade(options.tertiaryColor, 0.5)};
} }
.edgeLabel .label {
fill: ${options.nodeBorder};
font-size: 14px;
}
.label { .label {
font-family: ${options.fontFamily}; font-family: ${options.fontFamily};
@@ -65,14 +68,6 @@ const getStyles = (options: FlowChartStyleOptions) =>
stroke: ${options.lineColor} !important; stroke: ${options.lineColor} !important;
stroke-width: 1; stroke-width: 1;
} }
.background {
fill: ${options.tertiaryColor};
opacity: 0.7;
background-color: ${options.tertiaryColor};
rect {
opacity: 0.5;
}
}
`; `;
export default getStyles; export default getStyles;

View File

@@ -21,7 +21,13 @@ export const diagram = {
if (cnf.layout) { if (cnf.layout) {
setConfig({ layout: cnf.layout }); setConfig({ layout: cnf.layout });
} }
cnf.flowchart.htmlLabels = cnf.flowchart?.htmlLabels ?? cnf?.htmlLabels;
cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute; cnf.flowchart.arrowMarkerAbsolute = cnf.arrowMarkerAbsolute;
setConfig({ flowchart: { arrowMarkerAbsolute: cnf.arrowMarkerAbsolute } }); setConfig({
flowchart: {
arrowMarkerAbsolute: cnf.arrowMarkerAbsolute,
htmlLabels: cnf.flowchart.htmlLabels,
},
});
}, },
}; };

View File

@@ -2,7 +2,6 @@ import { getConfig } from '../../diagram-api/diagramAPI.js';
import type { DiagramDB } from '../../diagram-api/types.js'; import type { DiagramDB } from '../../diagram-api/types.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import type { Node, Edge } from '../../rendering-util/types.js'; import type { Node, Edge } from '../../rendering-util/types.js';
import { shouldUseHtmlLabels } from '../../utils.js';
import { import {
setAccTitle, setAccTitle,
@@ -318,17 +317,11 @@ export class RequirementDB implements DiagramDB {
for (const relation of this.relations) { for (const relation of this.relations) {
let counter = 0; let counter = 0;
const isContains = relation.type === this.Relationships.CONTAINS; const isContains = relation.type === this.Relationships.CONTAINS;
let relationLabel = `&lt;&lt;${relation.type}&gt;&gt;`;
if (!shouldUseHtmlLabels()) {
relationLabel = relationLabel.replace(/&lt;/g, '<').replace(/&gt;/g, '>');
}
const edge: Edge = { const edge: Edge = {
id: `${relation.src}-${relation.dst}-${counter}`, id: `${relation.src}-${relation.dst}-${counter}`,
start: this.requirements.get(relation.src)?.name ?? this.elements.get(relation.src)?.name, start: this.requirements.get(relation.src)?.name ?? this.elements.get(relation.src)?.name,
end: this.requirements.get(relation.dst)?.name ?? this.elements.get(relation.dst)?.name, end: this.requirements.get(relation.dst)?.name ?? this.elements.get(relation.dst)?.name,
label: relationLabel, label: `&lt;&lt;${relation.type}&gt;&gt;`,
classes: 'relationshipLine', classes: 'relationshipLine',
style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'], style: ['fill:none', isContains ? '' : 'stroke-dasharray: 10,7'],
labelpos: 'c', labelpos: 'c',

View File

@@ -55,11 +55,6 @@ const getStyles = (options) => `
.labelBkg { .labelBkg {
background-color: ${options.edgeLabelBackground}; background-color: ${options.edgeLabelBackground};
} }
.background {
fill: ${options.edgeLabelBackground};
stroke: none;
}
`; `;
// fill', conf.rect_fill) // fill', conf.rect_fill)

View File

@@ -176,9 +176,8 @@ function updateTextContentAndStyles(tspan: any, wrappedLine: MarkdownWord[]) {
if (index === 0) { if (index === 0) {
innerTspan.text(word.content); innerTspan.text(word.content);
} else { } else {
const prev = wrappedLine[index - 1].content; // TODO: check what joiner to use.
const insertSpace = !prev.endsWith('<') && !word.content.startsWith('>'); innerTspan.text(' ' + word.content);
innerTspan.text((insertSpace ? ' ' : '') + word.content);
} }
}); });
} }

View File

@@ -9,7 +9,6 @@ import intersectRect from '../rendering-elements/intersect/intersect-rect.js';
import createLabel from './createLabel.js'; import createLabel from './createLabel.js';
import { createRoundedRectPathD } from './shapes/roundedRectPath.ts'; import { createRoundedRectPathD } from './shapes/roundedRectPath.ts';
import { styles2String, userNodeOverrides } from './shapes/handDrawnShapeStyles.js'; import { styles2String, userNodeOverrides } from './shapes/handDrawnShapeStyles.js';
import { shouldUseHtmlLabels } from '../../utils.js';
const rect = async (parent, node) => { const rect = async (parent, node) => {
log.info('Creating subgraph rect for ', node.id, node); log.info('Creating subgraph rect for ', node.id, node);
@@ -26,7 +25,8 @@ const rect = async (parent, node) => {
.attr('id', node.id) .attr('id', node.id)
.attr('data-look', node.look); .attr('data-look', node.look);
const useHtmlLabels = shouldUseHtmlLabels(); const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
// Create the label and insert it after the rect // Create the label and insert it after the rect
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label '); const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');

View File

@@ -1,12 +1,13 @@
import { select } from 'd3'; import { select } from 'd3';
import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getConfig } from '../../diagram-api/diagramAPI.js';
import common, { import common, {
evaluate,
hasKatex, hasKatex,
renderKatexSanitized, renderKatexSanitized,
sanitizeText, sanitizeText,
} from '../../diagrams/common/common.js'; } from '../../diagrams/common/common.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { decodeEntities, shouldUseHtmlLabels } from '../../utils.js'; import { decodeEntities } from '../../utils.js';
/** /**
* @param dom * @param dom
@@ -62,7 +63,8 @@ const createLabel = async (_vertexText, style, isTitle, isNode) => {
if (typeof vertexText === 'object') { if (typeof vertexText === 'object') {
vertexText = vertexText[0]; vertexText = vertexText[0];
} }
if (shouldUseHtmlLabels()) {
if (evaluate(getConfig().flowchart.htmlLabels)) {
// TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that? // TODO: addHtmlLabel accepts a labelStyle. Do we possibly have that?
vertexText = vertexText.replace(/\\n|\n/g, '<br />'); vertexText = vertexText.replace(/\\n|\n/g, '<br />');
log.info('vertexText' + vertexText); log.info('vertexText' + vertexText);

View File

@@ -1,7 +1,7 @@
import { getConfig } from '../../diagram-api/diagramAPI.js'; import { getConfig } from '../../diagram-api/diagramAPI.js';
import { log } from '../../logger.js'; import { log } from '../../logger.js';
import { createText } from '../createText.js'; import { createText } from '../createText.js';
import utils, { shouldUseHtmlLabels } from '../../utils.js'; import utils from '../../utils.js';
import { import {
getLineFunctionsWithOffset, getLineFunctionsWithOffset,
markerOffsets, markerOffsets,
@@ -44,13 +44,10 @@ export const getLabelStyles = (styleArray) => {
}; };
export const insertEdgeLabel = async (elem, edge) => { export const insertEdgeLabel = async (elem, edge) => {
const useHtmlLabels = shouldUseHtmlLabels(); const config = getConfig();
let useHtmlLabels = config.flowchart.htmlLabels;
const { labelStyles } = styles2String(edge); const { labelStyles } = styles2String(edge);
edge.labelStyle = labelStyles; edge.labelStyle = labelStyles;
// if (useHtmlLabels === false) {
// edge.label = edge.label.replaceAll('&gt;', '>').replaceAll('&lt;', '<');
// }
const labelElement = await createText(elem, edge.label, { const labelElement = await createText(elem, edge.label, {
style: edge.labelStyle, style: edge.labelStyle,
useHtmlLabels, useHtmlLabels,

View File

@@ -4,12 +4,7 @@ import { getConfig } from '../../../diagram-api/diagramAPI.js';
import { select } from 'd3'; import { select } from 'd3';
import defaultConfig from '../../../defaultConfig.js'; import defaultConfig from '../../../defaultConfig.js';
import { evaluate, sanitizeText } from '../../../diagrams/common/common.js'; import { evaluate, sanitizeText } from '../../../diagrams/common/common.js';
import { import { decodeEntities, handleUndefinedAttr, parseFontSize } from '../../../utils.js';
decodeEntities,
handleUndefinedAttr,
parseFontSize,
shouldUseHtmlLabels,
} from '../../../utils.js';
import type { D3Selection, Point } from '../../../types.js'; import type { D3Selection, Point } from '../../../types.js';
export const labelHelper = async <T extends SVGGraphicsElement>( export const labelHelper = async <T extends SVGGraphicsElement>(
@@ -18,7 +13,7 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
_classes?: string _classes?: string
) => { ) => {
let cssClasses; let cssClasses;
const useHtmlLabels = shouldUseHtmlLabels(); const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.flowchart?.htmlLabels);
if (!_classes) { if (!_classes) {
cssClasses = 'node default'; cssClasses = 'node default';
} else { } else {

View File

@@ -7,12 +7,12 @@ import {
curveBumpX, curveBumpX,
curveBumpY, curveBumpY,
curveBundle, curveBundle,
curveCardinal,
curveCardinalClosed, curveCardinalClosed,
curveCardinalOpen, curveCardinalOpen,
curveCatmullRom, curveCardinal,
curveCatmullRomClosed, curveCatmullRomClosed,
curveCatmullRomOpen, curveCatmullRomOpen,
curveCatmullRom,
curveLinear, curveLinear,
curveLinearClosed, curveLinearClosed,
curveMonotoneX, curveMonotoneX,
@@ -23,17 +23,16 @@ import {
curveStepBefore, curveStepBefore,
select, select,
} from 'd3'; } from 'd3';
import common from './diagrams/common/common.js';
import { sanitizeDirective } from './utils/sanitizeDirective.js';
import { log } from './logger.js';
import { detectType } from './diagram-api/detectType.js';
import assignWithDepth from './assignWithDepth.js';
import type { MermaidConfig } from './config.type.js';
import memoize from 'lodash-es/memoize.js'; import memoize from 'lodash-es/memoize.js';
import merge from 'lodash-es/merge.js'; import merge from 'lodash-es/merge.js';
import assignWithDepth from './assignWithDepth.js';
import { getUserDefinedConfig } from './config.js';
import type { MermaidConfig } from './config.type.js';
import { detectType } from './diagram-api/detectType.js';
import { directiveRegex } from './diagram-api/regexes.js'; import { directiveRegex } from './diagram-api/regexes.js';
import common, { evaluate } from './diagrams/common/common.js';
import { log } from './logger.js';
import type { D3Element, Point, TextDimensionConfig, TextDimensions } from './types.js'; import type { D3Element, Point, TextDimensionConfig, TextDimensions } from './types.js';
import { sanitizeDirective } from './utils/sanitizeDirective.js';
export const ZERO_WIDTH_SPACE = '\u200b'; export const ZERO_WIDTH_SPACE = '\u200b';
@@ -982,14 +981,3 @@ export function isLabelCoordinateInPath(point: Point, dAttr: string) {
return sanitizedD.includes(roundedX.toString()) || sanitizedD.includes(roundedY.toString()); return sanitizedD.includes(roundedX.toString()) || sanitizedD.includes(roundedY.toString());
} }
export const shouldUseHtmlLabels = () => {
const siteConfig = getUserDefinedConfig();
let useHtmlLabels;
if (siteConfig.flowchart?.htmlLabels !== undefined) {
useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
} else {
useHtmlLabels = evaluate(siteConfig.htmlLabels);
}
return useHtmlLabels;
};