UNPKG

mermaid

Version:

Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.

442 lines (435 loc) 15.8 kB
import { decodeEntities } from "./chunk-55PJQP7W.mjs"; import { __name, common_default, getConfig2 as getConfig, hasKatex, log, renderKatex, sanitizeText2 as sanitizeText } from "./chunk-3XYRH5AP.mjs"; // src/rendering-util/icons.ts import { getIconData, iconToHTML, iconToSVG, replaceIDs, stringToIcon } from "@iconify/utils"; var unknownIcon = { body: '<g><rect width="80" height="80" style="fill: #087ebf; stroke-width: 0px;"/><text transform="translate(21.16 64.67)" style="fill: #fff; font-family: ArialMT, Arial; font-size: 67.75px;"><tspan x="0" y="0">?</tspan></text></g>', height: 80, width: 80 }; var iconsStore = /* @__PURE__ */ new Map(); var loaderStore = /* @__PURE__ */ new Map(); var registerIconPacks = /* @__PURE__ */ __name((iconLoaders) => { for (const iconLoader of iconLoaders) { if (!iconLoader.name) { throw new Error( 'Invalid icon loader. Must have a "name" property with non-empty string value.' ); } log.debug("Registering icon pack:", iconLoader.name); if ("loader" in iconLoader) { loaderStore.set(iconLoader.name, iconLoader.loader); } else if ("icons" in iconLoader) { iconsStore.set(iconLoader.name, iconLoader.icons); } else { log.error("Invalid icon loader:", iconLoader); throw new Error('Invalid icon loader. Must have either "icons" or "loader" property.'); } } }, "registerIconPacks"); var getRegisteredIconData = /* @__PURE__ */ __name(async (iconName, fallbackPrefix) => { const data = stringToIcon(iconName, true, fallbackPrefix !== void 0); if (!data) { throw new Error(`Invalid icon name: ${iconName}`); } const prefix = data.prefix || fallbackPrefix; if (!prefix) { throw new Error(`Icon name must contain a prefix: ${iconName}`); } let icons = iconsStore.get(prefix); if (!icons) { const loader = loaderStore.get(prefix); if (!loader) { throw new Error(`Icon set not found: ${data.prefix}`); } try { const loaded = await loader(); icons = { ...loaded, prefix }; iconsStore.set(prefix, icons); } catch (e) { log.error(e); throw new Error(`Failed to load icon set: ${data.prefix}`); } } const iconData = getIconData(icons, data.name); if (!iconData) { throw new Error(`Icon not found: ${iconName}`); } return iconData; }, "getRegisteredIconData"); var isIconAvailable = /* @__PURE__ */ __name(async (iconName) => { try { await getRegisteredIconData(iconName); return true; } catch { return false; } }, "isIconAvailable"); var getIconSVG = /* @__PURE__ */ __name(async (iconName, customisations, extraAttributes) => { let iconData; try { iconData = await getRegisteredIconData(iconName, customisations?.fallbackPrefix); } catch (e) { log.error(e); iconData = unknownIcon; } const renderData = iconToSVG(iconData, customisations); const svg = iconToHTML(replaceIDs(renderData.body), { ...renderData.attributes, ...extraAttributes }); return svg; }, "getIconSVG"); // 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.replace(/ /g, "&nbsp;"); } return withoutExtraSpaces; } __name(preprocessMarkdown, "preprocessMarkdown"); 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" }); } }); return lines; } __name(markdownToLines, "markdownToLines"); 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; } return `Unsupported markdown: ${node.type}`; } __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"); async function addHtmlSpan(element, node, width, classes, addBackground = false) { const fo = element.append("foreignObject"); fo.attr("width", `${10 * width}px`); fo.attr("height", `${10 * width}px`); const div = fo.append("xhtml:div"); let label = node.label; if (node.label && hasKatex(node.label)) { label = await renderKatex(node.label.replace(common_default.lineBreakRegex, "\n"), getConfig()); } const labelClass = node.isNode ? "nodeLabel" : "edgeLabel"; const span = div.append("span"); span.html(label); 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"); 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) { return textElement.append("tspan").attr("class", "text-outer-tspan").attr("x", 0).attr("y", lineIndex * lineHeight - 0.1 + "em").attr("dy", lineHeight + "em"); } __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) { 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"); 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); 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 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(word.content); } else { innerTspan.text(" " + word.content); } }); } __name(updateTextContentAndStyles, "updateTextContentAndStyles"); async function replaceIconSubstring(text) { 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).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, isNode = true, width = 200, addSvgBackground = false } = {}, config) => { log.debug( "XYZ createText", text, style, isTitle, classes, useHtmlLabels, isNode, "addSvgBackground: ", addSvgBackground ); if (useHtmlLabels) { const htmlText = markdownToHTML(text, config); const decodedReplacedText = await replaceIconSubstring(decodeEntities(htmlText)); 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); return vertexNode; } else { const sanitizeBR = text.replace(/<br\s*\/?>/g, "<br/>"); const structuredText = markdownToLines(sanitizeBR.replace("<br>", "<br/>"), config); const svgLabel = createFormattedText( width, el, structuredText, text ? addSvgBackground : false ); 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); } return svgLabel; } }, "createText"); export { unknownIcon, registerIconPacks, getIconSVG, computeDimensionOfText, replaceIconSubstring, createText };