feat: enhance MDX rendering with code copy functionality
- Add code copy button to code blocks with internationalized label - Modify MDX component to support raw code string extraction - Update content collections to inject raw code string into pre elements - Improve code block styling and add hover-based copy button - Remove unnecessary prose wrapper from various components - Enhance code block presentation with better overflow and border handling
This commit is contained in:
parent
9a6ce6b7c3
commit
805a871146
@ -8,6 +8,7 @@ import remarkGfm from 'remark-gfm';
|
||||
import { createHighlighter } from 'shiki';
|
||||
import path from "path";
|
||||
import { LOCALES, DEFAULT_LOCALE } from "@/i18n/routing";
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
/**
|
||||
* Content Collections documentation
|
||||
@ -100,17 +101,7 @@ export const posts = defineCollection({
|
||||
locale: z.enum(LOCALES as [string, ...string[]]).optional()
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
const body = await compileMDX(context, data, {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
codeImport
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
[rehypePrettyCode, prettyCodeOptions]
|
||||
]
|
||||
});
|
||||
const body = await compileWithCodeCopy(context, data);
|
||||
|
||||
// Determine the locale from the file path or use the provided locale
|
||||
const pathParts = data._meta.path.split(path.sep);
|
||||
@ -190,17 +181,7 @@ export const pages = defineCollection({
|
||||
locale: z.enum(LOCALES as [string, ...string[]]).optional()
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
const body = await compileMDX(context, data, {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
codeImport
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
[rehypePrettyCode, prettyCodeOptions]
|
||||
]
|
||||
});
|
||||
const body = await compileWithCodeCopy(context, data);
|
||||
|
||||
// Determine the locale from the file path or use the provided locale
|
||||
const pathParts = data._meta.path.split(path.sep);
|
||||
@ -258,17 +239,7 @@ export const releases = defineCollection({
|
||||
locale: z.enum(LOCALES as [string, ...string[]]).optional()
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
const body = await compileMDX(context, data, {
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
codeImport
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
rehypeAutolinkHeadings,
|
||||
[rehypePrettyCode, prettyCodeOptions]
|
||||
]
|
||||
});
|
||||
const body = await compileWithCodeCopy(context, data);
|
||||
|
||||
// Determine the locale from the file path or use the provided locale
|
||||
const pathParts = data._meta.path.split(path.sep);
|
||||
@ -327,6 +298,40 @@ const prettyCodeOptions: Options = {
|
||||
}
|
||||
};
|
||||
|
||||
const compileWithCodeCopy = async (
|
||||
context: any,
|
||||
data: any,
|
||||
options: {
|
||||
remarkPlugins?: any[];
|
||||
rehypePlugins?: any[];
|
||||
} = {}
|
||||
) => {
|
||||
return await compileMDX(context, data, {
|
||||
...options,
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
codeImport,
|
||||
...(options.remarkPlugins || [])
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypePrettyCode, prettyCodeOptions],
|
||||
// add __rawString__ to pre element
|
||||
() => (tree) => {
|
||||
visit(tree, (node) => {
|
||||
if (node?.type === "element" && node?.tagName === "pre") {
|
||||
const [codeEl] = node.children;
|
||||
if (codeEl.tagName !== "code") return;
|
||||
node.__rawString__ = codeEl.children?.[0]?.value;
|
||||
}
|
||||
});
|
||||
},
|
||||
rehypeAutolinkHeadings,
|
||||
...(options.rehypePlugins || [])
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
collections: [authors, categories, posts, pages, releases]
|
||||
});
|
@ -13,7 +13,8 @@
|
||||
"language": "Switch language",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
"system": "System",
|
||||
"copy": "Copy"
|
||||
},
|
||||
"HomePage": {
|
||||
"title": "next-intl example"
|
||||
|
@ -13,7 +13,8 @@
|
||||
"language": "切换语言",
|
||||
"light": "浅色模式",
|
||||
"dark": "深色模式",
|
||||
"system": "跟随系统"
|
||||
"system": "跟随系统",
|
||||
"copy": "复制"
|
||||
},
|
||||
"HomePage": {
|
||||
"title": "next-intl 示例"
|
||||
|
@ -137,11 +137,9 @@ export default async function BlogPostPage(props: NextPageProps) {
|
||||
</div>
|
||||
|
||||
{/* blog post content */}
|
||||
<div className="prose prose-gray dark:prose-invert max-w-none">
|
||||
<Mdx code={post.body.code} />
|
||||
</div>
|
||||
<Mdx code={post.body.code} />
|
||||
|
||||
<div className="flex items-center justify-start mt-16">
|
||||
<div className="flex items-center justify-start my-16">
|
||||
<AllPostsButton />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,9 +39,7 @@ export function CustomPage({
|
||||
{/* Content */}
|
||||
<Card className="mb-8">
|
||||
<CardContent>
|
||||
<div className="prose prose-gray dark:prose-invert max-w-none">
|
||||
<Mdx code={content} />
|
||||
</div>
|
||||
<Mdx code={content} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -40,9 +40,7 @@ export function ReleaseCard({
|
||||
<Separator />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="prose prose-gray dark:prose-invert max-w-none">
|
||||
<Mdx code={content} />
|
||||
</div>
|
||||
<Mdx code={content} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CheckIcon, CopyIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
@ -11,8 +12,8 @@ interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
|
||||
export function CopyButton({ value, className, ...props }: CopyButtonProps) {
|
||||
const [hasCopied, setHasCopied] = React.useState(false);
|
||||
const t = useTranslations('Common');
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setHasCopied(false);
|
||||
@ -35,7 +36,7 @@ export function CopyButton({ value, className, ...props }: CopyButtonProps) {
|
||||
onClick={() => handleCopyValue(value)}
|
||||
{...props}
|
||||
>
|
||||
<span className="sr-only">Copy</span>
|
||||
<span className="sr-only">{t('copy')}</span>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="size-4" />
|
||||
) : (
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
} from '@/components/ui/accordion';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CopyButton } from './copy-button';
|
||||
|
||||
const components = {
|
||||
h1: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
@ -135,14 +136,29 @@ const components = {
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
pre: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
|
||||
<pre
|
||||
className={cn(
|
||||
'relative mt-4 max-w-[calc(100vw-64px)] overflow-x-auto rounded bg-muted px-1 py-2 font-mono text-sm',
|
||||
className
|
||||
pre: ({
|
||||
className,
|
||||
__rawString__,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLPreElement> & { __rawString__?: string }) => (
|
||||
<div className="my-4 group relative w-full overflow-hidden">
|
||||
<pre
|
||||
className={cn(
|
||||
"max-h-[650px] overflow-x-auto rounded-lg border bg-zinc-900 py-4 dark:bg-zinc-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{__rawString__ && (
|
||||
<CopyButton
|
||||
value={__rawString__}
|
||||
className={cn(
|
||||
"absolute right-4 top-4 z-20",
|
||||
"duration-250 opacity-0 transition-all group-hover:opacity-100",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
code: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
|
||||
<code
|
||||
@ -163,7 +179,12 @@ const components = {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Callout,
|
||||
Image,
|
||||
Image: ({ className, ...props }: React.ComponentProps<typeof Image>) => (
|
||||
<Image
|
||||
className={cn('my-4 w-full rounded-lg', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
Tabs: ({ className, ...props }: React.ComponentProps<typeof Tabs>) => (
|
||||
<Tabs className={cn('relative mt-6 w-full', className)} {...props} />
|
||||
),
|
||||
@ -207,7 +228,7 @@ type MdxProps = {
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: update
|
||||
* render mdx content with custom components
|
||||
*/
|
||||
export function Mdx({ code }: MdxProps) {
|
||||
const Component = useMDXComponent(code);
|
||||
|
Loading…
Reference in New Issue
Block a user