mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-31 00:20:06 +01:00
Compare commits
2 Commits
shared-ren
...
fix/align-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a325ae8f3 | ||
|
|
bc53201551 |
@@ -1,5 +0,0 @@
|
|||||||
---
|
|
||||||
'mermaid': patch
|
|
||||||
---
|
|
||||||
|
|
||||||
fix: Refactor shared label rendering logic across diagrams, with no markdown-specific changes
|
|
||||||
@@ -4,7 +4,6 @@ import { getConfig } from '../../diagram-api/diagramAPI.js';
|
|||||||
import { select } from 'd3';
|
import { select } from 'd3';
|
||||||
import { evaluate, sanitizeText } from '../../diagrams/common/common.js';
|
import { evaluate, sanitizeText } from '../../diagrams/common/common.js';
|
||||||
import { decodeEntities } from '../../utils.js';
|
import { decodeEntities } from '../../utils.js';
|
||||||
import { configureLabelImages } from '../../rendering-util/rendering-elements/shapes/labelImageUtils.js';
|
|
||||||
|
|
||||||
export const labelHelper = async (parent, node, _classes, isNode) => {
|
export const labelHelper = async (parent, node, _classes, isNode) => {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
@@ -66,7 +65,46 @@ export const labelHelper = async (parent, node, _classes, isNode) => {
|
|||||||
const dv = select(text);
|
const dv = select(text);
|
||||||
|
|
||||||
// if there are images, need to wait for them to load before getting the bounding box
|
// if there are images, need to wait for them to load before getting the bounding box
|
||||||
await configureLabelImages(div, labelText);
|
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);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bbox = div.getBoundingClientRect();
|
bbox = div.getBoundingClientRect();
|
||||||
dv.attr('width', bbox.width);
|
dv.attr('width', bbox.width);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type { CanonicalUrlConfig } from './canonical-urls.js';
|
|||||||
*/
|
*/
|
||||||
export const canonicalConfig: CanonicalUrlConfig = {
|
export const canonicalConfig: CanonicalUrlConfig = {
|
||||||
// Base URL for the Mermaid documentation site
|
// Base URL for the Mermaid documentation site
|
||||||
baseUrl: 'https://mermaid.ai/open-source',
|
baseUrl: 'https://docs.mermaidchart.com',
|
||||||
|
|
||||||
// Disable automatic generation - only use specificCanonicalUrls
|
// Disable automatic generation - only use specificCanonicalUrls
|
||||||
autoGenerate: false,
|
autoGenerate: false,
|
||||||
@@ -57,6 +57,93 @@ 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 {
|
export function getCanonicalUrl(relativePath: string): string | undefined {
|
||||||
return `https://mermaid.ai/open-source/${relativePath}`;
|
return specificCanonicalUrls[relativePath];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PageData } from 'vitepress';
|
import type { PageData } from 'vitepress';
|
||||||
import { canonicalConfig } from './canonical-config.js';
|
import { canonicalConfig, specificCanonicalUrls } from './canonical-config.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for canonical URL generation
|
* Configuration for canonical URL generation
|
||||||
@@ -48,15 +48,31 @@ 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
|
* Transform a relative path to a canonical URL path
|
||||||
*/
|
*/
|
||||||
export function transformPath(relativePath: string, config: CanonicalUrlConfig): string {
|
function transformPath(relativePath: string, config: CanonicalUrlConfig): string {
|
||||||
let transformedPath = relativePath;
|
let transformedPath = relativePath;
|
||||||
|
|
||||||
// Apply built-in transformations
|
// Apply built-in transformations
|
||||||
if (config.transformations?.removeMarkdownExtension) {
|
if (config.transformations?.removeMarkdownExtension) {
|
||||||
transformedPath = transformedPath.replace(/\.md$/, '.html');
|
transformedPath = transformedPath.replace(/\.md$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.transformations?.removeIndex) {
|
if (config.transformations?.removeIndex) {
|
||||||
@@ -100,9 +116,45 @@ function generateCanonicalUrl(relativePath: string, config: CanonicalUrlConfig):
|
|||||||
export function addCanonicalUrls(pageData: PageData): void {
|
export function addCanonicalUrls(pageData: PageData): void {
|
||||||
const config = canonicalConfig;
|
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
|
// Generate canonical URL
|
||||||
const canonicalUrl = generateCanonicalUrl(pageData.relativePath, config);
|
const canonicalUrl = generateCanonicalUrl(pageData.relativePath, config);
|
||||||
transformPath(pageData.relativePath, config);
|
|
||||||
addCanonicalToHead(pageData, canonicalUrl);
|
addCanonicalToHead(pageData, canonicalUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function applyStyle(dom, styleFn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addHtmlSpan(
|
export async function addHtmlSpan(
|
||||||
element,
|
element,
|
||||||
node,
|
node,
|
||||||
width,
|
width,
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ export const insertCluster = async (elem, node) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getClusterTitleWidth = (elem, node) => {
|
export const getClusterTitleWidth = (elem, node) => {
|
||||||
const label = createLabel(node.label, node.labelStyle, undefined, true, false, node.width);
|
const label = createLabel(node.label, node.labelStyle, undefined, true);
|
||||||
elem.node().appendChild(label);
|
elem.node().appendChild(label);
|
||||||
const width = label.getBBox().width;
|
const width = label.getBBox().width;
|
||||||
elem.node().removeChild(label);
|
elem.node().removeChild(label);
|
||||||
|
|||||||
@@ -1,109 +1,23 @@
|
|||||||
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 { evaluate } from '../../diagrams/common/common.js';
|
||||||
evaluate,
|
|
||||||
hasKatex,
|
|
||||||
renderKatexSanitized,
|
|
||||||
sanitizeText,
|
|
||||||
} from '../../diagrams/common/common.js';
|
|
||||||
import { log } from '../../logger.js';
|
import { log } from '../../logger.js';
|
||||||
import { decodeEntities } from '../../utils.js';
|
import { decodeEntities } from '../../utils.js';
|
||||||
import { configureLabelImages } from './shapes/labelImageUtils.js';
|
import { addHtmlSpan } from '../createText.js';
|
||||||
|
|
||||||
const DEFAULT_WRAPPING_WIDTH = 200;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param dom
|
|
||||||
* @param styleFn
|
|
||||||
*/
|
|
||||||
function applyStyle(dom, styleFn) {
|
|
||||||
if (styleFn) {
|
|
||||||
dom.attr('style', styleFn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the wrapping width from config or returns the default.
|
|
||||||
* @returns {number} The wrapping width to use
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
|
|
||||||
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 span = div.append('span');
|
|
||||||
span.html(sanitizedLabel);
|
|
||||||
applyStyle(span, node.labelStyle);
|
|
||||||
span.attr('class', labelClass);
|
|
||||||
|
|
||||||
applyStyle(div, node.labelStyle);
|
|
||||||
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
|
* @deprecated svg-util/createText instead
|
||||||
*/
|
*/
|
||||||
const createLabel = async (_vertexText, style, isTitle, isNode, addBackground = false, width) => {
|
const createLabel = async (_vertexText, style, isTitle, isNode) => {
|
||||||
let vertexText = _vertexText || '';
|
let vertexText = _vertexText || '';
|
||||||
if (typeof vertexText === 'object') {
|
if (typeof vertexText === 'object') {
|
||||||
vertexText = vertexText[0];
|
vertexText = vertexText[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
if (evaluate(getConfig().flowchart.htmlLabels)) {
|
||||||
// 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);
|
||||||
const node = {
|
const node = {
|
||||||
@@ -114,28 +28,38 @@ const createLabel = async (_vertexText, style, isTitle, isNode, addBackground =
|
|||||||
),
|
),
|
||||||
labelStyle: style ? style.replace('fill:', 'color:') : style,
|
labelStyle: style ? style.replace('fill:', 'color:') : style,
|
||||||
};
|
};
|
||||||
return await addHtmlLabel(node, width, addBackground);
|
const config = getConfig();
|
||||||
}
|
const width = config.flowchart?.wrappingWidth || 200;
|
||||||
const svgLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
const tempContainer = select(document.createElementNS('http://www.w3.org/2000/svg', 'g'));
|
||||||
svgLabel.setAttribute('style', style.replace('color:', 'fill:'));
|
const vertexNode = await addHtmlSpan(tempContainer, node, width, '', false, config);
|
||||||
|
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 = [];
|
||||||
|
}
|
||||||
|
|
||||||
const rows =
|
for (const row of rows) {
|
||||||
typeof vertexText === 'string'
|
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||||
? vertexText.split(/\\n|\n|<br\s*\/?>/gi)
|
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
||||||
: Array.isArray(vertexText)
|
tspan.setAttribute('dy', '1em');
|
||||||
? vertexText
|
tspan.setAttribute('x', '0');
|
||||||
: [];
|
if (isTitle) {
|
||||||
|
tspan.setAttribute('class', 'title-row');
|
||||||
for (const row of rows) {
|
} else {
|
||||||
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
tspan.setAttribute('class', 'row');
|
||||||
tspan.setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:space', 'preserve');
|
}
|
||||||
tspan.setAttribute('dy', '1em');
|
tspan.textContent = row.trim();
|
||||||
tspan.setAttribute('x', '0');
|
svgLabel.appendChild(tspan);
|
||||||
tspan.setAttribute('class', isTitle ? 'title-row' : 'row');
|
}
|
||||||
tspan.textContent = row.trim();
|
return svgLabel;
|
||||||
svgLabel.appendChild(tspan);
|
|
||||||
}
|
}
|
||||||
return svgLabel;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createLabel;
|
export default createLabel;
|
||||||
|
|||||||
@@ -40,18 +40,13 @@ export const clear = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getLabelStyles = (styleArray) => {
|
export const getLabelStyles = (styleArray) => {
|
||||||
if (!styleArray) {
|
let styles = styleArray ? styleArray.reduce((acc, style) => acc + ';' + style, '') : '';
|
||||||
return '';
|
return styles;
|
||||||
}
|
|
||||||
if (typeof styleArray === 'string') {
|
|
||||||
return styleArray;
|
|
||||||
}
|
|
||||||
return styleArray.reduce((acc, style) => acc + ';' + style, '');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const insertEdgeLabel = async (elem, edge) => {
|
export const insertEdgeLabel = async (elem, edge) => {
|
||||||
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
|
||||||
const width = edge.width || getConfig().flowchart?.wrappingWidth;
|
|
||||||
const { labelStyles } = styles2String(edge);
|
const { labelStyles } = styles2String(edge);
|
||||||
edge.labelStyle = labelStyles;
|
edge.labelStyle = labelStyles;
|
||||||
const labelElement = await createText(elem, edge.label, {
|
const labelElement = await createText(elem, edge.label, {
|
||||||
@@ -92,11 +87,7 @@ export const insertEdgeLabel = async (elem, edge) => {
|
|||||||
// Create the actual text element
|
// Create the actual text element
|
||||||
const startLabelElement = await createLabel(
|
const startLabelElement = await createLabel(
|
||||||
edge.startLabelLeft,
|
edge.startLabelLeft,
|
||||||
getLabelStyles(edge.labelStyle),
|
getLabelStyles(edge.labelStyle)
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
useHtmlLabels,
|
|
||||||
width
|
|
||||||
);
|
);
|
||||||
const startEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
|
const startEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
|
||||||
const inner = startEdgeLabelLeft.insert('g').attr('class', 'inner');
|
const inner = startEdgeLabelLeft.insert('g').attr('class', 'inner');
|
||||||
@@ -113,11 +104,7 @@ export const insertEdgeLabel = async (elem, edge) => {
|
|||||||
// Create the actual text element
|
// Create the actual text element
|
||||||
const startLabelElement = await createLabel(
|
const startLabelElement = await createLabel(
|
||||||
edge.startLabelRight,
|
edge.startLabelRight,
|
||||||
getLabelStyles(edge.labelStyle),
|
getLabelStyles(edge.labelStyle)
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
useHtmlLabels,
|
|
||||||
width
|
|
||||||
);
|
);
|
||||||
const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
|
const startEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
|
||||||
const inner = startEdgeLabelRight.insert('g').attr('class', 'inner');
|
const inner = startEdgeLabelRight.insert('g').attr('class', 'inner');
|
||||||
@@ -134,14 +121,7 @@ export const insertEdgeLabel = async (elem, edge) => {
|
|||||||
}
|
}
|
||||||
if (edge.endLabelLeft) {
|
if (edge.endLabelLeft) {
|
||||||
// Create the actual text element
|
// Create the actual text element
|
||||||
const endLabelElement = await createLabel(
|
const endLabelElement = await createLabel(edge.endLabelLeft, getLabelStyles(edge.labelStyle));
|
||||||
edge.endLabelLeft,
|
|
||||||
getLabelStyles(edge.labelStyle),
|
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
useHtmlLabels,
|
|
||||||
width
|
|
||||||
);
|
|
||||||
const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
|
const endEdgeLabelLeft = elem.insert('g').attr('class', 'edgeTerminals');
|
||||||
const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner');
|
const inner = endEdgeLabelLeft.insert('g').attr('class', 'inner');
|
||||||
fo = inner.node().appendChild(endLabelElement);
|
fo = inner.node().appendChild(endLabelElement);
|
||||||
@@ -158,14 +138,7 @@ export const insertEdgeLabel = async (elem, edge) => {
|
|||||||
}
|
}
|
||||||
if (edge.endLabelRight) {
|
if (edge.endLabelRight) {
|
||||||
// Create the actual text element
|
// Create the actual text element
|
||||||
const endLabelElement = await createLabel(
|
const endLabelElement = await createLabel(edge.endLabelRight, getLabelStyles(edge.labelStyle));
|
||||||
edge.endLabelRight,
|
|
||||||
getLabelStyles(edge.labelStyle),
|
|
||||||
undefined,
|
|
||||||
false,
|
|
||||||
useHtmlLabels,
|
|
||||||
width
|
|
||||||
);
|
|
||||||
const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
|
const endEdgeLabelRight = elem.insert('g').attr('class', 'edgeTerminals');
|
||||||
const inner = endEdgeLabelRight.insert('g').attr('class', 'inner');
|
const inner = endEdgeLabelRight.insert('g').attr('class', 'inner');
|
||||||
|
|
||||||
|
|||||||
@@ -62,12 +62,9 @@ export async function erBox<T extends SVGGraphicsElement>(parent: D3Selection<T>
|
|||||||
|
|
||||||
// drawRect doesn't center non-htmlLabels correctly as of now, so translate label
|
// drawRect doesn't center non-htmlLabels correctly as of now, so translate label
|
||||||
if (!evaluate(config.htmlLabels)) {
|
if (!evaluate(config.htmlLabels)) {
|
||||||
const textElement = shapeSvg.select('.label text');
|
const textElement = shapeSvg.select('text');
|
||||||
const textNode = textElement.node() as SVGTextElement;
|
const bbox = (textElement.node() as SVGTextElement)?.getBBox();
|
||||||
if (textNode) {
|
textElement.attr('transform', `translate(${-bbox.width / 2}, 0)`);
|
||||||
const bbox = textNode.getBBox();
|
|
||||||
textElement.attr('transform', `translate(${-bbox.width / 2}, 0)`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return shapeSvg;
|
return shapeSvg;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
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);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -38,12 +38,9 @@ export async function rectWithTitle<T extends SVGGraphicsElement>(
|
|||||||
|
|
||||||
const description = node.description;
|
const description = node.description;
|
||||||
|
|
||||||
const title = node.label || '';
|
const title = node.label;
|
||||||
const width = node.width || getConfig().flowchart?.wrappingWidth;
|
|
||||||
|
|
||||||
const text = label
|
const text = label.node()!.appendChild(await createLabel(title, node.labelStyle, true, true));
|
||||||
.node()!
|
|
||||||
.appendChild(await createLabel(title, node.labelStyle, true, true, false, width));
|
|
||||||
let bbox = { width: 0, height: 0 };
|
let bbox = { width: 0, height: 0 };
|
||||||
if (evaluate(getConfig()?.flowchart?.htmlLabels)) {
|
if (evaluate(getConfig()?.flowchart?.htmlLabels)) {
|
||||||
const div = text.children[0];
|
const div = text.children[0];
|
||||||
@@ -59,12 +56,10 @@ export async function rectWithTitle<T extends SVGGraphicsElement>(
|
|||||||
.node()!
|
.node()!
|
||||||
.appendChild(
|
.appendChild(
|
||||||
await createLabel(
|
await createLabel(
|
||||||
Array.isArray(textRows) ? textRows.join('<br/>') : textRows,
|
textRows.join ? textRows.join('<br/>') : textRows,
|
||||||
node.labelStyle,
|
node.labelStyle,
|
||||||
true,
|
true,
|
||||||
true,
|
true
|
||||||
false,
|
|
||||||
width
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,10 @@ import { createText } from '../../createText.js';
|
|||||||
import type { Node } from '../../types.js';
|
import type { Node } from '../../types.js';
|
||||||
import { getConfig } from '../../../diagram-api/diagramAPI.js';
|
import { getConfig } from '../../../diagram-api/diagramAPI.js';
|
||||||
import { select } from 'd3';
|
import { select } from 'd3';
|
||||||
|
import defaultConfig from '../../../defaultConfig.js';
|
||||||
import { evaluate, sanitizeText } from '../../../diagrams/common/common.js';
|
import { evaluate, sanitizeText } from '../../../diagrams/common/common.js';
|
||||||
import { decodeEntities, handleUndefinedAttr } from '../../../utils.js';
|
import { decodeEntities, handleUndefinedAttr, parseFontSize } from '../../../utils.js';
|
||||||
import type { D3Selection, Point } from '../../../types.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>(
|
export const labelHelper = async <T extends SVGGraphicsElement>(
|
||||||
parent: D3Selection<T>,
|
parent: D3Selection<T>,
|
||||||
@@ -22,7 +13,7 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
|
|||||||
_classes?: string
|
_classes?: string
|
||||||
) => {
|
) => {
|
||||||
let cssClasses;
|
let cssClasses;
|
||||||
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.flowchart?.htmlLabels);
|
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig()?.htmlLabels);
|
||||||
if (!_classes) {
|
if (!_classes) {
|
||||||
cssClasses = 'node default';
|
cssClasses = 'node default';
|
||||||
} else {
|
} else {
|
||||||
@@ -66,7 +57,47 @@ export const labelHelper = async <T extends SVGGraphicsElement>(
|
|||||||
const dv = select(text);
|
const dv = select(text);
|
||||||
|
|
||||||
// if there are images, need to wait for them to load before getting the bounding box
|
// if there are images, need to wait for them to load before getting the bounding box
|
||||||
await configureLabelImages(div, label);
|
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);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
bbox = div.getBoundingClientRect();
|
bbox = div.getBoundingClientRect();
|
||||||
dv.attr('width', bbox.width);
|
dv.attr('width', bbox.width);
|
||||||
|
|||||||
Reference in New Issue
Block a user