在MDX中使用Next.js和Rehype Pretty Code添加“复制到剪贴板”按钮

🔥 天冷了🔥 烤烤火

代码共享是现代Web开发的一个重要方面,它使开发人员能够合作并互相学习。许多网站通过在其代码片段中添加"复制到剪贴板"按钮,使代码共享变得更加容易。这一功能在移动设备上尤其有用,为用户提供了一种快速简便的方法,无需手动选择文本即可共享代码片段。一些代码高亮库包含此功能,可以通过内置或插件方式实现。

然而,如果你在你的Next.js应用程序中使用MDX,为实现"复制到剪贴板"按钮并没有一种适用于所有情况的解决方案。

本教程将指导你如何为使用Rehype Pretty Code处理的代码片段创建一个"复制到剪贴板"按钮,Rehype Pretty Code是MDX-based Next.js网站中常用的用于语法高亮的Rehype插件。我们将使用Next.js 13+,并启用"app"目录。在本文结束时,你将能够简化代码共享,并提升你的Next.js网站的用户体验。事实上,这篇博客就使用了我们将要创建的功能,所以你可以在实际运行中看到结果。让我们开始吧!

设置

像往常一样,我们需要安装所需的依赖。

css 复制代码
npm install rehype-pretty-code shiki unist-util-visit

在这个项目中,我们使用Rehype Pretty Code插件,该插件为MD或MDX文件提供语法高亮。此高亮是在构建时执行的,对页面速度产生积极影响。此外,你可能需要一个用于处理MDX内容的库。这篇博客使用Contentlayer,该库对MDX处理提供了强大的支持。然而,请注意它目前处于beta版本,可能并不适合所有人。

如果你想提高Next.js网站的可发现性,我写了一篇关于如何添加站点地图的文章:在Next.js 13中实现动态站点地图:使用Pages目录或App目录

提取代码内容

在我们的案例中,渲染按钮是一项挑战,因为语法高亮是在服务器端执行的。当我们打算在客户端渲染按钮时,代码内容将已经包装在各种标记中,这些标记是语法高亮所必需的。一种潜在的解决方案是在复制时解析代码内容,删除所有HTML标记。然而,这种方法效率低下,因为它需要撤销Rehype Pretty Code为我们完成的一切。

幸运的是,有一种更好的方法来实现我们的目标。我们可以在语法高亮阶段之前提取未经样式化的代码内容,并将其附加为代码节点的属性。然后,我们可以在自定义MDX组件内访问此内容,该组件用于渲染复制按钮。

首先,我们需要创建一个访问器函数,它遍历内容的节点树并从所有代码元素中提取未修改的(原始文本)内容,这些元素嵌套在pre标记中。我们将这个文本内容存储在pre节点本身上。为了遍历节点树,我们将使用unist-util-visit包中的visit函数。这个访问器函数应该被添加到现有的Rehype插件列表中,在Contentlayer的情况下,它是contentlayer.config.js文件。

javascript 复制代码
{
  rehypePlugins: [
    () => (tree) => {
      visit(tree, (node) => {
        if (node?.type === "element" && node?.tagName === "pre") {
          const [codeEl] = node.children;
 
          if (codeEl.tagName !== "code") return;
 
          node.raw = codeEl.children?.[0].value;
        }
      });
    }
  ]
}

这将为我们提供一种保留未修改的代码内容的方式,我们可以稍后从节点的raw属性中访问它。

现在我们添加Rehype Pretty Code插件的配置。

javascript 复制代码
{
  rehypePlugins: [
    () => (tree) => {
      visit(tree, (node) => {
        if (node?.type === "element" && node?.tagName === "pre") {
          const [codeEl] = node.children;
 
          if (codeEl.tagName !== "code") return;
 
          node.raw = codeEl.children?.[0].value;
        }
      });
    },
    [
      rehypePrettyCode,
      {
        theme: {
          dark: "one-dark-pro",
          light: "github-light",
        },
        // 其他rehypePrettyCode配置
      },
    ],
  ]
}

值得注意的是,我们在以下代码中同时使用了浅色和深色主题。该插件为每个主题生成两个单独的代码块,通过CSS隐藏其中一个,具体取决于所选的主题。为了在主题之间切换,此博客使用class属性,确保每次只有一个主题可见,具体代码如下:

css 复制代码
html.light[data-theme="dark"] {
    display: none;
}
 
html.dark[data-theme="light"] {
    display: none;
}

重要的是要注意,使用两个主题还有另一个含义:我们必须将raw属性转发到两个不同的pre元素,而不仅仅是一个。为了实现这一点,我们需要在语法高亮完成后实现另一个访问器函数。

javascript 复制代码
{
  rehypePlugins: [
    () => (tree) => {
      visit(tree, (node) => {
        if (node?.type === "element" && node?.tagName === "pre") {
          const [codeEl]
​
 = node.children;
 
          if (codeEl.tagName !== "code") return;
 
          node.raw = codeEl.children?.[0].value;
        }
      });
    },
    [
      rehypePrettyCode,
      {
        theme: {
          dark: "one-dark-pro",
          light: "github-light",
        },
        // 其他rehypePrettyCode配置
      },
    ],
    () => (tree) => {
      visit(tree, (node) => {
        if (node?.type === "element" && node?.tagName === "div") {
          if (!("data-rehype-pretty-code-fragment" in node.properties)) {
            return;
          }
 
          for (const child of node.children) {
            if (child.tagName === "pre") {
              child.properties["raw"] = node.raw;
            }
          }
        }
      });
    },
  ]
}

在这段代码中,我们选择所有包含data-rehype-pretty-code-fragment数据属性的div元素。然后,我们遍历每个div内的pre子元素(每个主题一个)并将原始代码内容作为属性添加到它们中。有了这个实现,用于渲染pre元素的自定义MDX组件将在可用的props中具有raw属性。接下来,我们将这个Pre组件添加到我们的代码中:

ini 复制代码
export const Pre = ({ children, raw, ...props }) => {
  const lang = props["data-language"];
  return (
    <pre {...props} className={"p-0"}>
      <div
        className={'code-header'}>
        {lang}
      </div>
      {children}
    </pre>
  );
};

我们还提取了data-language以显示给定片段的代码语言。现在,我们将其用作MDX渲染器内的组件之一。

javascript 复制代码
import { useMDXComponent } from "next-contentlayer/hooks";
import { Pre } from "./components/Pre";
 
const components = {
  pre: Pre,
};
 
export const Mdx = ({ code }) => {
  const MDXContent = useMDXComponent(code);
 
  return <MDXContent components={components} />;
};

Contentlayer库处理MDX渲染,我们使用其next-contentlayer包进行Next.js集成。该包提供了useMDXComponent钩子,我们使用它来渲染MDX并通过组件对象传递我们的自定义组件。这允许MDX文件中的每个pre元素都用我们的自定义Pre组件替换。pre标记的内容将通过Pre组件内的children prop访问。

添加CopyButton组件

现在我们准备添加CopyButton组件,它将处理"复制到剪贴板"功能。

为实现"复制到剪贴板"功能,我们可以使用JavaScript访问剪贴板,并使用React渲染UI。在JavaScript中,以前我们会使用document.execCommand("copy")方法将内容复制到剪贴板。然而,这种方法现在不再推荐使用,并且在某些浏览器中不受支持。一个更可靠的方法是使用Clipboard API,该API提供了一组异步方法来读取和写入剪贴板。

javascript 复制代码
"use client";
 
import { useState } from "react";
 
export const CopyButton = ({ text }) => {
  const [isCopied, setIsCopied] = useState(false);
 
  const copy = async () => {
    await navigator.clipboard.writeText(text);
    setIsCopied(true);
 
    setTimeout(() => {
      setIsCopied(false);
    }, 10000);
  };
 
  return (
    <button disabled={isCopied} onClick={copy}>
      {isCopied ? "Copied!" : "Copy"}
    </button>
  );
};

请注意,由于我们使用了app目录,其中所有组件默认为Server Components,我们需要通过"use client"指令明确将此组件标记为客户端组件。

组件本身非常简单。当单击Copy文本时,我们使用Navigator.clipboard API存储代码内容(通过text prop可用)。此外,我们将按钮文本更改为Copied!并设置十秒的超时以将其重置。

最后,我们可以在Pre组件内使用这个"复制到剪贴板"按钮。

javascript 复制代码
import { CopyButton } from "./CopyButton";
 
export const Pre = ({ children, raw, ...props }) => {
  const lang = props["data-language"] || "shell";
  return (
    <pre {...props} className={"p-0"}>
      <div className={"code-header"}>
        {lang}
        <CopyButton text={raw} />
      </div>
      {children}
    </pre>
  );
};

最终结果应该与本文中代码片段的Copy按钮类似(减去样式)。

总结

"复制到剪贴板"按钮是改进代码片段可访问性的绝佳方式,特别是在移动设备上。然而,当为使用Rehype Pretty Code处理的代码片段创建"复制到剪贴板"按钮时,在MDX-based Next.js应用程序中,需要采用特定的方法。主要的概念是将未经处理的代码内容作为pre元素的属性传递,这样在应用语法高亮后,可以在稍后访问内容。这种方法是有效的,并简化了代码共享,从而提升了在使用MDX和Rehype Pretty Code的Next.js网站上的用户体验。CopyButton组件的概念并非特定于Next.js。它被实现为一个React组件,利用JavaScript在浏览器中提供的Clipboard API。

📌 休息驿站

相关推荐
浪九天1 小时前
Vue 不同大版本与 Node.js 版本匹配的详细参数
前端·vue.js·node.js
qianmoQ1 小时前
第五章:工程化实践 - 第三节 - Tailwind CSS 大型项目最佳实践
前端·css
C#Thread1 小时前
C#上位机--流程控制(IF语句)
开发语言·javascript·ecmascript
椰果uu2 小时前
前端八股万文总结——JS+ES6
前端·javascript·es6
微wx笑2 小时前
chrome扩展程序如何实现国际化
前端·chrome
~废弃回忆 �༄2 小时前
CSS中伪类选择器
前端·javascript·css·css中伪类选择器
CUIYD_19892 小时前
Chrome 浏览器(版本号49之后)‌解决跨域问题
前端·chrome
IT、木易2 小时前
跟着AI学vue第五章
前端·javascript·vue.js
薛定谔的猫-菜鸟程序员2 小时前
Vue 2全屏滚动动画实战:结合fullpage-vue与animate.css打造炫酷H5页面
前端·css·vue.js
春天姐姐3 小时前
vue3项目开发总结
前端·vue.js·git