239 lines
8.1 KiB
JavaScript
239 lines
8.1 KiB
JavaScript
|
import { l as log, J as decodeEntities } from "./mermaid-0d192ec3.js";
|
||
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
||
|
import { dedent } from "ts-dedent";
|
||
|
function preprocessMarkdown(markdown) {
|
||
|
const withoutMultipleNewlines = markdown.replace(/\n{2,}/g, "\n");
|
||
|
const withoutExtraSpaces = dedent(withoutMultipleNewlines);
|
||
|
return withoutExtraSpaces;
|
||
|
}
|
||
|
function markdownToLines(markdown) {
|
||
|
const preprocessedMarkdown = preprocessMarkdown(markdown);
|
||
|
const { children } = fromMarkdown(preprocessedMarkdown);
|
||
|
const lines = [[]];
|
||
|
let currentLine = 0;
|
||
|
function processNode(node, parentType = "normal") {
|
||
|
if (node.type === "text") {
|
||
|
const textLines = node.value.split("\n");
|
||
|
textLines.forEach((textLine, index) => {
|
||
|
if (index !== 0) {
|
||
|
currentLine++;
|
||
|
lines.push([]);
|
||
|
}
|
||
|
textLine.split(" ").forEach((word) => {
|
||
|
if (word) {
|
||
|
lines[currentLine].push({ content: word, type: parentType });
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
} else if (node.type === "strong" || node.type === "emphasis") {
|
||
|
node.children.forEach((contentNode) => {
|
||
|
processNode(contentNode, node.type);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
children.forEach((treeNode) => {
|
||
|
if (treeNode.type === "paragraph") {
|
||
|
treeNode.children.forEach((contentNode) => {
|
||
|
processNode(contentNode);
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
return lines;
|
||
|
}
|
||
|
function markdownToHTML(markdown) {
|
||
|
const { children } = fromMarkdown(markdown);
|
||
|
function output(node) {
|
||
|
if (node.type === "text") {
|
||
|
return node.value.replace(/\n/g, "<br/>");
|
||
|
} else if (node.type === "strong") {
|
||
|
return `<strong>${node.children.map(output).join("")}</strong>`;
|
||
|
} else if (node.type === "emphasis") {
|
||
|
return `<em>${node.children.map(output).join("")}</em>`;
|
||
|
} else if (node.type === "paragraph") {
|
||
|
return `<p>${node.children.map(output).join("")}</p>`;
|
||
|
}
|
||
|
return `Unsupported markdown: ${node.type}`;
|
||
|
}
|
||
|
return children.map(output).join("");
|
||
|
}
|
||
|
function splitTextToChars(text) {
|
||
|
if (Intl.Segmenter) {
|
||
|
return [...new Intl.Segmenter().segment(text)].map((s) => s.segment);
|
||
|
}
|
||
|
return [...text];
|
||
|
}
|
||
|
function splitWordToFitWidth(checkFit, word) {
|
||
|
const characters = splitTextToChars(word.content);
|
||
|
return splitWordToFitWidthRecursion(checkFit, [], characters, word.type);
|
||
|
}
|
||
|
function splitWordToFitWidthRecursion(checkFit, usedChars, remainingChars, type) {
|
||
|
if (remainingChars.length === 0) {
|
||
|
return [
|
||
|
{ content: usedChars.join(""), type },
|
||
|
{ content: "", type }
|
||
|
];
|
||
|
}
|
||
|
const [nextChar, ...rest] = remainingChars;
|
||
|
const newWord = [...usedChars, nextChar];
|
||
|
if (checkFit([{ content: newWord.join(""), type }])) {
|
||
|
return splitWordToFitWidthRecursion(checkFit, newWord, rest, type);
|
||
|
}
|
||
|
if (usedChars.length === 0 && nextChar) {
|
||
|
usedChars.push(nextChar);
|
||
|
remainingChars.shift();
|
||
|
}
|
||
|
return [
|
||
|
{ content: usedChars.join(""), type },
|
||
|
{ content: remainingChars.join(""), type }
|
||
|
];
|
||
|
}
|
||
|
function splitLineToFitWidth(line, checkFit) {
|
||
|
if (line.some(({ content }) => content.includes("\n"))) {
|
||
|
throw new Error("splitLineToFitWidth does not support newlines in the line");
|
||
|
}
|
||
|
return splitLineToFitWidthRecursion(line, checkFit);
|
||
|
}
|
||
|
function splitLineToFitWidthRecursion(words, checkFit, lines = [], newLine = []) {
|
||
|
if (words.length === 0) {
|
||
|
if (newLine.length > 0) {
|
||
|
lines.push(newLine);
|
||
|
}
|
||
|
return lines.length > 0 ? lines : [];
|
||
|
}
|
||
|
let joiner = "";
|
||
|
if (words[0].content === " ") {
|
||
|
joiner = " ";
|
||
|
words.shift();
|
||
|
}
|
||
|
const nextWord = words.shift() ?? { content: " ", type: "normal" };
|
||
|
const lineWithNextWord = [...newLine];
|
||
|
if (joiner !== "") {
|
||
|
lineWithNextWord.push({ content: joiner, type: "normal" });
|
||
|
}
|
||
|
lineWithNextWord.push(nextWord);
|
||
|
if (checkFit(lineWithNextWord)) {
|
||
|
return splitLineToFitWidthRecursion(words, checkFit, lines, lineWithNextWord);
|
||
|
}
|
||
|
if (newLine.length > 0) {
|
||
|
lines.push(newLine);
|
||
|
words.unshift(nextWord);
|
||
|
} else if (nextWord.content) {
|
||
|
const [line, rest] = splitWordToFitWidth(checkFit, nextWord);
|
||
|
lines.push([line]);
|
||
|
if (rest.content) {
|
||
|
words.unshift(rest);
|
||
|
}
|
||
|
}
|
||
|
return splitLineToFitWidthRecursion(words, checkFit, lines);
|
||
|
}
|
||
|
function applyStyle(dom, styleFn) {
|
||
|
if (styleFn) {
|
||
|
dom.attr("style", styleFn);
|
||
|
}
|
||
|
}
|
||
|
function addHtmlSpan(element, node, width, classes, addBackground = false) {
|
||
|
const fo = element.append("foreignObject");
|
||
|
const div = fo.append("xhtml:div");
|
||
|
const label = node.label;
|
||
|
const labelClass = node.isNode ? "nodeLabel" : "edgeLabel";
|
||
|
div.html(
|
||
|
`
|
||
|
<span class="${labelClass} ${classes}" ` + (node.labelStyle ? 'style="' + node.labelStyle + '"' : "") + ">" + label + "</span>"
|
||
|
);
|
||
|
applyStyle(div, node.labelStyle);
|
||
|
div.style("display", "table-cell");
|
||
|
div.style("white-space", "nowrap");
|
||
|
div.style("max-width", width + "px");
|
||
|
div.attr("xmlns", "http://www.w3.org/1999/xhtml");
|
||
|
if (addBackground) {
|
||
|
div.attr("class", "labelBkg");
|
||
|
}
|
||
|
let bbox = div.node().getBoundingClientRect();
|
||
|
if (bbox.width === width) {
|
||
|
div.style("display", "table");
|
||
|
div.style("white-space", "break-spaces");
|
||
|
div.style("width", width + "px");
|
||
|
bbox = div.node().getBoundingClientRect();
|
||
|
}
|
||
|
fo.style("width", bbox.width);
|
||
|
fo.style("height", bbox.height);
|
||
|
return fo.node();
|
||
|
}
|
||
|
function createTspan(textElement, lineIndex, lineHeight) {
|
||
|
return textElement.append("tspan").attr("class", "text-outer-tspan").attr("x", 0).attr("y", lineIndex * lineHeight - 0.1 + "em").attr("dy", lineHeight + "em");
|
||
|
}
|
||
|
function computeWidthOfText(parentNode, lineHeight, line) {
|
||
|
const testElement = parentNode.append("text");
|
||
|
const testSpan = createTspan(testElement, 1, lineHeight);
|
||
|
updateTextContentAndStyles(testSpan, line);
|
||
|
const textLength = testSpan.node().getComputedTextLength();
|
||
|
testElement.remove();
|
||
|
return textLength;
|
||
|
}
|
||
|
function createFormattedText(width, g, structuredText, addBackground = false) {
|
||
|
const lineHeight = 1.1;
|
||
|
const labelGroup = g.append("g");
|
||
|
const bkg = labelGroup.insert("rect").attr("class", "background");
|
||
|
const textElement = labelGroup.append("text").attr("y", "-10.1");
|
||
|
let lineIndex = 0;
|
||
|
for (const line of structuredText) {
|
||
|
const checkWidth = (line2) => computeWidthOfText(labelGroup, lineHeight, line2) <= width;
|
||
|
const linesUnderWidth = checkWidth(line) ? [line] : splitLineToFitWidth(line, checkWidth);
|
||
|
for (const preparedLine of linesUnderWidth) {
|
||
|
const tspan = createTspan(textElement, lineIndex, lineHeight);
|
||
|
updateTextContentAndStyles(tspan, preparedLine);
|
||
|
lineIndex++;
|
||
|
}
|
||
|
}
|
||
|
if (addBackground) {
|
||
|
const bbox = textElement.node().getBBox();
|
||
|
const padding = 2;
|
||
|
bkg.attr("x", -padding).attr("y", -padding).attr("width", bbox.width + 2 * padding).attr("height", bbox.height + 2 * padding);
|
||
|
return labelGroup.node();
|
||
|
} else {
|
||
|
return textElement.node();
|
||
|
}
|
||
|
}
|
||
|
function updateTextContentAndStyles(tspan, wrappedLine) {
|
||
|
tspan.text("");
|
||
|
wrappedLine.forEach((word, index) => {
|
||
|
const innerTspan = tspan.append("tspan").attr("font-style", word.type === "emphasis" ? "italic" : "normal").attr("class", "text-inner-tspan").attr("font-weight", word.type === "strong" ? "bold" : "normal");
|
||
|
if (index === 0) {
|
||
|
innerTspan.text(word.content);
|
||
|
} else {
|
||
|
innerTspan.text(" " + word.content);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
const createText = (el, text = "", {
|
||
|
style = "",
|
||
|
isTitle = false,
|
||
|
classes = "",
|
||
|
useHtmlLabels = true,
|
||
|
isNode = true,
|
||
|
width = 200,
|
||
|
addSvgBackground = false
|
||
|
} = {}) => {
|
||
|
log.info("createText", text, style, isTitle, classes, useHtmlLabels, isNode, addSvgBackground);
|
||
|
if (useHtmlLabels) {
|
||
|
const htmlText = markdownToHTML(text);
|
||
|
const node = {
|
||
|
isNode,
|
||
|
label: decodeEntities(htmlText).replace(
|
||
|
/fa[blrs]?:fa-[\w-]+/g,
|
||
|
(s) => `<i class='${s.replace(":", " ")}'></i>`
|
||
|
),
|
||
|
labelStyle: style.replace("fill:", "color:")
|
||
|
};
|
||
|
const vertexNode = addHtmlSpan(el, node, width, classes, addSvgBackground);
|
||
|
return vertexNode;
|
||
|
} else {
|
||
|
const structuredText = markdownToLines(text);
|
||
|
const svgLabel = createFormattedText(width, el, structuredText, addSvgBackground);
|
||
|
return svgLabel;
|
||
|
}
|
||
|
};
|
||
|
export {
|
||
|
createText as c
|
||
|
};
|