diff --git a/src/hooks/markdownMermaid.js b/src/hooks/markdownMermaid.js index 669423c..acb839d 100644 --- a/src/hooks/markdownMermaid.js +++ b/src/hooks/markdownMermaid.js @@ -1,7 +1,17 @@ import mermaid from "mermaid"; import { waitForElement } from "../utils"; +import { getLineById } from "./markdownSourceMap"; +import IMurMurHash from "imurmurhash"; -const markdownItMermaid = (md, { preview }) => { +// We want to keep a cache based on line numbers to retrieve the previous version. +// This allows for a flicker-free editing experience. +// key = line number +const lineCache = new Map(); +const hashSeed = 42; +// key = hash of diagram source code +const contentCache = new Map(); + +const markdownItMermaid = (md, { preview, lineMap, parent }) => { mermaid.initialize({ theme: "neutral", suppressErrorRendering: true, @@ -21,23 +31,45 @@ const markdownItMermaid = (md, { preview }) => { } const code = token.content.trim(); + const lineNumber = getLineById(lineMap.current, token.attrGet("data-line-id")); + let cached = lineCache.get(lineNumber); + if (!cached) { + cached = contentCache.get(new IMurMurHash(code, hashSeed).result()); + } const id = Math.random().toString().replace(".", ""); token.attrSet("data-mermaid-id", id); - waitForElement(preview.current, `[data-mermaid-id="${id}"]`).then((el) => { - mermaid - .render(`mermaid-${id}`, code) - .then(({ svg }) => { - el.innerHTML = svg; - el.className = "mermaid"; - }) - .catch((err) => { - el.innerHTML = `Mermaid error:\n${err}`; - el.classList.remove("mermaid"); - }); - }); - - return `
${code}
`; + if (cached) { + token.attrSet("class", "mermaid"); + } + + if (!cached || cached.code !== code) { + const container = document.createElement("div"); + container.style.position = "fixed"; + container.style.visibility = "none"; + document.body.appendChild(container); + + waitForElement(preview.current, `[data-mermaid-id="${id}"]`).then((el) => { + mermaid + .render(`mermaid-${id}`, code, container) + .then(({ svg }) => { + const saved = { svg, code }; + lineCache.set(lineNumber, saved); + contentCache.set(new IMurMurHash(code, hashSeed).result(), saved); + el.innerHTML = svg; + el.className = "mermaid"; + }) + .catch((err) => { + el.innerHTML = `Mermaid error:\n${err}`; + el.classList.remove("mermaid"); + }) + .finally(() => { + container.remove(); + }); + }); + } + + return `
${cached?.svg ?? code}
`; }; }; diff --git a/src/hooks/markdownSourceMap.js b/src/hooks/markdownSourceMap.js index 0f23fb4..289453f 100644 --- a/src/hooks/markdownSourceMap.js +++ b/src/hooks/markdownSourceMap.js @@ -135,3 +135,11 @@ export function findNearestElementForLine(lineNumber, lineMap, preview) { return [match, num]; } + +export function getLineById(lineMap, id) { + for (const [line, value] of lineMap.entries()) { + if (value === id) { + return line; + } + } +} diff --git a/src/hooks/useText.js b/src/hooks/useText.js index 4f8c577..893b395 100644 --- a/src/hooks/useText.js +++ b/src/hooks/useText.js @@ -82,7 +82,7 @@ export const useText = ({ initialText, transforms, customRoles, preview, backsla .use(markdownitDocutils) .use(markdownReplacer(transforms, parent)) .use(useCustomRoles(customRoles, parent)) - .use(markdownMermaid, { preview }) + .use(markdownMermaid, { preview, lineMap, parent }) .use(markdownSourceMap); if (backslashLineBreak) md.use(backslashLineBreakPlugin); return md;