All checks were successful
Deploy / deploy (push) Successful in 9m29s
- Modified redirects in next.config.ts to change the destination for "/sohbet" to "/listem". - Added new dependencies in package.json: marked, motion, react-markdown, remark-breaks, remark-gfm, shiki, and use-stick-to-bottom. - Updated pnpm-lock.yaml to reflect the new package versions and dependencies. - Removed obsolete stack files for Flutter, Nuxt, Nuxt.js, React Native, and Svelte from the UI/UX Pro Max skill data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
111 lines
2.6 KiB
TypeScript
111 lines
2.6 KiB
TypeScript
import { cn } from "@/lib/utils"
|
|
import { marked } from "marked"
|
|
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>
|
|
}
|
|
|
|
function parseMarkdownIntoBlocks(markdown: string): string[] {
|
|
const tokens = marked.lexer(markdown)
|
|
return tokens.map((token) => token.raw)
|
|
}
|
|
|
|
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 }
|