From 13b465fe7343fe418a3b0629beaf0972eff213ed Mon Sep 17 00:00:00 2001 From: MyPrototypeWhat <43230886+MyPrototypeWhat@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:23:18 +0800 Subject: [PATCH] fix: shadow markdown (#1871) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:[Bug]: 当模型回复的html代码没有正确在代码框中时,html代码内容会影响到UI界面 #1767 原因:解析html之后css会污染应用样式 解决:将markdown完全放入shadow dom中,shadow dom天然隔绝样式,即可解决 * feat: Conditionally hide thinking loader for paused messages * feat: Implement Shadow DOM for Markdown rendering * feat: Add StyleProvider to Shadow DOM Markdown rendering * fix: Refactor Markdown rendering with inline ShadowDOM component Modify ReactMarkdown component to use style component for ShadowDOM rendering instead of wrapping component, simplifying the rendering approach --------- Co-authored-by: lizhixuan --- .../components/MarkdownShadowDOMRenderer.tsx | 63 +++++++++++++++++++ .../src/pages/home/Markdown/Markdown.tsx | 10 +-- 2 files changed, 69 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/components/MarkdownShadowDOMRenderer.tsx diff --git a/src/renderer/src/components/MarkdownShadowDOMRenderer.tsx b/src/renderer/src/components/MarkdownShadowDOMRenderer.tsx new file mode 100644 index 00000000..9972d521 --- /dev/null +++ b/src/renderer/src/components/MarkdownShadowDOMRenderer.tsx @@ -0,0 +1,63 @@ +import { StyleProvider } from '@ant-design/cssinjs' +import React, { useEffect, useRef } from 'react' +import { createPortal } from 'react-dom' +import { StyleSheetManager } from 'styled-components' + +interface Props { + children: React.ReactNode +} + +const ShadowDOMRenderer: React.FC = ({ children }) => { + const hostRef = useRef(null) + const [shadowRoot, setShadowRoot] = React.useState(null) + + useEffect(() => { + const host = hostRef.current + if (!host) return + + // 创建 shadow root + const shadow = host.shadowRoot || host.attachShadow({ mode: 'open' }) + + // 获取原始样式表 + const markdownStyleSheet = Array.from(document.styleSheets).find((sheet) => { + try { + return Array.from(sheet.cssRules).some((rule: CSSRule) => { + return rule.cssText?.includes('.markdown') + }) + } catch { + return false + } + }) + + if (markdownStyleSheet) { + const style = document.createElement('style') + const cssRules = Array.from(markdownStyleSheet.cssRules) + .map((rule) => rule.cssText) + .join('\n') + + style.textContent = cssRules + shadow.appendChild(style) + } + + setShadowRoot(shadow) + }, []) + + if (!shadowRoot) { + return
+ } + + return ( +
+ {createPortal( + + + {children} + + , + shadowRoot + )} +
+ ) +} + +export default ShadowDOMRenderer diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx index 9bed5e9d..6248c802 100644 --- a/src/renderer/src/pages/home/Markdown/Markdown.tsx +++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx @@ -1,13 +1,14 @@ import 'katex/dist/katex.min.css' import 'katex/dist/contrib/copy-tex' +import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer' import { useSettings } from '@renderer/hooks/useSettings' -import { Message } from '@renderer/types' +import type { Message } from '@renderer/types' import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats' import { isEmpty } from 'lodash' -import { FC, useMemo } from 'react' +import { type FC, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import ReactMarkdown, { Components } from 'react-markdown' +import ReactMarkdown, { type Components } from 'react-markdown' import rehypeKatex from 'rehype-katex' // @ts-ignore next-line import rehypeMathjax from 'rehype-mathjax' @@ -52,11 +53,12 @@ const Markdown: FC = ({ message }) => { return (