mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
feat: codeblock dot language (#6783)
* feat(CodeBlock): support dot language in code block - render DOT using @viz-js/viz - highlight DOT using @viz-js/lang-dot (CodeEditor only) - extract a special view map, update file structure - extract and reuse the PreviewError component across special views - update dependencies, fix peer dependencies * chore: prepare for merge
This commit is contained in:
parent
db642f0837
commit
2a72f391b7
@ -92,6 +92,7 @@
|
|||||||
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
|
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
"@cherrystudio/embedjs-ollama": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-openai": "^0.1.31",
|
"@cherrystudio/embedjs-openai": "^0.1.31",
|
||||||
|
"@codemirror/view": "^6.0.0",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
"@electron-toolkit/eslint-config-ts": "^3.0.0",
|
||||||
"@electron-toolkit/preload": "^3.0.0",
|
"@electron-toolkit/preload": "^3.0.0",
|
||||||
@ -141,6 +142,8 @@
|
|||||||
"@vitest/coverage-v8": "^3.1.4",
|
"@vitest/coverage-v8": "^3.1.4",
|
||||||
"@vitest/ui": "^3.1.4",
|
"@vitest/ui": "^3.1.4",
|
||||||
"@vitest/web-worker": "^3.1.4",
|
"@vitest/web-worker": "^3.1.4",
|
||||||
|
"@viz-js/lang-dot": "^1.0.5",
|
||||||
|
"@viz-js/viz": "^3.14.0",
|
||||||
"@xyflow/react": "^12.4.4",
|
"@xyflow/react": "^12.4.4",
|
||||||
"antd": "patch:antd@npm%3A5.24.7#~/.yarn/patches/antd-npm-5.24.7-356a553ae5.patch",
|
"antd": "patch:antd@npm%3A5.24.7#~/.yarn/patches/antd-npm-5.24.7-356a553ae5.patch",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
@ -225,6 +228,7 @@
|
|||||||
"tiny-pinyin": "^1.3.2",
|
"tiny-pinyin": "^1.3.2",
|
||||||
"tokenx": "^1.1.0",
|
"tokenx": "^1.1.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
|
"unified": "^11.0.5",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vite": "6.2.6",
|
"vite": "6.2.6",
|
||||||
"vitest": "^3.1.4",
|
"vitest": "^3.1.4",
|
||||||
|
|||||||
@ -129,9 +129,7 @@
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
||||||
background-color: var(--color-background-mute);
|
background-color: var(--color-background-mute);
|
||||||
&:has(.mermaid),
|
&:has(.special-preview) {
|
||||||
&:has(.plantuml-preview),
|
|
||||||
&:has(.svg-preview) {
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
&:not(pre pre) {
|
&:not(pre pre) {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { CodeTool, TOOL_SPECS, useCodeTool } from '@renderer/components/CodeToolbar'
|
import { TOOL_SPECS, useCodeTool } from '@renderer/components/CodeToolbar'
|
||||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||||
import { useCodeHighlight } from '@renderer/hooks/useCodeHighlight'
|
import { useCodeHighlight } from '@renderer/hooks/useCodeHighlight'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
@ -12,10 +12,10 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { ThemedToken } from 'shiki/core'
|
import { ThemedToken } from 'shiki/core'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface CodePreviewProps {
|
import { BasicPreviewProps } from './types'
|
||||||
children: string
|
|
||||||
|
interface CodePreviewProps extends BasicPreviewProps {
|
||||||
language: string
|
language: string
|
||||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_COLLAPSE_HEIGHT = 350
|
const MAX_COLLAPSE_HEIGHT = 350
|
||||||
|
|||||||
102
src/renderer/src/components/CodeBlockView/GraphvizPreview.tsx
Normal file
102
src/renderer/src/components/CodeBlockView/GraphvizPreview.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||||
|
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||||
|
import { AsyncInitializer } from '@renderer/utils/asyncInitializer'
|
||||||
|
import { Flex, Spin } from 'antd'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
|
import React, { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import PreviewError from './PreviewError'
|
||||||
|
import { BasicPreviewProps } from './types'
|
||||||
|
|
||||||
|
// 管理 viz 实例
|
||||||
|
const vizInitializer = new AsyncInitializer(async () => {
|
||||||
|
const module = await import('@viz-js/viz')
|
||||||
|
return await module.instance()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 预览 Graphviz 图表
|
||||||
|
* 通过防抖渲染提供比较统一的体验,减少闪烁。
|
||||||
|
*/
|
||||||
|
const GraphvizPreview: React.FC<BasicPreviewProps> = ({ children, setTools }) => {
|
||||||
|
const graphvizRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
// 使用通用图像工具
|
||||||
|
const { handleZoom, handleCopyImage, handleDownload } = usePreviewToolHandlers(graphvizRef, {
|
||||||
|
imgSelector: 'svg',
|
||||||
|
prefix: 'graphviz',
|
||||||
|
enableWheelZoom: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用工具栏
|
||||||
|
usePreviewTools({
|
||||||
|
setTools,
|
||||||
|
handleZoom,
|
||||||
|
handleCopyImage,
|
||||||
|
handleDownload
|
||||||
|
})
|
||||||
|
|
||||||
|
// 实际的渲染函数
|
||||||
|
const renderGraphviz = useCallback(async (content: string) => {
|
||||||
|
if (!content || !graphvizRef.current) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const viz = await vizInitializer.get()
|
||||||
|
const svgElement = viz.renderSVGElement(content)
|
||||||
|
|
||||||
|
// 清空容器并添加新的 SVG
|
||||||
|
graphvizRef.current.innerHTML = ''
|
||||||
|
graphvizRef.current.appendChild(svgElement)
|
||||||
|
|
||||||
|
// 渲染成功,清除错误记录
|
||||||
|
setError(null)
|
||||||
|
} catch (error) {
|
||||||
|
setError((error as Error).message || 'DOT syntax error or rendering failed')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// debounce 渲染
|
||||||
|
const debouncedRender = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce((content: string) => {
|
||||||
|
startTransition(() => renderGraphviz(content))
|
||||||
|
}, 300),
|
||||||
|
[renderGraphviz]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 触发渲染
|
||||||
|
useEffect(() => {
|
||||||
|
if (children) {
|
||||||
|
setIsLoading(true)
|
||||||
|
debouncedRender(children)
|
||||||
|
} else {
|
||||||
|
debouncedRender.cancel()
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedRender.cancel()
|
||||||
|
}
|
||||||
|
}, [children, debouncedRender])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
|
||||||
|
<Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}>
|
||||||
|
{error && <PreviewError>{error}</PreviewError>}
|
||||||
|
<StyledGraphviz ref={graphvizRef} className="graphviz special-preview" />
|
||||||
|
</Flex>
|
||||||
|
</Spin>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledGraphviz = styled.div`
|
||||||
|
overflow: auto;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(GraphvizPreview)
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { nanoid } from '@reduxjs/toolkit'
|
import { nanoid } from '@reduxjs/toolkit'
|
||||||
import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||||
import { Flex, Spin } from 'antd'
|
import { Flex, Spin } from 'antd'
|
||||||
@ -7,16 +7,14 @@ import { debounce } from 'lodash'
|
|||||||
import React, { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { memo, startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
import PreviewError from './PreviewError'
|
||||||
children: string
|
import { BasicPreviewProps } from './types'
|
||||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 预览 Mermaid 图表
|
/** 预览 Mermaid 图表
|
||||||
* 通过防抖渲染提供比较统一的体验,减少闪烁。
|
* 通过防抖渲染提供比较统一的体验,减少闪烁。
|
||||||
* FIXME: 等将来容易判断代码块结束位置时再重构。
|
* FIXME: 等将来容易判断代码块结束位置时再重构。
|
||||||
*/
|
*/
|
||||||
const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
const MermaidPreview: React.FC<BasicPreviewProps> = ({ children, setTools }) => {
|
||||||
const { mermaid, isLoading: isLoadingMermaid, error: mermaidError } = useMermaid()
|
const { mermaid, isLoading: isLoadingMermaid, error: mermaidError } = useMermaid()
|
||||||
const mermaidRef = useRef<HTMLDivElement>(null)
|
const mermaidRef = useRef<HTMLDivElement>(null)
|
||||||
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
const diagramId = useRef<string>(`mermaid-${nanoid(6)}`).current
|
||||||
@ -143,7 +141,7 @@ const MermaidPreview: React.FC<Props> = ({ children, setTools }) => {
|
|||||||
return (
|
return (
|
||||||
<Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
|
<Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
|
||||||
<Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}>
|
<Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}>
|
||||||
{(mermaidError || error) && <StyledError>{mermaidError || error}</StyledError>}
|
{(mermaidError || error) && <PreviewError>{mermaidError || error}</PreviewError>}
|
||||||
<StyledMermaid ref={mermaidRef} className="mermaid" />
|
<StyledMermaid ref={mermaidRef} className="mermaid" />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Spin>
|
</Spin>
|
||||||
@ -154,14 +152,4 @@ const StyledMermaid = styled.div`
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledError = styled.div`
|
|
||||||
overflow: auto;
|
|
||||||
padding: 16px;
|
|
||||||
color: #ff4d4f;
|
|
||||||
border: 1px solid #ff4d4f;
|
|
||||||
border-radius: 4px;
|
|
||||||
word-wrap: break-word;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default memo(MermaidPreview)
|
export default memo(MermaidPreview)
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import { LoadingOutlined } from '@ant-design/icons'
|
import { LoadingOutlined } from '@ant-design/icons'
|
||||||
import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||||
import { Spin } from 'antd'
|
import { Spin } from 'antd'
|
||||||
import pako from 'pako'
|
import pako from 'pako'
|
||||||
import React, { memo, useCallback, useRef, useState } from 'react'
|
import React, { memo, useCallback, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { BasicPreviewProps } from './types'
|
||||||
|
|
||||||
const PlantUMLServer = 'https://www.plantuml.com/plantuml'
|
const PlantUMLServer = 'https://www.plantuml.com/plantuml'
|
||||||
function encode64(data: Uint8Array) {
|
function encode64(data: Uint8Array) {
|
||||||
let r = ''
|
let r = ''
|
||||||
@ -132,12 +134,7 @@ const PlantUMLServerImage: React.FC<PlantUMLServerImageProps> = ({ format, diagr
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PlantUMLProps {
|
const PlantUmlPreview: React.FC<BasicPreviewProps> = ({ children, setTools }) => {
|
||||||
children: string
|
|
||||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PlantUmlPreview: React.FC<PlantUMLProps> = ({ children, setTools }) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@ -174,7 +171,7 @@ const PlantUmlPreview: React.FC<PlantUMLProps> = ({ children, setTools }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
<PlantUMLServerImage format="svg" diagram={children} className="plantuml-preview" />
|
<PlantUMLServerImage format="svg" diagram={children} className="plantuml-preview special-preview" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/renderer/src/components/CodeBlockView/PreviewError.tsx
Normal file
14
src/renderer/src/components/CodeBlockView/PreviewError.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
import { styled } from 'styled-components'
|
||||||
|
|
||||||
|
const PreviewError = styled.div`
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
border: 1px solid #ff4d4f;
|
||||||
|
border-radius: 4px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(PreviewError)
|
||||||
@ -1,15 +1,12 @@
|
|||||||
import { CodeTool, usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||||
import { memo, useEffect, useRef } from 'react'
|
import { memo, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
interface Props {
|
import { BasicPreviewProps } from './types'
|
||||||
children: string
|
|
||||||
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用 Shadow DOM 渲染 SVG
|
* 使用 Shadow DOM 渲染 SVG
|
||||||
*/
|
*/
|
||||||
const SvgPreview: React.FC<Props> = ({ children, setTools }) => {
|
const SvgPreview: React.FC<BasicPreviewProps> = ({ children, setTools }) => {
|
||||||
const svgContainerRef = useRef<HTMLDivElement>(null)
|
const svgContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -58,7 +55,7 @@ const SvgPreview: React.FC<Props> = ({ children, setTools }) => {
|
|||||||
handleDownload
|
handleDownload
|
||||||
})
|
})
|
||||||
|
|
||||||
return <div ref={svgContainerRef} className="svg-preview" />
|
return <div ref={svgContainerRef} className="svg-preview special-preview" />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(SvgPreview)
|
export default memo(SvgPreview)
|
||||||
|
|||||||
20
src/renderer/src/components/CodeBlockView/constants.ts
Normal file
20
src/renderer/src/components/CodeBlockView/constants.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import GraphvizPreview from './GraphvizPreview'
|
||||||
|
import MermaidPreview from './MermaidPreview'
|
||||||
|
import PlantUmlPreview from './PlantUmlPreview'
|
||||||
|
import SvgPreview from './SvgPreview'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 特殊视图语言列表
|
||||||
|
*/
|
||||||
|
export const SPECIAL_VIEWS = ['mermaid', 'plantuml', 'svg', 'dot', 'graphviz']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 特殊视图组件映射表
|
||||||
|
*/
|
||||||
|
export const SPECIAL_VIEW_COMPONENTS = {
|
||||||
|
mermaid: MermaidPreview,
|
||||||
|
plantuml: PlantUmlPreview,
|
||||||
|
svg: SvgPreview,
|
||||||
|
dot: GraphvizPreview,
|
||||||
|
graphviz: GraphvizPreview
|
||||||
|
} as const
|
||||||
2
src/renderer/src/components/CodeBlockView/index.ts
Normal file
2
src/renderer/src/components/CodeBlockView/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './types'
|
||||||
|
export * from './view'
|
||||||
14
src/renderer/src/components/CodeBlockView/types.ts
Normal file
14
src/renderer/src/components/CodeBlockView/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { CodeTool } from '@renderer/components/CodeToolbar'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览组件的基本 props
|
||||||
|
*/
|
||||||
|
export interface BasicPreviewProps {
|
||||||
|
children: string
|
||||||
|
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视图模式
|
||||||
|
*/
|
||||||
|
export type ViewMode = 'source' | 'special' | 'split'
|
||||||
@ -12,13 +12,10 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import CodePreview from './CodePreview'
|
import CodePreview from './CodePreview'
|
||||||
|
import { SPECIAL_VIEW_COMPONENTS, SPECIAL_VIEWS } from './constants'
|
||||||
import HtmlArtifactsCard from './HtmlArtifactsCard'
|
import HtmlArtifactsCard from './HtmlArtifactsCard'
|
||||||
import MermaidPreview from './MermaidPreview'
|
|
||||||
import PlantUmlPreview from './PlantUmlPreview'
|
|
||||||
import StatusBar from './StatusBar'
|
import StatusBar from './StatusBar'
|
||||||
import SvgPreview from './SvgPreview'
|
import { ViewMode } from './types'
|
||||||
|
|
||||||
type ViewMode = 'source' | 'special' | 'split'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: string
|
children: string
|
||||||
@ -42,7 +39,7 @@ interface Props {
|
|||||||
* - quick 工具
|
* - quick 工具
|
||||||
* - core 工具
|
* - core 工具
|
||||||
*/
|
*/
|
||||||
const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { codeEditor, codeExecution } = useSettings()
|
const { codeEditor, codeExecution } = useSettings()
|
||||||
|
|
||||||
@ -57,7 +54,7 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
|||||||
return codeExecution.enabled && language === 'python'
|
return codeExecution.enabled && language === 'python'
|
||||||
}, [codeExecution.enabled, language])
|
}, [codeExecution.enabled, language])
|
||||||
|
|
||||||
const hasSpecialView = useMemo(() => ['mermaid', 'plantuml', 'svg'].includes(language), [language])
|
const hasSpecialView = useMemo(() => SPECIAL_VIEWS.includes(language), [language])
|
||||||
|
|
||||||
const isInSpecialView = useMemo(() => {
|
const isInSpecialView = useMemo(() => {
|
||||||
return hasSpecialView && viewMode === 'special'
|
return hasSpecialView && viewMode === 'special'
|
||||||
@ -201,14 +198,16 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
|||||||
|
|
||||||
// 特殊视图组件映射
|
// 特殊视图组件映射
|
||||||
const specialView = useMemo(() => {
|
const specialView = useMemo(() => {
|
||||||
if (language === 'mermaid') {
|
const SpecialView = SPECIAL_VIEW_COMPONENTS[language as keyof typeof SPECIAL_VIEW_COMPONENTS]
|
||||||
return <MermaidPreview setTools={setTools}>{children}</MermaidPreview>
|
|
||||||
} else if (language === 'plantuml' && isValidPlantUML(children)) {
|
if (!SpecialView) return null
|
||||||
return <PlantUmlPreview setTools={setTools}>{children}</PlantUmlPreview>
|
|
||||||
} else if (language === 'svg') {
|
// PlantUML 语法验证
|
||||||
return <SvgPreview setTools={setTools}>{children}</SvgPreview>
|
if (language === 'plantuml' && !isValidPlantUML(children)) {
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SpecialView setTools={setTools}>{children}</SpecialView>
|
||||||
}, [children, language])
|
}, [children, language])
|
||||||
|
|
||||||
const renderHeader = useMemo(() => {
|
const renderHeader = useMemo(() => {
|
||||||
@ -242,7 +241,7 @@ const CodeBlockView: React.FC<Props> = ({ children, language, onSave }) => {
|
|||||||
{isExecutable && output && <StatusBar>{output}</StatusBar>}
|
{isExecutable && output && <StatusBar>{output}</StatusBar>}
|
||||||
</CodeBlockWrapper>
|
</CodeBlockWrapper>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
|
const CodeBlockWrapper = styled.div<{ $isInSpecialView: boolean }>`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -293,5 +292,3 @@ const SplitViewWrapper = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export default memo(CodeBlockView)
|
|
||||||
@ -12,12 +12,20 @@ const linterLoaders: Record<string, () => Promise<any>> = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLanguageExtensions = (language: string, lint?: boolean) => {
|
/**
|
||||||
const { languageMap } = useCodeStyle()
|
* 特殊语言加载器
|
||||||
const [extensions, setExtensions] = useState<Extension[]>([])
|
*/
|
||||||
|
const specialLanguageLoaders: Record<string, () => Promise<Extension>> = {
|
||||||
|
dot: async () => {
|
||||||
|
const mod = await import('@viz-js/lang-dot')
|
||||||
|
return mod.dot()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 加载语言
|
/**
|
||||||
useEffect(() => {
|
* 加载语言扩展
|
||||||
|
*/
|
||||||
|
async function loadLanguageExtension(language: string, languageMap: Record<string, string>): Promise<Extension | null> {
|
||||||
let normalizedLang = languageMap[language as keyof typeof languageMap] || language.toLowerCase()
|
let normalizedLang = languageMap[language as keyof typeof languageMap] || language.toLowerCase()
|
||||||
|
|
||||||
// 如果语言名包含 `-`,转换为驼峰命名法
|
// 如果语言名包含 `-`,转换为驼峰命名法
|
||||||
@ -25,32 +33,90 @@ export const useLanguageExtensions = (language: string, lint?: boolean) => {
|
|||||||
normalizedLang = normalizedLang.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
|
normalizedLang = normalizedLang.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
|
||||||
}
|
}
|
||||||
|
|
||||||
import('@uiw/codemirror-extensions-langs')
|
// 尝试加载特殊语言
|
||||||
.then(({ loadLanguage }) => {
|
const specialLoader = specialLanguageLoaders[normalizedLang]
|
||||||
const extension = loadLanguage(normalizedLang as any)
|
if (specialLoader) {
|
||||||
if (extension) {
|
try {
|
||||||
setExtensions((prev) => [...prev, extension])
|
return await specialLoader()
|
||||||
|
} catch (error) {
|
||||||
|
console.debug(`Failed to load language ${normalizedLang}`, error)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.catch((error) => {
|
|
||||||
console.debug(`Failed to load language: ${normalizedLang}`, error)
|
// 回退到 uiw/codemirror 包含的语言
|
||||||
})
|
try {
|
||||||
}, [language, languageMap])
|
const { loadLanguage } = await import('@uiw/codemirror-extensions-langs')
|
||||||
|
const extension = loadLanguage(normalizedLang as any)
|
||||||
|
return extension || null
|
||||||
|
} catch (error) {
|
||||||
|
console.debug(`Failed to load language ${normalizedLang}`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 linter 扩展
|
||||||
|
*/
|
||||||
|
async function loadLinterExtension(language: string): Promise<Extension | null> {
|
||||||
|
const loader = linterLoaders[language]
|
||||||
|
if (!loader) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await loader()
|
||||||
|
} catch (error) {
|
||||||
|
console.debug(`Failed to load linter for ${language}`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载语言相关扩展
|
||||||
|
*/
|
||||||
|
export const useLanguageExtensions = (language: string, lint?: boolean) => {
|
||||||
|
const { languageMap } = useCodeStyle()
|
||||||
|
const [extensions, setExtensions] = useState<Extension[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lint) return
|
let cancelled = false
|
||||||
|
|
||||||
const loader = linterLoaders[language]
|
const loadAllExtensions = async () => {
|
||||||
if (loader) {
|
try {
|
||||||
loader()
|
// 加载所有扩展
|
||||||
.then((extension) => {
|
const [languageResult, linterResult] = await Promise.allSettled([
|
||||||
setExtensions((prev) => [...prev, extension])
|
loadLanguageExtension(language, languageMap),
|
||||||
})
|
lint ? loadLinterExtension(language) : Promise.resolve(null)
|
||||||
.catch((error) => {
|
])
|
||||||
console.error(`Failed to load linter for ${language}`, error)
|
|
||||||
})
|
if (cancelled) return
|
||||||
|
|
||||||
|
const results: Extension[] = []
|
||||||
|
|
||||||
|
// 语言扩展
|
||||||
|
if (languageResult.status === 'fulfilled' && languageResult.value) {
|
||||||
|
results.push(languageResult.value)
|
||||||
}
|
}
|
||||||
}, [language, lint])
|
|
||||||
|
// linter 扩展
|
||||||
|
if (linterResult.status === 'fulfilled' && linterResult.value) {
|
||||||
|
results.push(linterResult.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
setExtensions(results)
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
console.debug('Failed to load language extensions:', error)
|
||||||
|
setExtensions([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAllExtensions()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [language, lint, languageMap])
|
||||||
|
|
||||||
return extensions
|
return extensions
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,7 +99,8 @@ export const CodeStyleProvider: React.FC<PropsWithChildren> = ({ children }) =>
|
|||||||
bash: 'shell',
|
bash: 'shell',
|
||||||
'objective-c++': 'objective-cpp',
|
'objective-c++': 'objective-cpp',
|
||||||
svg: 'xml',
|
svg: 'xml',
|
||||||
vab: 'vb'
|
vab: 'vb',
|
||||||
|
graphviz: 'dot'
|
||||||
} as Record<string, string>
|
} as Record<string, string>
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import CodeBlockView from '@renderer/components/CodeBlockView'
|
import { CodeBlockView } from '@renderer/components/CodeBlockView'
|
||||||
import React, { memo, useCallback } from 'react'
|
import React, { memo, useCallback } from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
32
yarn.lock
32
yarn.lock
@ -3412,7 +3412,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0, @lezer/common@npm:^1.2.1":
|
"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.0.3, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0, @lezer/common@npm:^1.2.1":
|
||||||
version: 1.2.3
|
version: 1.2.3
|
||||||
resolution: "@lezer/common@npm:1.2.3"
|
resolution: "@lezer/common@npm:1.2.3"
|
||||||
checksum: 10c0/fe9f8e111080ef94037a34ca2af1221c8d01c1763ba5ecf708a286185c76119509a5d19d924c8842172716716ddce22d7834394670c4a9432f0ba9f3b7c0f50d
|
checksum: 10c0/fe9f8e111080ef94037a34ca2af1221c8d01c1763ba5ecf708a286185c76119509a5d19d924c8842172716716ddce22d7834394670c4a9432f0ba9f3b7c0f50d
|
||||||
@ -3515,7 +3515,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lezer/lr@npm:^1.0.0, @lezer/lr@npm:^1.1.0, @lezer/lr@npm:^1.3.0, @lezer/lr@npm:^1.3.1, @lezer/lr@npm:^1.3.10, @lezer/lr@npm:^1.3.3, @lezer/lr@npm:^1.4.0":
|
"@lezer/lr@npm:^1.0.0, @lezer/lr@npm:^1.1.0, @lezer/lr@npm:^1.3.0, @lezer/lr@npm:^1.3.1, @lezer/lr@npm:^1.3.10, @lezer/lr@npm:^1.3.3, @lezer/lr@npm:^1.4.0, @lezer/lr@npm:^1.4.2":
|
||||||
version: 1.4.2
|
version: 1.4.2
|
||||||
resolution: "@lezer/lr@npm:1.4.2"
|
resolution: "@lezer/lr@npm:1.4.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3578,7 +3578,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lezer/xml@npm:^1.0.0":
|
"@lezer/xml@npm:^1.0.0, @lezer/xml@npm:^1.0.2":
|
||||||
version: 1.0.6
|
version: 1.0.6
|
||||||
resolution: "@lezer/xml@npm:1.0.6"
|
resolution: "@lezer/xml@npm:1.0.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6962,6 +6962,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@viz-js/lang-dot@npm:^1.0.5":
|
||||||
|
version: 1.0.5
|
||||||
|
resolution: "@viz-js/lang-dot@npm:1.0.5"
|
||||||
|
dependencies:
|
||||||
|
"@codemirror/language": "npm:^6.8.0"
|
||||||
|
"@lezer/common": "npm:^1.0.3"
|
||||||
|
"@lezer/highlight": "npm:^1.1.6"
|
||||||
|
"@lezer/lr": "npm:^1.4.2"
|
||||||
|
"@lezer/xml": "npm:^1.0.2"
|
||||||
|
checksum: 10c0/86e81bf077e0a6f418fe2d5cfd8d7f7a7c032bdec13e5dfe3d21620c548e674832f6c9b300eeaad7b0842a3c4044d4ce33d5af9e359ae1efeda0a84d772b77a4
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@viz-js/viz@npm:^3.14.0":
|
||||||
|
version: 3.14.0
|
||||||
|
resolution: "@viz-js/viz@npm:3.14.0"
|
||||||
|
checksum: 10c0/901afa2d99e8f33cc4abf352f1559e0c16958e01f0750a65a33799aebfe175a18d74f6945f1ff93f64b53b69976dc3d07d39d65c58dda955abd0979dacc4294c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@vue/compiler-core@npm:3.5.17":
|
"@vue/compiler-core@npm:3.5.17":
|
||||||
version: 3.5.17
|
version: 3.5.17
|
||||||
resolution: "@vue/compiler-core@npm:3.5.17"
|
resolution: "@vue/compiler-core@npm:3.5.17"
|
||||||
@ -7075,6 +7095,7 @@ __metadata:
|
|||||||
"@cherrystudio/embedjs-openai": "npm:^0.1.31"
|
"@cherrystudio/embedjs-openai": "npm:^0.1.31"
|
||||||
"@cherrystudio/mac-system-ocr": "npm:^0.2.2"
|
"@cherrystudio/mac-system-ocr": "npm:^0.2.2"
|
||||||
"@cherrystudio/pdf-to-img-napi": "npm:^0.0.1"
|
"@cherrystudio/pdf-to-img-napi": "npm:^0.0.1"
|
||||||
|
"@codemirror/view": "npm:^6.0.0"
|
||||||
"@electron-toolkit/eslint-config-prettier": "npm:^3.0.0"
|
"@electron-toolkit/eslint-config-prettier": "npm:^3.0.0"
|
||||||
"@electron-toolkit/eslint-config-ts": "npm:^3.0.0"
|
"@electron-toolkit/eslint-config-ts": "npm:^3.0.0"
|
||||||
"@electron-toolkit/preload": "npm:^3.0.0"
|
"@electron-toolkit/preload": "npm:^3.0.0"
|
||||||
@ -7127,6 +7148,8 @@ __metadata:
|
|||||||
"@vitest/coverage-v8": "npm:^3.1.4"
|
"@vitest/coverage-v8": "npm:^3.1.4"
|
||||||
"@vitest/ui": "npm:^3.1.4"
|
"@vitest/ui": "npm:^3.1.4"
|
||||||
"@vitest/web-worker": "npm:^3.1.4"
|
"@vitest/web-worker": "npm:^3.1.4"
|
||||||
|
"@viz-js/lang-dot": "npm:^1.0.5"
|
||||||
|
"@viz-js/viz": "npm:^3.14.0"
|
||||||
"@xyflow/react": "npm:^12.4.4"
|
"@xyflow/react": "npm:^12.4.4"
|
||||||
antd: "patch:antd@npm%3A5.24.7#~/.yarn/patches/antd-npm-5.24.7-356a553ae5.patch"
|
antd: "patch:antd@npm%3A5.24.7#~/.yarn/patches/antd-npm-5.24.7-356a553ae5.patch"
|
||||||
archiver: "npm:^7.0.1"
|
archiver: "npm:^7.0.1"
|
||||||
@ -7221,6 +7244,7 @@ __metadata:
|
|||||||
tokenx: "npm:^1.1.0"
|
tokenx: "npm:^1.1.0"
|
||||||
turndown: "npm:7.2.0"
|
turndown: "npm:7.2.0"
|
||||||
typescript: "npm:^5.6.2"
|
typescript: "npm:^5.6.2"
|
||||||
|
unified: "npm:^11.0.5"
|
||||||
uuid: "npm:^10.0.0"
|
uuid: "npm:^10.0.0"
|
||||||
vite: "npm:6.2.6"
|
vite: "npm:6.2.6"
|
||||||
vitest: "npm:^3.1.4"
|
vitest: "npm:^3.1.4"
|
||||||
@ -19565,7 +19589,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"unified@npm:^11.0.0":
|
"unified@npm:^11.0.0, unified@npm:^11.0.5":
|
||||||
version: 11.0.5
|
version: 11.0.5
|
||||||
resolution: "unified@npm:11.0.5"
|
resolution: "unified@npm:11.0.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user