- markdown.tsx: blok bölme marked.lexer yerine çit-farkındalıklı boş satır bölücüsü — react-markdown'ın yanında ikinci tam parser (~460 KB paket) client'a inmiyordu artık gerekmiyor; memoized blok yapısı aynen korunur - markdown-lazy: react-markdown+remark zinciri (~215 KB) lazy; fallback aynı class'lı düz metin (stream'de flash/CLS yok), sohbet mount'unda preloadMarkdown ile chunk önden ısınır — /listem 1315->1133 KB eager JS - el-cizimi-sema: mermaid importu IntersectionObserver'a bağlandı (rootMargin 200px); şemalar hep fold altında, ~1 MB mount'ta inmiyor Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
128 lines
3.3 KiB
TypeScript
128 lines
3.3 KiB
TypeScript
import { cn } from "@/lib/utils"
|
||
import { memo, useId, useMemo } from "react"
|
||
import ReactMarkdown, { Components } from "react-markdown"
|
||
import remarkBreaks from "remark-breaks"
|
||
import remarkGfm from "remark-gfm"
|
||
import { CodeBlock, CodeBlockCode } from "./code-block"
|
||
|
||
export type MarkdownProps = {
|
||
children: string
|
||
id?: string
|
||
className?: string
|
||
components?: Partial<Components>
|
||
}
|
||
|
||
// Stream memoization'ı için bloklara bölme. Eskiden marked.lexer yapıyordu —
|
||
// react-markdown'ın micromark'ına ek ikinci bir tam parser'ı (~460 KB paket)
|
||
// client'a taşımak blok bölmek için gereksiz: kod çiti dışındaki boş satırlar
|
||
// zaten markdown'da blok sınırıdır. Çit içi boş satırlar korunur.
|
||
function parseMarkdownIntoBlocks(markdown: string): string[] {
|
||
const bloklar: string[] = []
|
||
let mevcut: string[] = []
|
||
let citte = false
|
||
for (const satir of markdown.split("\n")) {
|
||
if (/^\s*(```|~~~)/.test(satir)) citte = !citte
|
||
if (!citte && satir.trim() === "") {
|
||
if (mevcut.length) {
|
||
bloklar.push(mevcut.join("\n"))
|
||
mevcut = []
|
||
}
|
||
} else {
|
||
mevcut.push(satir)
|
||
}
|
||
}
|
||
if (mevcut.length) bloklar.push(mevcut.join("\n"))
|
||
return bloklar.length ? bloklar : [markdown]
|
||
}
|
||
|
||
function extractLanguage(className?: string): string {
|
||
if (!className) return "plaintext"
|
||
const match = className.match(/language-(\w+)/)
|
||
return match ? match[1] : "plaintext"
|
||
}
|
||
|
||
const INITIAL_COMPONENTS: Partial<Components> = {
|
||
code: function CodeComponent({ className, children, ...props }) {
|
||
const isInline =
|
||
!props.node?.position?.start.line ||
|
||
props.node?.position?.start.line === props.node?.position?.end.line
|
||
|
||
if (isInline) {
|
||
return (
|
||
<span
|
||
className={cn(
|
||
"bg-primary-foreground rounded-sm px-1 font-mono text-sm",
|
||
className
|
||
)}
|
||
{...props}
|
||
>
|
||
{children}
|
||
</span>
|
||
)
|
||
}
|
||
|
||
const language = extractLanguage(className)
|
||
|
||
return (
|
||
<CodeBlock className={className}>
|
||
<CodeBlockCode code={children as string} language={language} />
|
||
</CodeBlock>
|
||
)
|
||
},
|
||
pre: function PreComponent({ children }) {
|
||
return <>{children}</>
|
||
},
|
||
}
|
||
|
||
const MemoizedMarkdownBlock = memo(
|
||
function MarkdownBlock({
|
||
content,
|
||
components = INITIAL_COMPONENTS,
|
||
}: {
|
||
content: string
|
||
components?: Partial<Components>
|
||
}) {
|
||
return (
|
||
<ReactMarkdown
|
||
remarkPlugins={[remarkGfm, remarkBreaks]}
|
||
components={components}
|
||
>
|
||
{content}
|
||
</ReactMarkdown>
|
||
)
|
||
},
|
||
function propsAreEqual(prevProps, nextProps) {
|
||
return prevProps.content === nextProps.content
|
||
}
|
||
)
|
||
|
||
MemoizedMarkdownBlock.displayName = "MemoizedMarkdownBlock"
|
||
|
||
function MarkdownComponent({
|
||
children,
|
||
id,
|
||
className,
|
||
components = INITIAL_COMPONENTS,
|
||
}: MarkdownProps) {
|
||
const generatedId = useId()
|
||
const blockId = id ?? generatedId
|
||
const blocks = useMemo(() => parseMarkdownIntoBlocks(children), [children])
|
||
|
||
return (
|
||
<div className={className}>
|
||
{blocks.map((block, index) => (
|
||
<MemoizedMarkdownBlock
|
||
key={`${blockId}-block-${index}`}
|
||
content={block}
|
||
components={components}
|
||
/>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const Markdown = memo(MarkdownComponent)
|
||
Markdown.displayName = "Markdown"
|
||
|
||
export { Markdown }
|