UNPKG

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
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(/&#39;/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, "&nbsp;"); } 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 };