From 616146f90400bcf80671410c04926d9dc1f11257 Mon Sep 17 00:00:00 2001
From: silverwind <me@silverwind.io>
Date: Wed, 16 Feb 2022 04:28:29 +0100
Subject: [PATCH] Various Mermaid improvements (#18776)

* Various Mermaid improvments

- Render into iframe for improved security
- Use built-in dark theme instead of color inversion
- Remove flexbox attributes, resulting in more consistent size rendering
- Update API usage and update to latest version

* restart ci

* misc tweaks

* remove unneccesary declaration

* make it work without allow-same-origin, add loading=lazy

* remove loading attribute, does not seem to work

* rename variable

* skip roundtrip to DOM for rendering

* don't guess chart height

* update comment to make it clear it's intentional

* tweak

* replace deprecated 'scrolling' property

* remove unused css file

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
---
 package-lock.json                        | 30 +++++++++---------
 package.json                             |  2 +-
 web_src/js/markup/mermaid.js             | 39 +++++++++++++++---------
 web_src/less/_base.less                  |  1 +
 web_src/less/animations.less             |  2 +-
 web_src/less/index.less                  |  1 -
 web_src/less/markup/content.less         |  8 +++++
 web_src/less/markup/mermaid.less         | 13 --------
 web_src/less/themes/theme-arc-green.less |  4 ---
 9 files changed, 50 insertions(+), 50 deletions(-)
 delete mode 100644 web_src/less/markup/mermaid.less

diff --git a/package-lock.json b/package-lock.json
index e14de9b68d..0f4500e23b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,7 @@
         "less": "4.1.2",
         "less-loader": "10.2.0",
         "license-checker-webpack-plugin": "0.2.1",
-        "mermaid": "8.13.10",
+        "mermaid": "8.14.0",
         "mini-css-extract-plugin": "2.5.3",
         "monaco-editor": "0.32.1",
         "monaco-editor-webpack-plugin": "7.0.1",
@@ -3466,9 +3466,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",
@@ -6751,15 +6751,15 @@
       }
     },
     "node_modules/mermaid": {
-      "version": "8.13.10",
-      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.13.10.tgz",
-      "integrity": "sha512-2ANep359uML87+wiYaWSu83eg9Qc0xCLnNJdCh100m4v0orS3fp8SScsZLcDSElRGHi+1zuVJsEEVEWH05+COQ==",
+      "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",
@@ -12526,9 +12526,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",
@@ -14898,15 +14898,15 @@
       "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
     },
     "mermaid": {
-      "version": "8.13.10",
-      "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-8.13.10.tgz",
-      "integrity": "sha512-2ANep359uML87+wiYaWSu83eg9Qc0xCLnNJdCh100m4v0orS3fp8SScsZLcDSElRGHi+1zuVJsEEVEWH05+COQ==",
+      "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",
diff --git a/package.json b/package.json
index 0c71cf9726..67de8a7f0a 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
     "less": "4.1.2",
     "less-loader": "10.2.0",
     "license-checker-webpack-plugin": "0.2.1",
-    "mermaid": "8.13.10",
+    "mermaid": "8.14.0",
     "mini-css-extract-plugin": "2.5.3",
     "monaco-editor": "0.32.1",
     "monaco-editor-webpack-plugin": "7.0.1",
diff --git a/web_src/js/markup/mermaid.js b/web_src/js/markup/mermaid.js
index 7c7ee26c3c..773c46e791 100644
--- a/web_src/js/markup/mermaid.js
+++ b/web_src/js/markup/mermaid.js
@@ -1,5 +1,11 @@
+import {isDarkTheme} from '../utils.js';
 const {mermaidMaxSourceCharacters} = window.config;
 
+const iframeCss = `
+  body {margin: 0; padding: 0}
+  #mermaid {display: block; margin: 0 auto}
+`;
+
 function displayError(el, err) {
   el.closest('pre').classList.remove('is-loading');
   const errorNode = document.createElement('div');
@@ -15,26 +21,22 @@ export async function renderMermaid() {
   const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid');
 
   mermaid.initialize({
-    mermaid: {
-      startOnLoad: false,
-    },
-    flowchart: {
-      useMaxWidth: true,
-      htmlLabels: false,
-    },
-    theme: 'neutral',
+    startOnLoad: false,
+    theme: isDarkTheme() ? 'dark' : 'neutral',
     securityLevel: 'strict',
   });
 
   for (const el of els) {
-    if (mermaidMaxSourceCharacters >= 0 && el.textContent.length > mermaidMaxSourceCharacters) {
-      displayError(el, new Error(`Mermaid source of ${el.textContent.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
+    const source = el.textContent;
+
+    if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) {
+      displayError(el, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`));
       continue;
     }
 
     let valid;
     try {
-      valid = mermaid.parse(el.textContent);
+      valid = mermaid.parse(source);
     } catch (err) {
       displayError(el, err);
     }
@@ -45,10 +47,17 @@ export async function renderMermaid() {
     }
 
     try {
-      mermaid.init(undefined, el, (id) => {
-        const svg = document.getElementById(id);
-        svg.classList.add('mermaid-chart');
-        svg.closest('pre').replaceWith(svg);
+      // can't use bindFunctions here because we can't cross the iframe boundary. This
+      // means js-based interactions won't work but they aren't intended to work either
+      mermaid.mermaidAPI.render('mermaid', source, (svgStr) => {
+        const heightStr = (svgStr.match(/height="(.+?)"/) || [])[1];
+        if (!heightStr) return displayError(el, new Error('Could not determine chart height'));
+        const iframe = document.createElement('iframe');
+        iframe.classList.add('markup-render');
+        iframe.sandbox = 'allow-scripts';
+        iframe.style.height = `${Math.ceil(parseFloat(heightStr))}px`;
+        iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svgStr}</body></html>`;
+        el.closest('pre').replaceWith(iframe);
       });
     } catch (err) {
       displayError(el, err);
diff --git a/web_src/less/_base.less b/web_src/less/_base.less
index 8e06d16405..55439d6cea 100644
--- a/web_src/less/_base.less
+++ b/web_src/less/_base.less
@@ -6,6 +6,7 @@
   /* other variables */
   --border-radius: .28571429rem;
   --opacity-disabled: .55;
+  --height-loading: 12rem;
   --color-primary: #4183c4;
   --color-primary-dark-1: #3876b3;
   --color-primary-dark-2: #31699f;
diff --git a/web_src/less/animations.less b/web_src/less/animations.less
index cdb10236fb..083e10089d 100644
--- a/web_src/less/animations.less
+++ b/web_src/less/animations.less
@@ -30,7 +30,7 @@
 
 .markup pre.is-loading,
 .editor-loading.is-loading {
-  height: 12rem;
+  height: var(--height-loading);
 }
 
 @keyframes fadein {
diff --git a/web_src/less/index.less b/web_src/less/index.less
index e95cb72eb0..805c68f2c4 100644
--- a/web_src/less/index.less
+++ b/web_src/less/index.less
@@ -10,7 +10,6 @@
 @import "./features/codeeditor.less";
 @import "./features/projects.less";
 @import "./markup/content.less";
-@import "./markup/mermaid.less";
 @import "./markup/codecopy.less";
 @import "./code/linebutton.less";
 
diff --git a/web_src/less/markup/content.less b/web_src/less/markup/content.less
index 71e98652c8..b8dafe3511 100644
--- a/web_src/less/markup/content.less
+++ b/web_src/less/markup/content.less
@@ -536,6 +536,14 @@
   }
 }
 
+.markup-render {
+  display: block;
+  border: none;
+  width: 100%;
+  height: var(--height-loading); // actual height is set in JS after loading
+  overflow: hidden;
+}
+
 .markup-block-error {
   margin-bottom: 0 !important;
   border-bottom-left-radius: 0 !important;
diff --git a/web_src/less/markup/mermaid.less b/web_src/less/markup/mermaid.less
deleted file mode 100644
index f68b577dec..0000000000
--- a/web_src/less/markup/mermaid.less
+++ /dev/null
@@ -1,13 +0,0 @@
-.mermaid-chart {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  padding: 1rem;
-  margin: 1rem auto;
-  height: auto;
-}
-
-/* mermaid's errorRenderer seems to unavoidably spew stuff into <body>, hide it */
-body > div[id*="mermaid-"] {
-  display: none !important;
-}
diff --git a/web_src/less/themes/theme-arc-green.less b/web_src/less/themes/theme-arc-green.less
index 5d107cef96..0b8d92b01f 100644
--- a/web_src/less/themes/theme-arc-green.less
+++ b/web_src/less/themes/theme-arc-green.less
@@ -455,10 +455,6 @@ img[src$="/img/matrix.svg"] {
   filter: invert(80%);
 }
 
-.mermaid-chart {
-  filter: invert(84%) hue-rotate(180deg);
-}
-
 .is-loading::after {
   border-color: #4a4c58 #4a4c58 #d7d7da #d7d7da;
 }