mermaid
Version:
Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.
418 lines (412 loc) • 14.9 kB
JavaScript
import {
getIconSVG,
isIconAvailable
} from "./chunk-4I5QYGJK.mjs";
import {
decodeEntities
} from "./chunk-NSK5VX7P.mjs";
import {
common_default,
getConfig,
hasKatex,
renderKatexSanitized,
sanitizeText
} from "./chunk-I66GZJ75.mjs";
import {
log
} from "./chunk-X3CZISLH.mjs";
import {
__name
} from "./chunk-Y2CYZVJY.mjs";
// src/rendering-util/createText.ts
import { select } from "d3";
// src/rendering-util/handle-markdown-text.ts
import { marked } from "marked";
import { dedent } from "ts-dedent";
function preprocessMarkdown(markdown, { markdownAutoWrap }) {
const withoutBR = markdown.replace(/<br\/>/g, "\n");
const withoutMultipleNewlines = withoutBR.replace(/\n{2,}/g, "\n");
const withoutExtraSpaces = dedent(withoutMultipleNewlines);
if (markdownAutoWrap === false) {
}
return withoutExtraSpaces;
}
__name(preprocessMarkdown, "preprocessMarkdown");
function nonMarkdownToLines(nonMarkdownText) {
return nonMarkdownText.split(/\\n|\n|<br\s*\/?>/gi).map(
(line) => line.trim().match(/<[^>]+>|[^\s<>]+/g)?.map((word) => ({ content: word, type: "normal" })) ?? []
);
}
__name(nonMarkdownToLines, "nonMarkdownToLines");
function markdownToLines(markdown, config = {}) {
const preprocessedMarkdown = preprocessMarkdown(markdown, config);
const nodes = marked.lexer(preprocessedMarkdown);
const lines = [[]];
let currentLine = 0;
function processNode(node, parentType = "normal") {
if (node.type === "text") {
const textLines = node.text.split("\n");
textLines.forEach((textLine, index) => {
if (index !== 0) {
currentLine++;
lines.push([]);
}
textLine.split(" ").forEach((word) => {
word = word.replace(/'/g, `'`);
if (word) {
lines[currentLine].push({ content: word, type: parentType });
}
});
});
} else if (node.type === "strong" || node.type === "em") {
node.tokens.forEach((contentNode) => {
processNode(contentNode, node.type);
});
} else if (node.type === "html") {
lines[currentLine].push({ content: node.text, type: "normal" });
}
}
__name(processNode, "processNode");
nodes.forEach((treeNode) => {
if (treeNode.type === "paragraph") {
treeNode.tokens?.forEach((contentNode) => {
processNode(contentNode);
});
} else if (treeNode.type === "html") {
lines[currentLine].push({ content: treeNode.text, type: "normal" });
} else {
lines[currentLine].push({ content: treeNode.raw, type: "normal" });
}
});
return lines;
}
__name(markdownToLines, "markdownToLines");
function nonMarkdownToHTML(text) {
if (!text) {
return "";
}
return `<p>${/**
* Replace new lines with <br /> tags.
*
* Unlike in markdown text, `\n` sequences are treated as line breaks here.
*/
text.replace(/\\n|\n/g, "<br />")}</p>`;
}
__name(nonMarkdownToHTML, "nonMarkdownToHTML");
function markdownToHTML(markdown, { markdownAutoWrap } = {}) {
const nodes = marked.lexer(markdown);
function output(node) {
if (node.type === "text") {
if (markdownAutoWrap === false) {
return node.text.replace(/\n */g, "<br/>").replace(/ /g, " ");
}
return node.text.replace(/\n */g, "<br/>");
} else if (node.type === "strong") {
return `<strong>${node.tokens?.map(output).join("")}</strong>`;
} else if (node.type === "em") {
return `<em>${node.tokens?.map(output).join("")}</em>`;
} else if (node.type === "paragraph") {
return `<p>${node.tokens?.map(output).join("")}</p>`;
} else if (node.type === "space") {
return "";
} else if (node.type === "html") {
return `${node.text}`;
} else if (node.type === "escape") {
return node.text;
}
log.warn(`Unsupported markdown: ${node.type}`);
return node.raw;
}
__name(output, "output");
return nodes.map(output).join("");
}
__name(markdownToHTML, "markdownToHTML");
// src/rendering-util/splitText.ts
function splitTextToChars(text) {
if (Intl.Segmenter) {
return [...new Intl.Segmenter().segment(text)].map((s) => s.segment);
}
return [...text];
}
__name(splitTextToChars, "splitTextToChars");
function splitWordToFitWidth(checkFit, word) {
const characters = splitTextToChars(word.content);
return splitWordToFitWidthRecursion(checkFit, [], characters, word.type);
}
__name(splitWordToFitWidth, "splitWordToFitWidth");
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 }
];
}
__name(splitWordToFitWidthRecursion, "splitWordToFitWidthRecursion");
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);
}
__name(splitLineToFitWidth, "splitLineToFitWidth");
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);
}
__name(splitLineToFitWidthRecursion, "splitLineToFitWidthRecursion");
// src/rendering-util/createText.ts
function applyStyle(dom, styleFn) {
if (styleFn) {
dom.attr("style", styleFn);
}
}
__name(applyStyle, "applyStyle");
var maxSafeSizeForWidth = 16384;
async function addHtmlSpan(element, node, width, classes, addBackground = false, config = getConfig()) {
const fo = element.append("foreignObject");
fo.attr("width", `${Math.min(10 * width, maxSafeSizeForWidth)}px`);
fo.attr("height", `${Math.min(10 * width, maxSafeSizeForWidth)}px`);
const div = fo.append("xhtml:div");
const sanitizedLabel = hasKatex(node.label) ? await renderKatexSanitized(node.label.replace(common_default.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} ${classes}`);
applyStyle(div, node.labelStyle);
div.style("display", "table-cell");
div.style("white-space", "nowrap");
div.style("line-height", "1.5");
if (width !== Number.POSITIVE_INFINITY) {
div.style("max-width", width + "px");
div.style("text-align", "center");
}
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();
}
return fo.node();
}
__name(addHtmlSpan, "addHtmlSpan");
function createTspan(textElement, lineIndex, lineHeight, centerText = false) {
const tspan = textElement.append("tspan").attr("class", "text-outer-tspan").attr("x", 0).attr("y", lineIndex * lineHeight - 0.1 + "em").attr("dy", lineHeight + "em");
if (centerText) {
tspan.attr("text-anchor", "middle");
}
return tspan;
}
__name(createTspan, "createTspan");
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;
}
__name(computeWidthOfText, "computeWidthOfText");
function computeDimensionOfText(parentNode, lineHeight, text) {
const testElement = parentNode.append("text");
const testSpan = createTspan(testElement, 1, lineHeight);
updateTextContentAndStyles(testSpan, [{ content: text, type: "normal" }]);
const textDimension = testSpan.node()?.getBoundingClientRect();
if (textDimension) {
testElement.remove();
}
return textDimension;
}
__name(computeDimensionOfText, "computeDimensionOfText");
function createFormattedText(width, g, structuredText, addBackground = false, centerText = false) {
const lineHeight = 1.1;
const labelGroup = g.append("g");
const bkg = labelGroup.insert("rect").attr("class", "background").attr("style", "stroke: none");
const textElement = labelGroup.append("text").attr("y", "-10.1");
if (centerText) {
textElement.attr("text-anchor", "middle");
}
let lineIndex = 0;
for (const line of structuredText) {
const checkWidth = /* @__PURE__ */ __name((line2) => computeWidthOfText(labelGroup, lineHeight, line2) <= width, "checkWidth");
const linesUnderWidth = checkWidth(line) ? [line] : splitLineToFitWidth(line, checkWidth);
for (const preparedLine of linesUnderWidth) {
const tspan = createTspan(textElement, lineIndex, lineHeight, centerText);
updateTextContentAndStyles(tspan, preparedLine);
lineIndex++;
}
}
if (addBackground) {
const bbox = textElement.node().getBBox();
const padding = 2;
bkg.attr("x", bbox.x - padding).attr("y", bbox.y - padding).attr("width", bbox.width + 2 * padding).attr("height", bbox.height + 2 * padding);
return labelGroup.node();
} else {
return textElement.node();
}
}
__name(createFormattedText, "createFormattedText");
function decodeHTMLEntities(text) {
const regex = /&(amp|lt|gt);/g;
return text.replace(regex, (match, entity) => {
switch (entity) {
case "amp":
return "&";
case "lt":
return "<";
case "gt":
return ">";
default:
return match;
}
});
}
__name(decodeHTMLEntities, "decodeHTMLEntities");
function updateTextContentAndStyles(tspan, wrappedLine) {
tspan.text("");
wrappedLine.forEach((word, index) => {
const innerTspan = tspan.append("tspan").attr("font-style", word.type === "em" ? "italic" : "normal").attr("class", "text-inner-tspan").attr("font-weight", word.type === "strong" ? "bold" : "normal");
if (index === 0) {
innerTspan.text(decodeHTMLEntities(word.content));
} else {
innerTspan.text(" " + decodeHTMLEntities(word.content));
}
});
}
__name(updateTextContentAndStyles, "updateTextContentAndStyles");
async function replaceIconSubstring(text, config = {}) {
const pendingReplacements = [];
text.replace(/(fa[bklrs]?):fa-([\w-]+)/g, (fullMatch, prefix, iconName) => {
pendingReplacements.push(
(async () => {
const registeredIconName = `${prefix}:${iconName}`;
if (await isIconAvailable(registeredIconName)) {
return await getIconSVG(registeredIconName, void 0, { class: "label-icon" });
} else {
return `<i class='${sanitizeText(fullMatch, config).replace(":", " ")}'></i>`;
}
})()
);
return fullMatch;
});
const replacements = await Promise.all(pendingReplacements);
return text.replace(/(fa[bklrs]?):fa-([\w-]+)/g, () => replacements.shift() ?? "");
}
__name(replaceIconSubstring, "replaceIconSubstring");
var createText = /* @__PURE__ */ __name(async (el, text = "", {
style = "",
isTitle = false,
classes = "",
useHtmlLabels = true,
markdown = true,
isNode = true,
/**
* The width to wrap the text within. Set to `Number.POSITIVE_INFINITY` for no wrapping.
*/
width = 200,
addSvgBackground = false
} = {}, config) => {
log.debug(
"XYZ createText",
text,
style,
isTitle,
classes,
useHtmlLabels,
isNode,
"addSvgBackground: ",
addSvgBackground
);
if (useHtmlLabels) {
const htmlText = markdown ? markdownToHTML(text, config) : nonMarkdownToHTML(text);
const decodedReplacedText = await replaceIconSubstring(decodeEntities(htmlText), config);
const inputForKatex = text.replace(/\\\\/g, "\\");
const node = {
isNode,
label: hasKatex(text) ? inputForKatex : decodedReplacedText,
labelStyle: style.replace("fill:", "color:")
};
const vertexNode = await addHtmlSpan(el, node, width, classes, addSvgBackground, config);
return vertexNode;
} else {
const sanitizeBR = decodeEntities(text.replace(/<br\s*\/?>/g, "<br/>"));
const structuredText = markdown ? markdownToLines(sanitizeBR.replace("<br>", "<br/>"), config) : nonMarkdownToLines(sanitizeBR);
const svgLabel = createFormattedText(
width,
el,
structuredText,
text ? addSvgBackground : false,
!isNode
);
if (isNode) {
if (/stroke:/.exec(style)) {
style = style.replace("stroke:", "lineColor:");
}
const nodeLabelTextStyle = style.replace(/stroke:[^;]+;?/g, "").replace(/stroke-width:[^;]+;?/g, "").replace(/fill:[^;]+;?/g, "").replace(/color:/g, "fill:");
select(svgLabel).attr("style", nodeLabelTextStyle);
} else {
const edgeLabelRectStyle = style.replace(/stroke:[^;]+;?/g, "").replace(/stroke-width:[^;]+;?/g, "").replace(/fill:[^;]+;?/g, "").replace(/background:/g, "fill:");
select(svgLabel).select("rect").attr("style", edgeLabelRectStyle.replace(/background:/g, "fill:"));
const edgeLabelTextStyle = style.replace(/stroke:[^;]+;?/g, "").replace(/stroke-width:[^;]+;?/g, "").replace(/fill:[^;]+;?/g, "").replace(/color:/g, "fill:");
select(svgLabel).select("text").attr("style", edgeLabelTextStyle);
}
if (isTitle) {
select(svgLabel).selectAll("tspan.text-outer-tspan").classed("title-row", true);
} else {
select(svgLabel).selectAll("tspan.text-outer-tspan").classed("row", true);
}
return svgLabel;
}
}, "createText");
export {
computeDimensionOfText,
createText
};