hololake-system-architecture/product-source/hololake-platform/src/components/MarkdownContent.tsx

119 lines
4 KiB
TypeScript

import { memo, useMemo, type MouseEvent, type ReactNode } from 'react'
import Markdown, { defaultUrlTransform } from 'react-markdown'
import remarkGfm from 'remark-gfm'
import rehypeHighlight from 'rehype-highlight'
import { preprocessWikilinks, WIKILINK_SCHEME } from '../utils/chatWikilinks'
import { supportsModernRegexFeatures } from '../utils/regexCapabilities'
import { openExternalUrl } from '../utils/url'
const MODERN_REGEX_AVAILABLE = supportsModernRegexFeatures()
const REMARK_PLUGINS = MODERN_REGEX_AVAILABLE ? [remarkGfm] : []
const REHYPE_PLUGINS = MODERN_REGEX_AVAILABLE ? [rehypeHighlight] : []
function wikilinkUrlTransform(url: string): string {
if (url.startsWith(WIKILINK_SCHEME)) return url
return defaultUrlTransform(url)
}
function isExplicitWebUrl(href?: string): href is string {
const lowerHref = href?.trim().toLowerCase() ?? ''
return lowerHref.startsWith('http://') || lowerHref.startsWith('https://')
}
function openExplicitWebUrl(event: MouseEvent<HTMLAnchorElement>, href: string) {
event.preventDefault()
void openExternalUrl(href).catch((error) => {
console.warn('[ai] Failed to open external link:', error)
})
}
function localNoteTarget(href?: string): string | null {
const raw = href?.trim()
if (!raw || raw.startsWith('#') || raw.startsWith('/') || raw.startsWith('\\')) return null
if (/^[a-z][a-z0-9+.-]*:/i.test(raw)) return null
try {
return decodeURIComponent(raw)
.split(/[?#]/, 1)[0]
.replace(/^\.\//, '')
.replace(/\.(?:md|hdlp)$/i, '') || null
} catch {
return raw.replace(/^\.\//, '').replace(/\.(?:md|hdlp)$/i, '') || null
}
}
interface MarkdownContentProps {
content: string
onWikilinkClick?: (target: string) => void
}
export const MarkdownContent = memo(function MarkdownContent({ content, onWikilinkClick }: MarkdownContentProps) {
const processedContent = useMemo(
() => onWikilinkClick ? preprocessWikilinks(content) : content,
[content, onWikilinkClick],
)
const components = useMemo(() => {
return {
a: ({ href, children }: { href?: string; children?: ReactNode }) => {
if (onWikilinkClick && href?.startsWith(WIKILINK_SCHEME)) {
const target = decodeURIComponent(href.slice(WIKILINK_SCHEME.length))
return (
<a
ref={(node) => {
node?.setAttribute('role', 'link')
node?.setAttribute('tabindex', '0')
}}
href={href}
className="chat-wikilink border-0 bg-transparent p-0"
data-wikilink-target={target}
onClick={(event) => {
event.preventDefault()
onWikilinkClick(target)
}}
>
{children}
</a>
)
}
if (isExplicitWebUrl(href)) {
return <a href={href} onClick={(event) => openExplicitWebUrl(event, href)}>{children}</a>
}
const noteTarget = onWikilinkClick ? localNoteTarget(href) : null
if (noteTarget) {
return (
<a
href={href}
className="chat-wikilink border-0 bg-transparent p-0"
data-wikilink-target={noteTarget}
onClick={(event) => {
event.preventDefault()
onWikilinkClick?.(noteTarget)
}}
>
{children}
</a>
)
}
return <a href={href}>{children}</a>
},
table: ({ children }: { children?: ReactNode }) => (
<div className="ai-markdown-table-wrap" role="region" aria-label="表格,可横向滚动">
<table>{children}</table>
</div>
),
}
}, [onWikilinkClick])
return (
<div className="ai-markdown min-w-0 max-w-full overflow-hidden">
<Markdown
remarkPlugins={REMARK_PLUGINS}
rehypePlugins={REHYPE_PLUGINS}
components={components}
urlTransform={onWikilinkClick ? wikilinkUrlTransform : undefined}
>
{processedContent}
</Markdown>
</div>
)
})