Compare commits

...

11 Commits

Author SHA1 Message Date
darshanr0107
58ddcde8d2 chore: add changeset
on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
2025-12-29 18:33:57 +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
Sidharth Vinod
d5bc07dc0c fix: Remove unnecessary changeset 2025-12-19 16:50:52 +05:30
Knut Sveidqvist
80a686be03 Merge pull request #7245 from mermaid-js/canonicals-to-mermaid-ai
setting canonicals to mermaid.ai/open-source
2025-12-19 10:12:44 +00:00
autofix-ci[bot]
d26f2c6043 [autofix.ci] apply automated fixes 2025-12-15 14:42:00 +00:00
Knut Sveidqvist
8ca7a28bf3 Update canonicals in big-trees-walk.md
Set canonicals for Mermaid documentation to mermaid.ai/open-source.
2025-12-15 15:32:33 +01:00
Knut Sveidqvist
6b77c9c4c7 setting canonicals to mermaid.ai/open-source 2025-12-15 15:17:56 +01:00
Shubham P
7b167cf331 Merge pull request #7242 from mermaid-js/renovate/patch-dompurify
fix(deps): update dependency dompurify to ^3.3.1
2025-12-15 11:41:32 +00:00
Shubham P
d435ac6fe1 Merge pull request #7228 from mermaid-js/renovate/peter-evans-create-pull-request-digest
chore(deps): update peter-evans/create-pull-request digest to 0979079
2025-12-15 06:47:12 +00:00
renovate[bot]
ed96d067fc fix(deps): update dependency dompurify to ^3.3.1 2025-12-15 00:42:27 +00:00
renovate[bot]
09c60be450 chore(deps): update peter-evans/create-pull-request digest to 0979079 2025-12-10 10:49:20 +00:00
14 changed files with 220 additions and 309 deletions

View File

@@ -0,0 +1,5 @@
---
'mermaid': patch
---
fix: Refactor shared label rendering logic across diagrams, with no markdown-specific changes

View File

@@ -58,7 +58,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Commit and create pull request
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412
uses: peter-evans/create-pull-request@0979079bc20c05bbbb590a56c21c4e2b1d1f1bbe
with:
add-paths: |
cypress/timings.json

View File

@@ -78,7 +78,7 @@
"d3-sankey": "^0.12.3",
"dagre-d3-es": "7.0.13",
"dayjs": "^1.11.19",
"dompurify": "^3.3.0",
"dompurify": "^3.3.1",
"katex": "^0.16.25",
"khroma": "^2.1.0",
"lodash-es": "^4.17.21",

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

@@ -8,7 +8,7 @@ import type { CanonicalUrlConfig } from './canonical-urls.js';
*/
export const canonicalConfig: CanonicalUrlConfig = {
// Base URL for the Mermaid documentation site
baseUrl: 'https://docs.mermaidchart.com',
baseUrl: 'https://mermaid.ai/open-source',
// Disable automatic generation - only use specificCanonicalUrls
autoGenerate: false,
@@ -57,93 +57,6 @@ export const canonicalConfig: CanonicalUrlConfig = {
},
};
/**
* Pages that should have specific canonical URLs
*
* Since autoGenerate is set to false, ONLY pages listed here will get canonical URLs.
*
* Usage: Add entries to this object where the key is the relative path
* of the markdown file and the value is the desired canonical URL.
*
* Examples:
* - 'intro/index.md': 'https://docs.mermaidchart.com/intro/index.html'
* - 'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html'
* - 'config/configuration.md': 'https://docs.mermaidchart.com/mermaid-oss/config/configuration.html'
*/
export const specificCanonicalUrls: Record<string, string> = {
// Add your specific canonical URLs here
// Example:
// 'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html',
// Intro section
'intro/index.md': 'https://docs.mermaidchart.com/intro/index.html',
'intro/getting-started.md':
'https://docs.mermaidchart.com/mermaid-oss/intro/getting-started.html',
'intro/syntax-reference.md':
'https://docs.mermaidchart.com/mermaid-oss/intro/syntax-reference.html',
// Syntax section
'syntax/flowchart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/flowchart.html',
'syntax/sequenceDiagram.md':
'https://docs.mermaidchart.com/mermaid-oss/syntax/sequenceDiagram.html',
'syntax/classDiagram.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/classDiagram.html',
'syntax/stateDiagram.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/stateDiagram.html',
'syntax/entityRelationshipDiagram.md':
'https://docs.mermaidchart.com/mermaid-oss/syntax/entityRelationshipDiagram.html',
'syntax/userJourney.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/userJourney.html',
'syntax/gantt.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/gantt.html',
'syntax/pie.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/pie.html',
'syntax/quadrantChart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/quadrantChart.html',
'syntax/requirementDiagram.md':
'https://docs.mermaidchart.com/mermaid-oss/syntax/requirementDiagram.html',
'syntax/mindmap.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/mindmap.html',
'syntax/timeline.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/timeline.html',
'syntax/gitgraph.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/gitgraph.html',
'syntax/c4.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/c4.html',
'syntax/sankey.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/sankey.html',
'syntax/xyChart.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/xyChart.html',
'syntax/block.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/block.html',
'syntax/packet.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/packet.html',
'syntax/kanban.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/kanban.html',
'syntax/architecture.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/architecture.html',
'syntax/radar.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/radar.html',
'syntax/examples.md': 'https://docs.mermaidchart.com/mermaid-oss/syntax/examples.html',
// Config section
'config/configuration.md': 'https://docs.mermaidchart.com/mermaid-oss/config/configuration.html',
'config/usage.md': 'https://docs.mermaidchart.com/mermaid-oss/config/usage.html',
'config/icons.md': 'https://docs.mermaidchart.com/mermaid-oss/config/icons.html',
'config/directives.md': 'https://docs.mermaidchart.com/mermaid-oss/config/directives.html',
'config/theming.md': 'https://docs.mermaidchart.com/mermaid-oss/config/theming.html',
'config/math.md': 'https://docs.mermaidchart.com/mermaid-oss/config/math.html',
'config/accessibility.md': 'https://docs.mermaidchart.com/mermaid-oss/config/accessibility.html',
'config/mermaidCLI.md': 'https://docs.mermaidchart.com/mermaid-oss/config/mermaidCLI.html',
'config/faq.md': 'https://docs.mermaidchart.com/mermaid-oss/config/faq.html',
// Ecosystem section
'ecosystem/mermaid-chart.md':
'https://docs.mermaidchart.com/mermaid-oss/ecosystem/mermaid-chart.html',
'ecosystem/tutorials.md': 'https://docs.mermaidchart.com/mermaid-oss/ecosystem/tutorials.html',
'ecosystem/integrations-community.md':
'https://docs.mermaidchart.com/mermaid-oss/ecosystem/integrations-community.html',
'ecosystem/integrations-create.md':
'https://docs.mermaidchart.com/mermaid-oss/ecosystem/integrations-create.html',
// Community section
'community/intro.md': 'https://docs.mermaidchart.com/mermaid-oss/community/intro.html',
'community/contributing.md':
'https://docs.mermaidchart.com/mermaid-oss/community/contributing.html',
'community/new-diagram.md':
'https://docs.mermaidchart.com/mermaid-oss/community/new-diagram.html',
'community/questions-and-suggestions.md':
'https://docs.mermaidchart.com/mermaid-oss/community/questions-and-suggestions.html',
'community/security.md': 'https://docs.mermaidchart.com/mermaid-oss/community/security.html',
};
/**
* Helper function to get canonical URL for a specific page
* This can be used in frontmatter or for manual overrides
*/
export function getCanonicalUrl(relativePath: string): string | undefined {
return specificCanonicalUrls[relativePath];
return `https://mermaid.ai/open-source/${relativePath}`;
}

View File

@@ -1,5 +1,5 @@
import type { PageData } from 'vitepress';
import { canonicalConfig, specificCanonicalUrls } from './canonical-config.js';
import { canonicalConfig } from './canonical-config.js';
/**
* Configuration for canonical URL generation
@@ -48,31 +48,15 @@ const defaultConfig: CanonicalUrlConfig = {
},
};
/**
* Check if a path matches any of the exclude patterns
*/
function shouldExcludePath(relativePath: string, excludePatterns: string[] = []): boolean {
return excludePatterns.some((pattern) => {
// Convert glob pattern to regex
const regexPattern = pattern
.replace(/\*\*/g, '.*')
.replace(/\*/g, '[^/]*')
.replace(/\?/g, '.')
.replace(/\./g, '\\.');
const regex = new RegExp(`^${regexPattern}$`);
return regex.test(relativePath);
});
}
/**
* Transform a relative path to a canonical URL path
*/
function transformPath(relativePath: string, config: CanonicalUrlConfig): string {
export function transformPath(relativePath: string, config: CanonicalUrlConfig): string {
let transformedPath = relativePath;
// Apply built-in transformations
if (config.transformations?.removeMarkdownExtension) {
transformedPath = transformedPath.replace(/\.md$/, '');
transformedPath = transformedPath.replace(/\.md$/, '.html');
}
if (config.transformations?.removeIndex) {
@@ -116,45 +100,9 @@ function generateCanonicalUrl(relativePath: string, config: CanonicalUrlConfig):
export function addCanonicalUrls(pageData: PageData): void {
const config = canonicalConfig;
// Check for specific canonical URLs first
const specificUrl = specificCanonicalUrls[pageData.relativePath];
if (specificUrl) {
addCanonicalToHead(pageData, specificUrl);
return;
}
// Skip if canonical URL is already explicitly set in frontmatter
if (pageData.frontmatter.canonical) {
// If it's already a full URL, use as-is
if (pageData.frontmatter.canonical.startsWith('http')) {
addCanonicalToHead(pageData, pageData.frontmatter.canonical);
return;
}
// If it's a relative path, convert to absolute URL
const canonicalUrl = config.baseUrl + pageData.frontmatter.canonical;
addCanonicalToHead(pageData, canonicalUrl);
return;
}
// Skip if canonicalPath is set in frontmatter
if (pageData.frontmatter.canonicalPath) {
const canonicalUrl = config.baseUrl + pageData.frontmatter.canonicalPath;
addCanonicalToHead(pageData, canonicalUrl);
return;
}
// Skip if auto-generation is disabled
if (!config.autoGenerate) {
return;
}
// Skip if path should be excluded
if (shouldExcludePath(pageData.relativePath, config.excludePatterns)) {
return;
}
// Generate canonical URL
const canonicalUrl = generateCanonicalUrl(pageData.relativePath, config);
transformPath(pageData.relativePath, config);
addCanonicalToHead(pageData, canonicalUrl);
}

View File

@@ -486,7 +486,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,13 +40,18 @@ 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, {
@@ -87,7 +92,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 +113,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 +134,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 +158,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

@@ -2,10 +2,19 @@ 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 { 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 +22,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 {
@@ -57,47 +66,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);

12
pnpm-lock.yaml generated
View File

@@ -257,8 +257,8 @@ importers:
specifier: ^1.11.19
version: 1.11.19
dompurify:
specifier: ^3.3.0
version: 3.3.0
specifier: ^3.3.1
version: 3.3.1
katex:
specifier: ^0.16.25
version: 0.16.25
@@ -5328,8 +5328,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.3.0:
resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==}
dompurify@3.3.1:
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -14677,7 +14677,7 @@ snapshots:
class-variance-authority: 0.7.1
clsx: 2.1.1
color-string: 2.1.2
dompurify: 3.3.0
dompurify: 3.3.1
highlight.js: 10.7.3
html-to-image: 1.11.13
immer: 10.1.3
@@ -16281,7 +16281,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.3.0:
dompurify@3.3.1:
optionalDependencies:
'@types/trusted-types': 2.0.7