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