fix: docs
This commit is contained in:
imccyu
2026-07-30 15:56:41 +08:00
parent 085383b145
commit 1e10966ef6
160 changed files with 2521 additions and 1135 deletions

View File

@@ -17,9 +17,13 @@ export interface CodeBlockProps {
lang?: string | undefined
/** Extra class merged onto the wrapper (callers position; this component draws). */
className?: string | undefined
/** Copy-button idle label; the owner passes localized copy (this package is cordis-free, so copy arrives via props). */
copyLabel?: string | undefined
/** Copy-button label during the post-copy confirmation window. */
copiedLabel?: string | undefined
}
export function CodeBlock({ code, lang, className }: CodeBlockProps) {
export function CodeBlock({ code, lang, className, copyLabel = '复制', copiedLabel = '复制成功' }: CodeBlockProps) {
const trimmed = code.endsWith('\n') ? code.slice(0, -1) : code
const html = useMemo(() => highlightToHtml(trimmed, lang), [trimmed, lang])
const rootRef = useRef<HTMLDivElement>(null)
@@ -55,7 +59,7 @@ export function CodeBlock({ code, lang, className }: CodeBlockProps) {
<div className={css.infostring}>{lang ?? ''}</div>
<div className={css.action}>
<button type="button" className={css.copyButton} onClick={onCopy}>
{copied ? '复制成功' : '复制'}
{copied ? copiedLabel : copyLabel}
</button>
</div>
</div>

View File

@@ -5,10 +5,17 @@ import css from './JsonBlock.module.css'
const MAX_CHARS = 20_000
export function JsonBlock({ label, payload, defaultOpen = false }: {
/** Default truncation footer; the owner passes a localized formatter. */
function defaultTruncatedLabel(total: number): string {
return `… 已截断,共 ${total} 字符`
}
export function JsonBlock({ label, payload, defaultOpen = false, truncatedLabel = defaultTruncatedLabel }: {
label: string
payload: unknown
defaultOpen?: boolean
/** Footer appended when the body exceeds the char cap, given the full length (this package is cordis-free, so copy arrives via props). */
truncatedLabel?: ((total: number) => string) | undefined
}) {
const [open, setOpen] = useState(defaultOpen)
const body = useMemo(() => {
@@ -21,8 +28,8 @@ export function JsonBlock({ label, payload, defaultOpen = false }: {
} catch {
s = String(payload)
}
return s.length > MAX_CHARS ? `${s.slice(0, MAX_CHARS)}\n… 已截断,共 ${s.length} 字符` : s
}, [open, payload])
return s.length > MAX_CHARS ? `${s.slice(0, MAX_CHARS)}\n${truncatedLabel(s.length)}` : s
}, [open, payload, truncatedLabel])
return (
<div className={css.root}>
<button type="button" className={css.toggle} onClick={() => { setOpen(v => !v) }}>

View File

@@ -1,4 +1,4 @@
import { isValidElement } from 'react'
import { isValidElement, useMemo } from 'react'
import ReactMarkdown from 'react-markdown'
import type { Components, UrlTransform } from 'react-markdown'
import remarkGfm from 'remark-gfm'
@@ -24,8 +24,16 @@ function sanitizeUrl(url: string): string {
const safeUrl: UrlTransform = url => sanitizeUrl(url)
/** Copy-button labels forwarded to fence CodeBlocks (this package is cordis-free, so copy arrives via props). */
export interface MarkdownCodeLabels {
/** Copy-button idle label. */
copyLabel?: string | undefined
/** Copy-button label during the post-copy confirmation window. */
copiedLabel?: string | undefined
}
/** Build the component table; while `streaming`, fences render the plain arm (see CodeBlock). */
function buildComponents(streaming: boolean): Components {
function buildComponents(streaming: boolean, codeLabels?: MarkdownCodeLabels): Components {
return {
a: ({ href = '', children }) => {
const safeHref = sanitizeUrl(href)
@@ -62,7 +70,14 @@ function buildComponents(streaming: boolean): Components {
// keeps the stock <pre> rather than guessing.
if (typeof raw !== 'string') return <pre>{children}</pre>
const lang = /language-([\w-]+)/.exec(child?.props.className ?? '')?.[1]
return <CodeBlock code={raw} lang={streaming ? undefined : lang} />
return (
<CodeBlock
code={raw}
lang={streaming ? undefined : lang}
copyLabel={codeLabels?.copyLabel}
copiedLabel={codeLabels?.copiedLabel}
/>
)
},
}
}
@@ -73,15 +88,29 @@ const streamingComponents = buildComponents(true)
/**
* Render untrusted assistant-authored Markdown as semantic React elements.
* @param props - Markdown source text preserved by the session projection;
* `streaming` renders fences plain (highlighting lands on the finalize swap).
* `streaming` renders fences plain (highlighting lands on the finalize swap);
* `codeLabels` forwards localized copy-button labels to fence CodeBlocks —
* pass a reference-stable object (memoized per locale revision), because the
* component table memoizes on its identity and a fresh literal per render
* would rebuild it every streaming chunk.
* @returns A GFM document with raw HTML, relative links, unsafe protocols, and remote images disabled.
*/
export function MarkdownText({ text, streaming = false }: { text: string; streaming?: boolean }) {
export function MarkdownText({ text, streaming = false, codeLabels }: {
text: string
streaming?: boolean
codeLabels?: MarkdownCodeLabels | undefined
}) {
// The label-free tables stay module-level singletons so the common case
// keeps referential stability across renders without a hook.
const components = useMemo(() => {
if (codeLabels === undefined) return streaming ? streamingComponents : staticComponents
return buildComponents(streaming, codeLabels)
}, [streaming, codeLabels])
return (
<div className={css.markdown}>
<ReactMarkdown
remarkPlugins={remarkPlugins}
components={streaming ? streamingComponents : staticComponents}
components={components}
urlTransform={safeUrl}
>
{text}