From 87b803790c10dde62cbc5452014c619eced5b36b Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Tue, 11 Mar 2025 18:45:45 +0000
Subject: [PATCH] fix(mermaid): themechange detector + expand simplification

---
 quartz/components/scripts/mermaid.inline.ts  |  144 +++++++++++++++-------------
 quartz/plugins/transformers/ofm.ts           |   60 -----------
 quartz/components/styles/mermaid.inline.scss |   48 +-------
 3 files changed, 87 insertions(+), 165 deletions(-)

diff --git a/quartz/components/scripts/mermaid.inline.ts b/quartz/components/scripts/mermaid.inline.ts
index 36d384c..19ef24d 100644
--- a/quartz/components/scripts/mermaid.inline.ts
+++ b/quartz/components/scripts/mermaid.inline.ts
@@ -12,7 +12,8 @@
   private scale = 1
   private readonly MIN_SCALE = 0.5
   private readonly MAX_SCALE = 3
-  private readonly ZOOM_SENSITIVITY = 0.001
+
+  cleanups: (() => void)[] = []
 
   constructor(
     private container: HTMLElement,
@@ -20,19 +21,33 @@
   ) {
     this.setupEventListeners()
     this.setupNavigationControls()
+    this.resetTransform()
   }
 
   private setupEventListeners() {
     // Mouse drag events
-    this.container.addEventListener("mousedown", this.onMouseDown.bind(this))
-    document.addEventListener("mousemove", this.onMouseMove.bind(this))
-    document.addEventListener("mouseup", this.onMouseUp.bind(this))
+    const mouseDownHandler = this.onMouseDown.bind(this)
+    const mouseMoveHandler = this.onMouseMove.bind(this)
+    const mouseUpHandler = this.onMouseUp.bind(this)
+    const resizeHandler = this.resetTransform.bind(this)
 
-    // Wheel zoom events
-    this.container.addEventListener("wheel", this.onWheel.bind(this), { passive: false })
+    this.container.addEventListener("mousedown", mouseDownHandler)
+    document.addEventListener("mousemove", mouseMoveHandler)
+    document.addEventListener("mouseup", mouseUpHandler)
+    window.addEventListener("resize", resizeHandler)
 
-    // Reset on window resize
-    window.addEventListener("resize", this.resetTransform.bind(this))
+    this.cleanups.push(
+      () => this.container.removeEventListener("mousedown", mouseDownHandler),
+      () => document.removeEventListener("mousemove", mouseMoveHandler),
+      () => document.removeEventListener("mouseup", mouseUpHandler),
+      () => window.removeEventListener("resize", resizeHandler),
+    )
+  }
+
+  cleanup() {
+    for (const cleanup of this.cleanups) {
+      cleanup()
+    }
   }
 
   private setupNavigationControls() {
@@ -84,26 +99,6 @@
     this.container.style.cursor = "grab"
   }
 
-  private onWheel(e: WheelEvent) {
-    e.preventDefault()
-
-    const delta = -e.deltaY * this.ZOOM_SENSITIVITY
-    const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
-
-    // Calculate mouse position relative to content
-    const rect = this.content.getBoundingClientRect()
-    const mouseX = e.clientX - rect.left
-    const mouseY = e.clientY - rect.top
-
-    // Adjust pan to zoom around mouse position
-    const scaleDiff = newScale - this.scale
-    this.currentPan.x -= mouseX * scaleDiff
-    this.currentPan.y -= mouseY * scaleDiff
-
-    this.scale = newScale
-    this.updateTransform()
-  }
-
   private zoom(delta: number) {
     const newScale = Math.min(Math.max(this.scale + delta, this.MIN_SCALE), this.MAX_SCALE)
 
@@ -126,7 +121,11 @@
 
   private resetTransform() {
     this.scale = 1
-    this.currentPan = { x: 0, y: 0 }
+    const svg = this.content.querySelector("svg")!
+    this.currentPan = {
+      x: svg.getBoundingClientRect().width / 2,
+      y: svg.getBoundingClientRect().height / 2,
+    }
     this.updateTransform()
   }
 }
@@ -149,38 +148,59 @@
   const nodes = center.querySelectorAll("code.mermaid") as NodeListOf<HTMLElement>
   if (nodes.length === 0) return
 
-  const computedStyleMap = cssVars.reduce(
-    (acc, key) => {
-      acc[key] = getComputedStyle(document.documentElement).getPropertyValue(key)
-      return acc
-    },
-    {} as Record<(typeof cssVars)[number], string>,
-  )
-
   mermaidImport ||= await import(
     // @ts-ignore
     "https://cdnjs.cloudflare.com/ajax/libs/mermaid/11.4.0/mermaid.esm.min.mjs"
   )
   const mermaid = mermaidImport.default
 
-  const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
-  mermaid.initialize({
-    startOnLoad: false,
-    securityLevel: "loose",
-    theme: darkMode ? "dark" : "base",
-    themeVariables: {
-      fontFamily: computedStyleMap["--codeFont"],
-      primaryColor: computedStyleMap["--light"],
-      primaryTextColor: computedStyleMap["--darkgray"],
-      primaryBorderColor: computedStyleMap["--tertiary"],
-      lineColor: computedStyleMap["--darkgray"],
-      secondaryColor: computedStyleMap["--secondary"],
-      tertiaryColor: computedStyleMap["--tertiary"],
-      clusterBkg: computedStyleMap["--light"],
-      edgeLabelBackground: computedStyleMap["--highlight"],
-    },
-  })
-  await mermaid.run({ nodes })
+  const textMapping: WeakMap<HTMLElement, string> = new WeakMap()
+  for (const node of nodes) {
+    textMapping.set(node, node.innerText)
+  }
+
+  async function renderMermaid() {
+    // de-init any other diagrams
+    for (const node of nodes) {
+      node.removeAttribute("data-processed")
+      const oldText = textMapping.get(node)
+      if (oldText) {
+        node.innerHTML = oldText
+      }
+    }
+
+    const computedStyleMap = cssVars.reduce(
+      (acc, key) => {
+        acc[key] = window.getComputedStyle(document.documentElement).getPropertyValue(key)
+        return acc
+      },
+      {} as Record<(typeof cssVars)[number], string>,
+    )
+
+    const darkMode = document.documentElement.getAttribute("saved-theme") === "dark"
+    mermaid.initialize({
+      startOnLoad: false,
+      securityLevel: "loose",
+      theme: darkMode ? "dark" : "base",
+      themeVariables: {
+        fontFamily: computedStyleMap["--codeFont"],
+        primaryColor: computedStyleMap["--light"],
+        primaryTextColor: computedStyleMap["--darkgray"],
+        primaryBorderColor: computedStyleMap["--tertiary"],
+        lineColor: computedStyleMap["--darkgray"],
+        secondaryColor: computedStyleMap["--secondary"],
+        tertiaryColor: computedStyleMap["--tertiary"],
+        clusterBkg: computedStyleMap["--light"],
+        edgeLabelBackground: computedStyleMap["--highlight"],
+      },
+    })
+
+    await mermaid.run({ nodes })
+  }
+
+  await renderMermaid()
+  document.addEventListener("themechange", renderMermaid)
+  window.addCleanup(() => document.removeEventListener("themechange", renderMermaid))
 
   for (let i = 0; i < nodes.length; i++) {
     const codeBlock = nodes[i] as HTMLElement
@@ -203,7 +223,6 @@
     if (!popupContainer) return
 
     let panZoom: DiagramPanZoom | null = null
-
     function showMermaid() {
       const container = popupContainer.querySelector("#mermaid-space") as HTMLElement
       const content = popupContainer.querySelector(".mermaid-content") as HTMLElement
@@ -224,24 +243,15 @@
 
     function hideMermaid() {
       popupContainer.classList.remove("active")
+      panZoom?.cleanup()
       panZoom = null
     }
 
-    function handleEscape(e: any) {
-      if (e.key === "Escape") {
-        hideMermaid()
-      }
-    }
-
-    const closeBtn = popupContainer.querySelector(".close-button") as HTMLButtonElement
-
-    closeBtn.addEventListener("click", hideMermaid)
     expandBtn.addEventListener("click", showMermaid)
     registerEscapeHandler(popupContainer, hideMermaid)
-    document.addEventListener("keydown", handleEscape)
 
     window.addCleanup(() => {
-      closeBtn.removeEventListener("click", hideMermaid)
+      panZoom?.cleanup()
       expandBtn.removeEventListener("click", showMermaid)
     })
   }
diff --git a/quartz/components/styles/mermaid.inline.scss b/quartz/components/styles/mermaid.inline.scss
index 79a1c84..f25448d 100644
--- a/quartz/components/styles/mermaid.inline.scss
+++ b/quartz/components/styles/mermaid.inline.scss
@@ -53,46 +53,16 @@
   }
 
   & > #mermaid-space {
-    display: grid;
-    width: 90%;
-    height: 90vh;
-    margin: 5vh auto;
-    background: var(--light);
-    box-shadow:
-      0 14px 50px rgba(27, 33, 48, 0.12),
-      0 10px 30px rgba(27, 33, 48, 0.16);
+    border: 1px solid var(--lightgray);
+    background-color: var(--light);
+    border-radius: 5px;
+    position: fixed;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    height: 80vh;
+    width: 80vw;
     overflow: hidden;
-    position: relative;
-
-    & > .mermaid-header {
-      display: flex;
-      justify-content: flex-end;
-      padding: 1rem;
-      border-bottom: 1px solid var(--lightgray);
-      background: var(--light);
-      z-index: 2;
-      max-height: fit-content;
-
-      & > .close-button {
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        width: 32px;
-        height: 32px;
-        padding: 0;
-        background: transparent;
-        border: none;
-        border-radius: 4px;
-        color: var(--darkgray);
-        cursor: pointer;
-        transition: all 0.2s ease;
-
-        &:hover {
-          background: var(--lightgray);
-          color: var(--dark);
-        }
-      }
-    }
 
     & > .mermaid-content {
       padding: 2rem;
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index 12baf2f..1f4873d 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -675,7 +675,6 @@
                     properties: {
                       className: ["expand-button"],
                       "aria-label": "Expand mermaid diagram",
-                      "aria-hidden": "true",
                       "data-view-component": true,
                     },
                     children: [
@@ -706,7 +705,7 @@
                   {
                     type: "element",
                     tagName: "div",
-                    properties: { id: "mermaid-container" },
+                    properties: { id: "mermaid-container", role: "dialog" },
                     children: [
                       {
                         type: "element",
@@ -716,63 +715,6 @@
                           {
                             type: "element",
                             tagName: "div",
-                            properties: { className: ["mermaid-header"] },
-                            children: [
-                              {
-                                type: "element",
-                                tagName: "button",
-                                properties: {
-                                  className: ["close-button"],
-                                  "aria-label": "close button",
-                                },
-                                children: [
-                                  {
-                                    type: "element",
-                                    tagName: "svg",
-                                    properties: {
-                                      "aria-hidden": "true",
-                                      xmlns: "http://www.w3.org/2000/svg",
-                                      width: 24,
-                                      height: 24,
-                                      viewBox: "0 0 24 24",
-                                      fill: "none",
-                                      stroke: "currentColor",
-                                      "stroke-width": "2",
-                                      "stroke-linecap": "round",
-                                      "stroke-linejoin": "round",
-                                    },
-                                    children: [
-                                      {
-                                        type: "element",
-                                        tagName: "line",
-                                        properties: {
-                                          x1: 18,
-                                          y1: 6,
-                                          x2: 6,
-                                          y2: 18,
-                                        },
-                                        children: [],
-                                      },
-                                      {
-                                        type: "element",
-                                        tagName: "line",
-                                        properties: {
-                                          x1: 6,
-                                          y1: 6,
-                                          x2: 18,
-                                          y2: 18,
-                                        },
-                                        children: [],
-                                      },
-                                    ],
-                                  },
-                                ],
-                              },
-                            ],
-                          },
-                          {
-                            type: "element",
-                            tagName: "div",
                             properties: { className: ["mermaid-content"] },
                             children: [],
                           },

--
Gitblit v1.10.0