From f699575b4520a7e78209909da0c042b6acfc4f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Tue, 22 Feb 2022 12:02:53 +0100 Subject: [PATCH] Cache mermaid graph rendering (#1023) * Cache mermaid graph rendering * Bump mermaid * Update naming --- assets/js/cell/markdown/mermaid.js | 49 +++++++++++++++++++++--------- assets/js/lib/cache_lru.js | 33 ++++++++++++++++++++ assets/package-lock.json | 28 ++++++++--------- 3 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 assets/js/lib/cache_lru.js diff --git a/assets/js/cell/markdown/mermaid.js b/assets/js/cell/markdown/mermaid.js index 4dd107d32..3a50ea790 100644 --- a/assets/js/cell/markdown/mermaid.js +++ b/assets/js/cell/markdown/mermaid.js @@ -1,3 +1,6 @@ +import { md5Base64 } from "../../lib/utils"; +import CacheLRU from "../../lib/cache_lru"; + let idCount = 0; let getId = () => `mermaid-graph-${idCount++}`; @@ -5,6 +8,32 @@ let mermaidInitialized = false; const fontAwesomeVersion = "5.15.4"; +const cache = new CacheLRU(25); + +/** + * Renders SVG graph from mermaid definition. + */ +export function renderMermaid(definition) { + const hash = md5Base64(definition); + const svg = cache.get(hash); + + if (svg) { + return Promise.resolve(svg); + } + + return importMermaid().then((mermaid) => { + injectFontAwesomeIfNeeded(definition); + + try { + const svg = mermaid.render(getId(), definition); + cache.set(hash, svg); + return svg; + } catch (e) { + return `
Mermaid\n${e.message}
`; + } + }); +} + function importMermaid() { return import( /* webpackChunkName: "mermaid" */ @@ -18,10 +47,13 @@ function importMermaid() { }); } -const maybeInjectFontAwesome = (value) => { +function injectFontAwesomeIfNeeded(definition) { const fontAwesomeUrl = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${fontAwesomeVersion}/css/all.min.css`; + + // Graphs may include Font Awesome icons via fa: prefix, so we + // load the icon set if needed if ( - value.includes("fa:") && + definition.includes("fa:") && !document.querySelector(`link[href="${fontAwesomeUrl}"]`) ) { const link = document.createElement("link"); @@ -30,17 +62,4 @@ const maybeInjectFontAwesome = (value) => { link.href = fontAwesomeUrl; document.head.appendChild(link); } -}; - -export function renderMermaid(value) { - return importMermaid().then((mermaid) => { - // Inject font-awesome when fa: prefix is used - maybeInjectFontAwesome(value); - - try { - return mermaid.render(getId(), value); - } catch (e) { - return `
Mermaid\n${e.message}
`; - } - }); } diff --git a/assets/js/lib/cache_lru.js b/assets/js/lib/cache_lru.js new file mode 100644 index 000000000..8e25c073d --- /dev/null +++ b/assets/js/lib/cache_lru.js @@ -0,0 +1,33 @@ +/** + * A Map-based LRU cache. + */ +export default class CacheLRU { + constructor(size) { + this.size = size; + this.cache = new Map(); + } + + get(key) { + if (this.cache.has(key)) { + const value = this.cache.get(key); + // Map keys are stored and iterated in insertion order, + // so we reinsert on every access + this.cache.delete(key); + this.cache.set(key, value); + return value; + } else { + return undefined; + } + } + + set(key, value) { + if (this.cache.has(key)) { + this.cache.delete(key); + } else if (this.cache.size === this.size) { + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + + this.cache.set(key, value); + } +} diff --git a/assets/package-lock.json b/assets/package-lock.json index 3a930687a..5e971cac4 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -5007,9 +5007,9 @@ } }, "node_modules/dompurify": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.4.tgz", - "integrity": "sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ==" + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.5.tgz", + "integrity": "sha512-kD+f8qEaa42+mjdOpKeztu9Mfx5bv9gVLO6K9jRx4uGvh6Wv06Srn4jr1wPNY2OOUGGSKHNFN+A8MA3v0E0QAQ==" }, "node_modules/domutils": { "version": "2.8.0", @@ -8810,15 +8810,15 @@ } }, "node_modules/mermaid": { - "version": "8.13.9", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.13.9.tgz", - "integrity": "sha512-kMH676xEomSe/gzxMpDx91L+z9L+9iB3lvtPFA8aeOPRNNrfd3ZDvDCGFnuqQaJvPRCxs3Me2JDaVVNOZjojrg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.14.0.tgz", + "integrity": "sha512-ITSHjwVaby1Li738sxhF48sLTxcNyUAoWfoqyztL1f7J6JOLpHOuQPNLBb6lxGPUA0u7xP9IRULgvod0dKu35A==", "dependencies": { "@braintree/sanitize-url": "^3.1.0", "d3": "^7.0.0", "dagre": "^0.8.5", "dagre-d3": "^0.6.4", - "dompurify": "2.3.4", + "dompurify": "2.3.5", "graphlib": "^2.1.8", "khroma": "^1.4.1", "moment-mini": "^2.24.0", @@ -16445,9 +16445,9 @@ } }, "dompurify": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.4.tgz", - "integrity": "sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ==" + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.5.tgz", + "integrity": "sha512-kD+f8qEaa42+mjdOpKeztu9Mfx5bv9gVLO6K9jRx4uGvh6Wv06Srn4jr1wPNY2OOUGGSKHNFN+A8MA3v0E0QAQ==" }, "domutils": { "version": "2.8.0", @@ -19230,15 +19230,15 @@ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, "mermaid": { - "version": "8.13.9", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.13.9.tgz", - "integrity": "sha512-kMH676xEomSe/gzxMpDx91L+z9L+9iB3lvtPFA8aeOPRNNrfd3ZDvDCGFnuqQaJvPRCxs3Me2JDaVVNOZjojrg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.14.0.tgz", + "integrity": "sha512-ITSHjwVaby1Li738sxhF48sLTxcNyUAoWfoqyztL1f7J6JOLpHOuQPNLBb6lxGPUA0u7xP9IRULgvod0dKu35A==", "requires": { "@braintree/sanitize-url": "^3.1.0", "d3": "^7.0.0", "dagre": "^0.8.5", "dagre-d3": "^0.6.4", - "dompurify": "2.3.4", + "dompurify": "2.3.5", "graphlib": "^2.1.8", "khroma": "^1.4.1", "moment-mini": "^2.24.0",