mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-24 10:40:07 +08:00
refactor(Mermaid): render mermaid in shadow dom (#9187)
* refactor(Mermaid): render mermaid in shadow dom
* refactor: pass style overrides to renderSvgInShadowHost
* refactor(MermaidPreview): separate measurement from rendering
* refactor: rename hostCss to customCss
* refactor: use custom properties in shadow host
* test: update snapshots
* fix: remove svg max-width
* refactor: add viewBox to svg (experimental)
* Revert "refactor: add viewBox to svg (experimental)"
This reverts commit 8a265fa8a4.
This commit is contained in:
parent
535dcf4778
commit
ded941b7b9
@ -1,9 +1,9 @@
|
||||
import { AsyncInitializer } from '@renderer/utils/asyncInitializer'
|
||||
import React, { memo, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useDebouncedRender } from './hooks/useDebouncedRender'
|
||||
import ImagePreviewLayout from './ImagePreviewLayout'
|
||||
import { ShadowWhiteContainer } from './styles'
|
||||
import { BasicPreviewHandles, BasicPreviewProps } from './types'
|
||||
import { renderSvgInShadowHost } from './utils'
|
||||
|
||||
@ -13,8 +13,10 @@ const vizInitializer = new AsyncInitializer(async () => {
|
||||
return await module.instance()
|
||||
})
|
||||
|
||||
/** 预览 Graphviz 图表
|
||||
* 使用 usePreviewRenderer hook 大幅简化组件逻辑
|
||||
/**
|
||||
* 预览 Graphviz 图表
|
||||
* - 使用 useDebouncedRender 改善体验
|
||||
* - 使用 shadow dom 渲染 SVG
|
||||
*/
|
||||
const GraphvizPreview = ({
|
||||
children,
|
||||
@ -41,16 +43,9 @@ const GraphvizPreview = ({
|
||||
ref={ref}
|
||||
imageRef={containerRef}
|
||||
source="graphviz">
|
||||
<StyledGraphviz ref={containerRef} className="graphviz special-preview" />
|
||||
<ShadowWhiteContainer ref={containerRef} className="graphviz special-preview" />
|
||||
</ImagePreviewLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledGraphviz = styled.div`
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export default memo(GraphvizPreview)
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { useDebouncedRender } from './hooks/useDebouncedRender'
|
||||
import ImagePreviewLayout from './ImagePreviewLayout'
|
||||
import { ShadowTransparentContainer } from './styles'
|
||||
import { BasicPreviewHandles, BasicPreviewProps } from './types'
|
||||
import { renderSvgInShadowHost } from './utils'
|
||||
|
||||
/** 预览 Mermaid 图表
|
||||
* 使用 usePreviewRenderer hook 重构,同时保留必要的可见性检测逻辑
|
||||
* FIXME: 等将来 mermaid-js 修复可见性问题后可以进一步简化
|
||||
/**
|
||||
* 预览 Mermaid 图表
|
||||
* - 使用 useDebouncedRender 改善体验
|
||||
* - 使用 shadow dom 渲染 SVG
|
||||
*/
|
||||
const MermaidPreview = ({
|
||||
children,
|
||||
@ -20,17 +22,39 @@ const MermaidPreview = ({
|
||||
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
||||
const [isVisible, setIsVisible] = useState(true)
|
||||
|
||||
// 定义渲染函数
|
||||
/**
|
||||
* 定义渲染函数,在临时容器中测量,在 shadow dom 中渲染。
|
||||
* 如果这个方案有问题,可以回退到 innerHTML。
|
||||
*/
|
||||
const renderMermaid = useCallback(
|
||||
async (content: string, container: HTMLDivElement) => {
|
||||
// 验证语法,提前抛出异常
|
||||
await mermaid.parse(content)
|
||||
|
||||
const { svg } = await mermaid.render(diagramId, content, container)
|
||||
// 获取容器宽度
|
||||
const { width } = container.getBoundingClientRect()
|
||||
if (width === 0) return
|
||||
|
||||
// 避免不可见时产生 undefined 和 NaN
|
||||
const fixedSvg = svg.replace(/translate\(undefined,\s*NaN\)/g, 'translate(0, 0)')
|
||||
container.innerHTML = fixedSvg
|
||||
// 创建临时的 div 用于 mermaid 测量
|
||||
const measureEl = document.createElement('div')
|
||||
measureEl.style.position = 'absolute'
|
||||
measureEl.style.left = '-9999px'
|
||||
measureEl.style.top = '-9999px'
|
||||
measureEl.style.width = `${width}px`
|
||||
document.body.appendChild(measureEl)
|
||||
|
||||
try {
|
||||
const { svg } = await mermaid.render(diagramId, content, measureEl)
|
||||
|
||||
// 避免不可见时产生 undefined 和 NaN
|
||||
const fixedSvg = svg.replace(/translate\(undefined,\s*NaN\)/g, 'translate(0, 0)')
|
||||
|
||||
// 有问题可以回退到 innerHTML
|
||||
renderSvgInShadowHost(fixedSvg, container)
|
||||
// container.innerHTML = fixedSvg
|
||||
} finally {
|
||||
document.body.removeChild(measureEl)
|
||||
}
|
||||
},
|
||||
[diagramId, mermaid]
|
||||
)
|
||||
@ -63,7 +87,7 @@ const MermaidPreview = ({
|
||||
const element = containerRef.current
|
||||
if (!element) return
|
||||
|
||||
const currentlyVisible = element.offsetParent !== null
|
||||
const currentlyVisible = element.offsetParent !== null && element.offsetWidth > 0 && element.offsetHeight > 0
|
||||
setIsVisible(currentlyVisible)
|
||||
}
|
||||
|
||||
@ -105,16 +129,9 @@ const MermaidPreview = ({
|
||||
ref={ref}
|
||||
imageRef={containerRef}
|
||||
source="mermaid">
|
||||
<StyledMermaid ref={containerRef} className="mermaid special-preview" />
|
||||
<ShadowTransparentContainer ref={containerRef} className="mermaid special-preview" />
|
||||
</ImagePreviewLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledMermaid = styled.div`
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export default memo(MermaidPreview)
|
||||
|
||||
@ -4,6 +4,7 @@ import React, { memo, useCallback, useEffect } from 'react'
|
||||
|
||||
import { useDebouncedRender } from './hooks/useDebouncedRender'
|
||||
import ImagePreviewLayout from './ImagePreviewLayout'
|
||||
import { ShadowWhiteContainer } from './styles'
|
||||
import { BasicPreviewHandles, BasicPreviewProps } from './types'
|
||||
import { renderSvgInShadowHost } from './utils'
|
||||
|
||||
@ -128,7 +129,7 @@ const PlantUmlPreview = ({
|
||||
ref={ref}
|
||||
imageRef={containerRef}
|
||||
source="plantuml">
|
||||
<div ref={containerRef} className="plantuml-preview special-preview" />
|
||||
<ShadowWhiteContainer ref={containerRef} className="plantuml-preview special-preview" />
|
||||
</ImagePreviewLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { memo, useCallback } from 'react'
|
||||
|
||||
import { useDebouncedRender } from './hooks/useDebouncedRender'
|
||||
import ImagePreviewLayout from './ImagePreviewLayout'
|
||||
import { ShadowWhiteContainer } from './styles'
|
||||
import { BasicPreviewHandles } from './types'
|
||||
import { renderSvgInShadowHost } from './utils'
|
||||
|
||||
@ -34,7 +35,7 @@ const SvgPreview = ({ children, enableToolbar = false, className, ref }: SvgPrev
|
||||
ref={ref}
|
||||
imageRef={containerRef}
|
||||
source="svg">
|
||||
<div ref={containerRef} className={className ?? 'svg-preview special-preview'}></div>
|
||||
<ShadowWhiteContainer ref={containerRef} className={className ?? 'svg-preview special-preview'} />
|
||||
</ImagePreviewLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
|
||||
exports[`GraphvizPreview > basic rendering > should match snapshot 1`] = `
|
||||
.c0 {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--shadow-host-background-color: white;
|
||||
--shadow-host-border: 0.5px solid var(--color-code-background);
|
||||
--shadow-host-border-radius: 8px;
|
||||
}
|
||||
|
||||
<div>
|
||||
|
||||
@ -2,10 +2,9 @@
|
||||
|
||||
exports[`MermaidPreview > basic rendering > should match snapshot 1`] = `
|
||||
.c0 {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
--shadow-host-background-color: transparent;
|
||||
--shadow-host-border: unset;
|
||||
--shadow-host-border-radius: unset;
|
||||
}
|
||||
|
||||
<div>
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`PlantUmlPreview > basic rendering > should match snapshot 1`] = `
|
||||
.c0 {
|
||||
--shadow-host-background-color: white;
|
||||
--shadow-host-border: 0.5px solid var(--color-code-background);
|
||||
--shadow-host-border-radius: 8px;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
data-source="plantuml"
|
||||
@ -15,7 +21,7 @@ exports[`PlantUmlPreview > basic rendering > should match snapshot 1`] = `
|
||||
data-testid="preview-content"
|
||||
>
|
||||
<div
|
||||
class="plantuml-preview special-preview"
|
||||
class="c0 plantuml-preview special-preview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`SvgPreview > basic rendering > should match snapshot 1`] = `
|
||||
.c0 {
|
||||
--shadow-host-background-color: white;
|
||||
--shadow-host-border: 0.5px solid var(--color-code-background);
|
||||
--shadow-host-border-radius: 8px;
|
||||
}
|
||||
|
||||
<div>
|
||||
<div
|
||||
data-source="svg"
|
||||
@ -15,7 +21,7 @@ exports[`SvgPreview > basic rendering > should match snapshot 1`] = `
|
||||
data-testid="preview-content"
|
||||
>
|
||||
<div
|
||||
class="svg-preview special-preview"
|
||||
class="c0 svg-preview special-preview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,3 +33,15 @@ export const PreviewContainer = styled(Flex).attrs({ role: 'alert' })`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const ShadowWhiteContainer = styled.div`
|
||||
--shadow-host-background-color: white;
|
||||
--shadow-host-border: 0.5px solid var(--color-code-background);
|
||||
--shadow-host-border-radius: 8px;
|
||||
`
|
||||
|
||||
export const ShadowTransparentContainer = styled.div`
|
||||
--shadow-host-background-color: transparent;
|
||||
--shadow-host-border: unset;
|
||||
--shadow-host-border-radius: unset;
|
||||
`
|
||||
|
||||
@ -28,11 +28,15 @@ export function renderSvgInShadowHost(svgContent: string, hostElement: HTMLEleme
|
||||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
:host {
|
||||
--shadow-host-background-color: white;
|
||||
--shadow-host-border: 0.5px solid var(--color-code-background);
|
||||
--shadow-host-border-radius: 8px;
|
||||
|
||||
background-color: var(--shadow-host-background-color);
|
||||
border: var(--shadow-host-border);
|
||||
border-radius: var(--shadow-host-border-radius);
|
||||
padding: 1em;
|
||||
background-color: white;
|
||||
overflow: hidden; /* Prevent scrollbars, as scaling is now handled */
|
||||
border: 0.5px solid var(--color-code-background);
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user