mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-30 16:17:31 +01:00
Compare commits
3 Commits
renovate/t
...
markdown-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bd2b8cc44 | ||
|
|
db3227771c | ||
|
|
48ad4c5e06 |
5
.changeset/swift-cloths-run.md
Normal file
5
.changeset/swift-cloths-run.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: Add markdown-specific behavior on top of the common rendering changes.
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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' }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface MindmapNode {
|
||||
x?: number;
|
||||
y?: number;
|
||||
isRoot?: boolean;
|
||||
labelType?: string;
|
||||
}
|
||||
|
||||
export type FilledMindMapNode = RequiredDeep<MindmapNode>;
|
||||
|
||||
@@ -291,6 +291,7 @@ export const dataFetcher = (
|
||||
rx: 10,
|
||||
ry: 10,
|
||||
look,
|
||||
labelType: 'markdown',
|
||||
};
|
||||
|
||||
// Clear the label for dividers who have no description
|
||||
|
||||
@@ -161,6 +161,7 @@ export interface NodeData {
|
||||
centerLabel?: boolean;
|
||||
position?: string;
|
||||
description?: string | string[];
|
||||
labelType?: string;
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface NodeMetaData {
|
||||
priority: 'Very High' | 'High' | 'Medium' | 'Low' | 'Very Low';
|
||||
assigned?: string;
|
||||
ticket?: string;
|
||||
labelType?: string;
|
||||
}
|
||||
|
||||
export interface ParticipantMetaData {
|
||||
|
||||
Reference in New Issue
Block a user