Compare commits

...

3 Commits

Author SHA1 Message Date
autofix-ci[bot]
7bd2b8cc44 [autofix.ci] apply automated fixes 2025-12-29 13:04:39 +00:00
darshanr0107
db3227771c fix: Add markdown-specific behavior on top of the common rendering changes.
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-12-29 18:27:35 +05:30
darshanr0107
48ad4c5e06 chore: separate out shared rendering changes for markdown
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-12-29 17:08:25 +05:30
22 changed files with 373 additions and 195 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Add markdown-specific behavior on top of the common rendering changes.

View File

@@ -1186,8 +1186,8 @@ end
end
githost["Github, Gitlab, BitBucket, etc."]
githost2["\`Github, Gitlab, BitBucket, etc.\`"]
a["1."]
b["- x"]
a["\`1.\`"]
b["\`- x\`"]
`;
it('should render raw strings', () => {

View File

@@ -1004,3 +1004,48 @@ graph TD
);
});
});
it('#5824: should be able to render string and markdown labels', () => {
imgSnapshotTest(
`
flowchart TB
mermaid{"What is\nyourmermaid version?"} --> v10["<11"] --"\`<**1**1\`"--> fine["No bug"]
mermaid --> v11[">= v11"] -- ">= v11" --> broken["Affected by https://github.com/mermaid-js/mermaid/issues/5824"]
subgraph subgraph1["\`How to fix **fix**\`"]
broken --> B["B"]
end
githost["Github, Gitlab, BitBucket, etc."]
githost2["\`Github, Gitlab, BitBucket, etc.\`"]
a["1."]
b["- x"]
`,
{
flowchart: { htmlLabels: true },
securityLevel: 'loose',
}
);
});
it('69: should render subgraphs with adhoc list headings', () => {
imgSnapshotTest(
`
graph TB
subgraph "1. first"
a1-->a2
end
subgraph 2. second
b1-->b2
end
`,
{ fontFamily: 'courier' }
);
});
it('70: should render subgraphs with markdown headings', () => {
imgSnapshotTest(
`
graph TB
subgraph "\`**strong**\`"
a1-->a2
end
`,
{ fontFamily: 'courier' }
);
});

View File

@@ -10,7 +10,7 @@
# Interface: ParseOptions
Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L89)
Defined in: [packages/mermaid/src/types.ts:90](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L90)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:89](https://github.com/mermaid-js/mer
> `optional` **suppressErrors**: `boolean`
Defined in: [packages/mermaid/src/types.ts:94](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L94)
Defined in: [packages/mermaid/src/types.ts:95](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L95)
If `true`, parse will return `false` instead of throwing error when the diagram is invalid.
The `parseError` function will not be called.

View File

@@ -10,7 +10,7 @@
# Interface: ParseResult
Defined in: [packages/mermaid/src/types.ts:97](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L97)
Defined in: [packages/mermaid/src/types.ts:98](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L98)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:97](https://github.com/mermaid-js/mer
> **config**: [`MermaidConfig`](MermaidConfig.md)
Defined in: [packages/mermaid/src/types.ts:105](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L105)
Defined in: [packages/mermaid/src/types.ts:106](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L106)
The config passed as YAML frontmatter or directives
@@ -28,6 +28,6 @@ The config passed as YAML frontmatter or directives
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:101](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L101)
Defined in: [packages/mermaid/src/types.ts:102](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L102)
The diagram type, e.g. 'flowchart', 'sequence', etc.

View File

@@ -10,7 +10,7 @@
# Interface: RenderResult
Defined in: [packages/mermaid/src/types.ts:115](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L115)
Defined in: [packages/mermaid/src/types.ts:116](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L116)
## Properties
@@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/types.ts:115](https://github.com/mermaid-js/me
> `optional` **bindFunctions**: (`element`) => `void`
Defined in: [packages/mermaid/src/types.ts:133](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L133)
Defined in: [packages/mermaid/src/types.ts:134](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L134)
Bind function to be called after the svg has been inserted into the DOM.
This is necessary for adding event listeners to the elements in the svg.
@@ -45,7 +45,7 @@ bindFunctions?.(div); // To call bindFunctions only if it's present.
> **diagramType**: `string`
Defined in: [packages/mermaid/src/types.ts:123](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L123)
Defined in: [packages/mermaid/src/types.ts:124](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L124)
The diagram type, e.g. 'flowchart', 'sequence', etc.
@@ -55,6 +55,6 @@ The diagram type, e.g. 'flowchart', 'sequence', etc.
> **svg**: `string`
Defined in: [packages/mermaid/src/types.ts:119](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L119)
Defined in: [packages/mermaid/src/types.ts:120](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/types.ts#L120)
The svg code for the rendered graph.

View File

@@ -4,6 +4,7 @@ import { getConfig } from '../../diagram-api/diagramAPI.js';
import { select } from 'd3';
import { evaluate, sanitizeText } from '../../diagrams/common/common.js';
import { decodeEntities } from '../../utils.js';
import { configureLabelImages } from '../../rendering-util/rendering-elements/shapes/labelImageUtils.js';
export const labelHelper = async (parent, node, _classes, isNode) => {
const config = getConfig();
@@ -65,46 +66,7 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
const dv = select(text);
// if there are images, need to wait for them to load before getting the bounding box
const images = div.getElementsByTagName('img');
if (images) {
const noImgText = labelText.replace(/<img[^>]*>/g, '').trim() === '';
await Promise.all(
[...images].map(
(img) =>
new Promise((res) => {
/**
*
*/
function setupImage() {
img.style.display = 'flex';
img.style.flexDirection = 'column';
if (noImgText) {
// default size if no text
const bodyFontSize = config.fontSize
? config.fontSize
: window.getComputedStyle(document.body).fontSize;
const enlargingFactor = 5;
const width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px';
img.style.minWidth = width;
img.style.maxWidth = width;
} else {
img.style.width = '100%';
}
res(img);
}
setTimeout(() => {
if (img.complete) {
setupImage();
}
});
img.addEventListener('error', setupImage);
img.addEventListener('load', setupImage);
})
)
);
}
await configureLabelImages(div, labelText);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);

View File

@@ -85,6 +85,17 @@ export class FlowDB implements DiagramDB {
return common.sanitizeText(txt, this.config);
}
private sanitizeNodeLabelType(labelType?: string) {
switch (labelType) {
case 'markdown':
case 'string':
case 'text':
return labelType;
default:
return 'markdown';
}
}
/**
* Function to lookup domId from id in the graph definition.
*
@@ -208,6 +219,7 @@ export class FlowDB implements DiagramDB {
if (doc?.label) {
vertex.text = doc?.label;
vertex.labelType = this.sanitizeNodeLabelType(doc?.labelType);
}
if (doc?.icon) {
vertex.icon = doc?.icon;
@@ -267,7 +279,7 @@ export class FlowDB implements DiagramDB {
if (edge.text.startsWith('"') && edge.text.endsWith('"')) {
edge.text = edge.text.substring(1, edge.text.length - 1);
}
edge.labelType = linkTextObj.type;
edge.labelType = this.sanitizeNodeLabelType(linkTextObj.type);
}
if (type !== undefined) {
@@ -702,7 +714,7 @@ You have to call mermaid.initialize.`
title: title.trim(),
classes: [],
dir,
labelType: _title.type,
labelType: this.sanitizeNodeLabelType(_title?.type),
};
log.info('Adding', subGraph.id, subGraph.nodes, subGraph.dir);
@@ -1012,6 +1024,7 @@ You have to call mermaid.initialize.`
const baseNode = {
id: vertex.id,
label: vertex.text,
labelType: vertex.labelType,
labelStyle: '',
parentId,
padding: config.flowchart?.padding || 8,
@@ -1088,6 +1101,7 @@ You have to call mermaid.initialize.`
id: subGraph.id,
label: subGraph.title,
labelStyle: '',
labelType: subGraph.labelType,
parentId: parentDB.get(subGraph.id),
padding: 8,
cssCompiledStyles: this.getCompiledStyles(subGraph.classes),
@@ -1119,6 +1133,7 @@ You have to call mermaid.initialize.`
end: rawEdge.end,
type: rawEdge.type ?? 'normal',
label: rawEdge.text,
labelType: rawEdge.labelType,
labelpos: 'c',
thickness: rawEdge.stroke,
minlen: rawEdge.length,

View File

@@ -29,7 +29,7 @@ export interface FlowVertex {
domId: string;
haveCallback?: boolean;
id: string;
labelType: 'text';
labelType: 'markdown' | 'string' | 'text';
link?: string;
linkTarget?: string;
props?: any;
@@ -62,7 +62,7 @@ export interface FlowEdge {
style?: string[];
length?: number;
text: string;
labelType: 'text';
labelType: 'markdown' | 'string' | 'text';
classes: string[];
id?: string;
animation?: 'fast' | 'slow';

View File

@@ -62,6 +62,7 @@ const getData = function () {
const node = {
id: section.id,
label: sanitizeText(section.label ?? '', conf),
labelType: 'markdown',
isGroup: true,
ticket: section.ticket,
shape: 'kanbanSection',
@@ -76,6 +77,7 @@ const getData = function () {
id: item.id,
parentId: section.id,
label: sanitizeText(item.label ?? '', conf),
labelType: 'markdown',
isGroup: false,
ticket: item?.ticket,
priority: item?.priority,

View File

@@ -261,6 +261,7 @@ export class MindmapDB {
id: node.id.toString(),
domId: 'node_' + node.id.toString(),
label: node.descr,
labelType: 'markdown',
isGroup: false,
shape: getShapeFromType(node.type),
width: node.width,

View File

@@ -16,6 +16,7 @@ export interface MindmapNode {
x?: number;
y?: number;
isRoot?: boolean;
labelType?: string;
}
export type FilledMindMapNode = RequiredDeep<MindmapNode>;

View File

@@ -291,6 +291,7 @@ export const dataFetcher = (
rx: 10,
ry: 10,
look,
labelType: 'markdown',
};
// Clear the label for dividers who have no description

View File

@@ -161,6 +161,7 @@ export interface NodeData {
centerLabel?: boolean;
position?: string;
description?: string | string[];
labelType?: string;
}
export interface Edge {

View File

@@ -30,11 +30,25 @@ const rect = async (parent, node) => {
// Create the label and insert it after the rect
const labelEl = shapeSvg.insert('g').attr('class', 'cluster-label ');
const text = await createText(labelEl, node.label, {
style: node.labelStyle,
useHtmlLabels,
isNode: true,
});
let text;
if (node.labelType === 'markdown') {
text = await createText(labelEl, node.label, {
style: node.labelStyle,
useHtmlLabels,
isNode: true,
width: node.width,
});
} else {
const labelElement = await createLabel(
node.label,
node.labelStyle,
false,
true,
false,
node.width
);
text = labelEl.node()?.appendChild(labelElement);
}
// Get the size of the label
let bbox = text.getBBox();
@@ -177,18 +191,36 @@ const roundedWithTitle = async (parent, node) => {
// add the rect
const outerRectG = shapeSvg.insert('g', ':first-child');
const useHtmlLabels = evaluate(siteConfig.flowchart.htmlLabels);
// Create the label and insert it after the rect
const label = shapeSvg.insert('g').attr('class', 'cluster-label');
let innerRect = shapeSvg.append('rect');
const text = label
.node()
.appendChild(await createLabel(node.label, node.labelStyle, undefined, true));
let text;
if (node.labelType === 'markdown') {
text = await createText(label, node.label, {
style: node.labelStyle,
useHtmlLabels,
isNode: true,
width: 10000, // Use large width to prevent text clipping for title labels
});
} else {
const labelElement = await createLabel(
node.label,
node.labelStyle,
false,
true,
false,
10000 // Use large width to prevent text clipping for title labels
);
text = label.node()?.appendChild(labelElement);
}
// Get the size of the label
let bbox = text.getBBox();
if (evaluate(siteConfig.flowchart.htmlLabels)) {
if (useHtmlLabels) {
const div = text.children[0];
const dv = select(text);
bbox = div.getBoundingClientRect();
@@ -486,7 +518,7 @@ export const insertCluster = async (elem, node) => {
};
export const getClusterTitleWidth = (elem, node) => {
const label = createLabel(node.label, node.labelStyle, undefined, true);
const label = createLabel(node.label, node.labelStyle, undefined, true, false, node.width);
elem.node().appendChild(label);
const width = label.getBBox().width;
elem.node().removeChild(label);

View File

@@ -8,6 +8,9 @@ import common, {
} from '../../diagrams/common/common.js';
import { log } from '../../logger.js';
import { decodeEntities } from '../../utils.js';
import { configureLabelImages } from './shapes/labelImageUtils.js';
const DEFAULT_WRAPPING_WIDTH = 200;
/**
* @param dom
@@ -20,45 +23,80 @@ function applyStyle(dom, styleFn) {
}
/**
* @param {any} node
* @returns {Promise<SVGForeignObjectElement>} Node
* Gets the wrapping width from config or returns the default.
* @returns {number} The wrapping width to use
*/
async function addHtmlLabel(node) {
const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));
const div = fo.append('xhtml:div');
function getWrappingWidth() {
return getConfig().flowchart?.wrappingWidth ?? DEFAULT_WRAPPING_WIDTH;
}
/**
* @param {any} node
* @param {number | undefined} width
* @param {boolean} addBackground
* @returns {Promise<SVGForeignObjectElement>}
*/
async function addHtmlLabel(node, width, addBackground = false) {
const labelWidth = width ?? getWrappingWidth();
const fo = select(document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject'));
fo.attr('width', 10000).attr('height', 10000);
const div = fo.append('xhtml:div');
const config = getConfig();
let label = node.label;
if (node.label && hasKatex(node.label)) {
label = await renderKatexSanitized(node.label.replace(common.lineBreakRegex, '\n'), config);
}
const sanitizedLabel = hasKatex(node.label)
? await renderKatexSanitized(node.label.replace(common.lineBreakRegex, '\n'), config)
: sanitizeText(node.label, config);
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
const labelSpan =
'<span class="' +
labelClass +
'" ' +
(node.labelStyle ? 'style="' + node.labelStyle + '"' : '') + // codeql [js/html-constructed-from-input] : false positive
'>' +
label +
'</span>';
div.html(sanitizeText(labelSpan, config));
const span = div.append('span');
span.html(sanitizedLabel);
applyStyle(span, node.labelStyle);
span.attr('class', labelClass);
applyStyle(div, node.labelStyle);
div.style('display', 'inline-block');
div.style('padding-right', '1px');
// Fix for firefox
div.style('white-space', 'nowrap');
div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
div
.style('display', 'inline-block')
.style('white-space', 'nowrap')
.style('line-height', '1.5')
.style('text-align', 'center')
.attr('xmlns', 'http://www.w3.org/1999/xhtml');
if (addBackground) {
div.attr('class', 'labelBkg');
}
const tempSvg = select(document.body)
.append('svg')
.attr('style', 'position: absolute; visibility: hidden; height: 0; width: 0;');
tempSvg.node().appendChild(fo.node());
// if there are images, need to wait for them to load before getting the bounding box
await configureLabelImages(div.node(), node.label);
// Check if text needs wrapping (same logic as createText)
let bbox = div.node().getBoundingClientRect();
if (bbox.width > labelWidth) {
div
.style('white-space', 'break-spaces')
.style('max-width', labelWidth + 'px')
.style('display', 'inline-block');
bbox = div.node().getBoundingClientRect();
}
fo.attr('width', bbox.width);
fo.attr('height', bbox.height);
tempSvg.remove();
return fo.node();
}
/**
* @param _vertexText
* @param style
* @param isTitle
* @param isNode
* @deprecated svg-util/createText instead
*/
const createLabel = async (_vertexText, style, isTitle, isNode) => {
const createLabel = async (_vertexText, style, isTitle, isNode, addBackground = false, width) => {
let vertexText = _vertexText || '';
if (typeof vertexText === 'object') {
vertexText = vertexText[0];
@@ -76,36 +114,28 @@ const createLabel = async (_vertexText, style, isTitle, isNode) => {
),
labelStyle: style ? style.replace('fill:', 'color:') : style,
};
let vertexNode = await addHtmlLabel(node);
// vertexNode.parentNode.removeChild(vertexNode);
return vertexNode;
} else {
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
let rows = [];
if (typeof vertexText === 'string') {
rows = vertexText.split(/\\n|\n|<br\s*\/?>/gi);
} else if (Array.isArray(vertexText)) {
rows = vertexText;
} else {
rows = [];
}
for (const row of rows) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', '0');
if (isTitle) {
tspan.setAttribute('class', 'title-row');
} else {
tspan.setAttribute('class', 'row');
}
tspan.textContent = row.trim();
svgLabel.appendChild(tspan);
}
return svgLabel;
return await addHtmlLabel(node, width, addBackground);
}
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
const rows =
typeof vertexText === 'string'
? vertexText.split(/\\n|\n|<br\s*\/?>/gi)
: Array.isArray(vertexText)
? vertexText
: [];
for (const row of rows) {
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
tspan.setAttribute('dy', '1em');
tspan.setAttribute('x', '0');
tspan.setAttribute('class', isTitle ? 'title-row' : 'row');
tspan.textContent = row.trim();
svgLabel.appendChild(tspan);
}
return svgLabel;
};
export default createLabel;

View File

@@ -40,21 +40,38 @@ export const clear = () => {
};
export const getLabelStyles = (styleArray) => {
let styles = styleArray ? styleArray.reduce((acc, style) => acc + ';' + style, '') : '';
return styles;
if (!styleArray) {
return '';
}
if (typeof styleArray === 'string') {
return styleArray;
}
return styleArray.reduce((acc, style) => acc + ';' + style, '');
};
export const insertEdgeLabel = async (elem, edge) => {
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
const width = edge.width || getConfig().flowchart?.wrappingWidth;
const { labelStyles } = styles2String(edge);
edge.labelStyle = labelStyles;
const labelElement = await createText(elem, edge.label, {
style: edge.labelStyle,
useHtmlLabels,
addSvgBackground: true,
isNode: false,
});
const labelElement =
edge.labelType === 'markdown'
? await createText(elem, edge.label, {
style: getLabelStyles(edge.labelStyle),
useHtmlLabels,
addSvgBackground: true,
isNode: false,
width,
})
: await createLabel(
edge.label,
getLabelStyles(edge.labelStyle),
undefined,
false,
true,
width
);
log.info('abc82', edge, edge.labelType);
// Create outer g, edgeLabel, this will be positioned after graph layout
@@ -87,7 +104,11 @@ export const insertEdgeLabel = async (elem, edge) => {
// Create the actual text element
const startLabelElement = await createLabel(
edge.startLabelLeft,
getLabelStyles(edge.labelStyle)
getLabelStyles(edge.labelStyle),
undefined,
false,
useHtmlLabels,
width
);
const startEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
const inner = startEdgeLabelLeft.insert('g').attr('class', 'inner');
@@ -104,7 +125,11 @@ export const insertEdgeLabel = async (elem, edge) => {
// Create the actual text element
const startLabelElement = await createLabel(
edge.startLabelRight,
getLabelStyles(edge.labelStyle)
getLabelStyles(edge.labelStyle),
undefined,
false,
useHtmlLabels,
width
);
const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
const inner = startEdgeLabelRight.insert('g').attr('class', 'inner');
@@ -121,7 +146,14 @@ export const insertEdgeLabel = async (elem, edge) => {
}
if (edge.endLabelLeft) {
// Create the actual text element
const endLabelElement = await createLabel(edge.endLabelLeft, getLabelStyles(edge.labelStyle));
const endLabelElement = await createLabel(
edge.endLabelLeft,
getLabelStyles(edge.labelStyle),
undefined,
false,
useHtmlLabels,
width
);
const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner');
fo = inner.node().appendChild(endLabelElement);
@@ -138,7 +170,14 @@ export const insertEdgeLabel = async (elem, edge) => {
}
if (edge.endLabelRight) {
// Create the actual text element
const endLabelElement = await createLabel(edge.endLabelRight, getLabelStyles(edge.labelStyle));
const endLabelElement = await createLabel(
edge.endLabelRight,
getLabelStyles(edge.labelStyle),
undefined,
false,
useHtmlLabels,
width
);
const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
const inner = endEdgeLabelRight.insert('g').attr('class', 'inner');

View File

@@ -62,9 +62,12 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
// drawRect doesn't center non-htmlLabels correctly as of now, so translate label
if (!evaluate(config.htmlLabels)) {
const textElement = shapeSvg.select('text');
const bbox = (textElement.node() as SVGTextElement)?.getBBox();
textElement.attr('transform', `translate(${-bbox.width / 2}, 0)`);
const textElement = shapeSvg.select('.label text');
const textNode = textElement.node() as SVGTextElement;
if (textNode) {
const bbox = textNode.getBBox();
textElement.attr('transform', `translate(${-bbox.width / 2}, 0)`);
}
}
return shapeSvg;
}

View File

@@ -0,0 +1,49 @@
import { getConfig } from '../../../diagram-api/diagramAPI.js';
import defaultConfig from '../../../defaultConfig.js';
import { parseFontSize } from '../../../utils.js';
export async function configureLabelImages(
container: HTMLElement,
labelText: string
): Promise<void> {
const images = container.getElementsByTagName('img');
if (!images || images.length === 0) {
return;
}
const noImgText = labelText.replace(/<img[^>]*>/g, '').trim() === '';
await Promise.all(
[...images].map(
(img) =>
new Promise((res) => {
function setupImage() {
img.style.display = 'flex';
img.style.flexDirection = 'column';
if (noImgText) {
// default size if no text
const bodyFontSize = getConfig().fontSize
? getConfig().fontSize
: window.getComputedStyle(document.body).fontSize;
const enlargingFactor = 5;
const [parsedBodyFontSize = defaultConfig.fontSize] = parseFontSize(bodyFontSize);
const width = parsedBodyFontSize * enlargingFactor + 'px';
img.style.minWidth = width;
img.style.maxWidth = width;
} else {
img.style.width = '100%';
}
res(img);
}
setTimeout(() => {
if (img.complete) {
setupImage();
}
});
img.addEventListener('error', setupImage);
img.addEventListener('load', setupImage);
})
)
);
}

View File

@@ -38,9 +38,12 @@ export async function rectWithTitle<T extends SVGGraphicsElement>(
const description = node.description;
const title = node.label;
const title = node.label || '';
const width = node.width || getConfig().flowchart?.wrappingWidth;
const text = label.node()!.appendChild(await createLabel(title, node.labelStyle, true, true));
const text = label
.node()!
.appendChild(await createLabel(title, node.labelStyle, true, true, false, width));
let bbox = { width: 0, height: 0 };
if (evaluate(getConfig()?.flowchart?.htmlLabels)) {
const div = text.children[0];
@@ -56,10 +59,12 @@ export async function rectWithTitle<T extends SVGGraphicsElement>(
.node()!
.appendChild(
await createLabel(
textRows.join ? textRows.join('<br/>') : textRows,
Array.isArray(textRows) ? textRows.join('<br/>') : textRows,
node.labelStyle,
true,
true
true,
false,
width
)
);

View File

@@ -1,11 +1,21 @@
import createLabel from '../createLabel.js';
import { createText } from '../../createText.js';
import type { Node } from '../../types.js';
import { getConfig } from '../../../diagram-api/diagramAPI.js';
import { select } from 'd3';
import defaultConfig from '../../../defaultConfig.js';
import { evaluate, sanitizeText } from '../../../diagrams/common/common.js';
import { decodeEntities, handleUndefinedAttr, parseFontSize } from '../../../utils.js';
import { evaluate, hasKatex, sanitizeText } from '../../../diagrams/common/common.js';
import { decodeEntities, handleUndefinedAttr } from '../../../utils.js';
import type { D3Selection, Point } from '../../../types.js';
import { configureLabelImages } from './labelImageUtils.js';
/**
* Waits for all images in a container to load and applies appropriate styling.
* This ensures accurate bounding box measurements after images are loaded.
*
* @param container - The HTML element containing img tags
* @param labelText - The original label text to check if there's text besides images
* @returns Promise that resolves when all images are loaded and styled
*/
export const labelHelper = async <T extends SVGGraphicsElement>(
parent: D3Selection<T>,
@@ -13,7 +23,7 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
_classes?: string
) => {
let cssClasses;
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.htmlLabels);
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.flowchart?.htmlLabels);
if (!_classes) {
cssClasses = 'node default';
} else {
@@ -40,14 +50,30 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
label = typeof node.label === 'string' ? node.label : node.label[0];
}
const text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), {
useHtmlLabels,
width: node.width || getConfig().flowchart?.wrappingWidth,
// @ts-expect-error -- This is currently not used. Should this be `classes` instead?
cssClasses: 'markdown-node-label',
style: node.labelStyle,
addSvgBackground: !!node.icon || !!node.img,
});
let text;
const addBackground = !!node.icon || !!node.img;
const width = node.width || getConfig().flowchart?.wrappingWidth;
if (node.labelType === 'markdown' || hasKatex(label)) {
text = await createText(labelEl, sanitizeText(decodeEntities(label), getConfig()), {
useHtmlLabels,
width,
// @ts-expect-error -- This is currently not used. Should this be `classes` instead?
cssClasses: 'markdown-node-label',
style: node.labelStyle,
addSvgBackground: addBackground,
});
} else {
const labelElement = await createLabel(
sanitizeText(decodeEntities(label), getConfig()),
node.labelStyle,
false,
true,
addBackground,
width
);
text = labelEl.node()?.appendChild(labelElement);
}
// Get the size of the label
let bbox = text.getBBox();
const halfPadding = (node?.padding ?? 0) / 2;
@@ -57,47 +83,7 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
const dv = select(text);
// if there are images, need to wait for them to load before getting the bounding box
const images = div.getElementsByTagName('img');
if (images) {
const noImgText = label.replace(/<img[^>]*>/g, '').trim() === '';
await Promise.all(
[...images].map(
(img) =>
new Promise((res) => {
/**
*
*/
function setupImage() {
img.style.display = 'flex';
img.style.flexDirection = 'column';
if (noImgText) {
// default size if no text
const bodyFontSize = getConfig().fontSize
? getConfig().fontSize
: window.getComputedStyle(document.body).fontSize;
const enlargingFactor = 5;
const [parsedBodyFontSize = defaultConfig.fontSize] = parseFontSize(bodyFontSize);
const width = parsedBodyFontSize * enlargingFactor + 'px';
img.style.minWidth = width;
img.style.maxWidth = width;
} else {
img.style.width = '100%';
}
res(img);
}
setTimeout(() => {
if (img.complete) {
setupImage();
}
});
img.addEventListener('error', setupImage);
img.addEventListener('load', setupImage);
})
)
);
}
await configureLabelImages(div, label);
bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width);

View File

@@ -11,6 +11,7 @@ export interface NodeMetaData {
priority: 'Very High' | 'High' | 'Medium' | 'Low' | 'Very Low';
assigned?: string;
ticket?: string;
labelType?: string;
}
export interface ParticipantMetaData {