Răsfoiți Sursa

fix(app): markdown rendering with morphdom for better dom functions (#10373)

Rahul A Mistry 3 săptămâni în urmă
părinte
comite
399fec770f
3 a modificat fișierele cu 53 adăugiri și 6 ștergeri
  1. 3 0
      bun.lock
  2. 1 0
      packages/ui/package.json
  3. 49 6
      packages/ui/src/components/markdown.tsx

+ 3 - 0
bun.lock

@@ -423,6 +423,7 @@
         "marked": "catalog:",
         "marked-katex-extension": "5.1.6",
         "marked-shiki": "catalog:",
+        "morphdom": "2.7.8",
         "remeda": "catalog:",
         "shiki": "catalog:",
         "solid-js": "catalog:",
@@ -3102,6 +3103,8 @@
 
     "mkdirp": ["[email protected]", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
 
+    "morphdom": ["[email protected]", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
+
     "mrmime": ["[email protected]", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
 
     "ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

+ 1 - 0
packages/ui/package.json

@@ -56,6 +56,7 @@
     "marked": "catalog:",
     "marked-katex-extension": "5.1.6",
     "marked-shiki": "catalog:",
+    "morphdom": "2.7.8",
     "remeda": "catalog:",
     "shiki": "catalog:",
     "solid-js": "catalog:",

+ 49 - 6
packages/ui/src/components/markdown.tsx

@@ -1,6 +1,7 @@
 import { useMarked } from "../context/marked"
 import { useI18n } from "../context/i18n"
 import DOMPurify from "dompurify"
+import morphdom from "morphdom"
 import { checksum } from "@opencode-ai/util/encode"
 import { ComponentProps, createEffect, createResource, createSignal, onCleanup, splitProps } from "solid-js"
 import { isServer } from "solid-js/web"
@@ -194,18 +195,61 @@ export function Markdown(
     { initialValue: "" },
   )
 
+  let copySetupTimer: ReturnType<typeof setTimeout> | undefined
+  let copyCleanup: (() => void) | undefined
+
   createEffect(() => {
     const container = root()
     const content = html()
     if (!container) return
-    if (!content) return
     if (isServer) return
-    const cleanup = setupCodeCopy(container, {
-      copy: i18n.t("ui.message.copy"),
-      copied: i18n.t("ui.message.copied"),
+
+    if (!content) {
+      container.innerHTML = ""
+      return
+    }
+
+    const temp = document.createElement("div")
+    temp.innerHTML = content
+
+    morphdom(container, temp, {
+      childrenOnly: true,
+      onBeforeElUpdated: (fromEl, toEl) => {
+        if (fromEl.isEqualNode(toEl)) return false
+        if (fromEl.getAttribute("data-component") === "markdown-code") {
+          const fromPre = fromEl.querySelector("pre")
+          const toPre = toEl.querySelector("pre")
+          if (fromPre && toPre && !fromPre.isEqualNode(toPre)) {
+            morphdom(fromPre, toPre)
+          }
+          return false
+        }
+        return true
+      },
+      onBeforeNodeDiscarded: (node) => {
+        if (node instanceof Element) {
+          if (node.getAttribute("data-slot") === "markdown-copy-button") return false
+          if (node.getAttribute("data-component") === "markdown-code") return false
+        }
+        return true
+      },
     })
-    onCleanup(cleanup)
+
+    if (copySetupTimer) clearTimeout(copySetupTimer)
+    copySetupTimer = setTimeout(() => {
+      if (copyCleanup) copyCleanup()
+      copyCleanup = setupCodeCopy(container, {
+        copy: i18n.t("ui.message.copy"),
+        copied: i18n.t("ui.message.copied"),
+      })
+    }, 150)
   })
+
+  onCleanup(() => {
+    if (copySetupTimer) clearTimeout(copySetupTimer)
+    if (copyCleanup) copyCleanup()
+  })
+
   return (
     <div
       data-component="markdown"
@@ -213,7 +257,6 @@ export function Markdown(
         ...(local.classList ?? {}),
         [local.class ?? ""]: !!local.class,
       }}
-      innerHTML={html.latest}
       ref={setRoot}
       {...others}
     />