惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
P
Privacy International News Feed
Security Latest
Security Latest
Spread Privacy
Spread Privacy
NISL@THU
NISL@THU
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Last Watchdog
The Last Watchdog
S
Schneier on Security
Engineering at Meta
Engineering at Meta
Google Online Security Blog
Google Online Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
Cyberwarzone
Cyberwarzone
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
A
Arctic Wolf
博客园 - 聂微东
I
Intezer
腾讯CDC
罗磊的独立博客
T
Tailwind CSS Blog
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
L
Lohrmann on Cybersecurity
人人都是产品经理
人人都是产品经理
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 【当耐特】
P
Palo Alto Networks Blog
Simon Willison's Weblog
Simon Willison's Weblog
W
WeLiveSecurity
N
News and Events Feed by Topic
SecWiki News
SecWiki News
S
Security Affairs
T
Threat Research - Cisco Blogs
The GitHub Blog
The GitHub Blog
F
Fortinet All Blogs
T
Troy Hunt's Blog
N
News and Events Feed by Topic
P
Proofpoint News Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
酷 壳 – CoolShell
酷 壳 – CoolShell
M
MIT News - Artificial intelligence
I
InfoQ
Hacker News: Ask HN
Hacker News: Ask HN
T
The Blog of Author Tim Ferriss
Schneier on Security
Schneier on Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
D
Darknet – Hacking Tools, Hacker News & Cyber Security

Ryan Wang's Blog

使用 Meilisearch 提升 Halo 网站的搜索体验 在 Halo 中导入 Markdown 和 Word 文档 通过 1Panel MCP 自动部署静态网站 使用 Rspack 构建 Halo 插件的前端部分 探讨如何为开源项目提交 issue 归档 - Ryan Wang's Blog 新起点丨开源建站工具 Halo 发布 2.0 版本 Halo 1.5.0 发布 Halo 1.4.10 发布 生态公园 ~ 2021-03-27 Halo 1.4.6 发布 Halo 1.4.3 发布 Halo 1.4.0 发布 分享一个教科书式的提问方式 优雅的让 Halo 支持 webp 图片输出 记录优化 Vue 应用的首次加载速度 博客迁移到 Raspberry Pi Flarum 的安装与配置 使用 Caddy 自动部署 Gridea 博客
为 Docusaurus 添加 Shiki 代码高亮支持
Ryan Wang · 2025-10-29 · via Ryan Wang's Blog

本文记录 https://docs.halo.run 集成 Shiki 代码高亮的过程。

引言

从 Halo 1.x 开始,我们就一直在使用 Docusaurus 来构建 Halo 的文档。直到 Halo 2.21,我们已经累积了大量的文档,期间发现代码块高亮的问题难以解决。Docusaurus 默认使用 Prism.js 来渲染代码块,且几乎没有其他选择。而我们的文档中使用的一些语言或框架(如 Vue),Prism 并没有提供高亮支持,因此长期以来这些代码块都没有显示语法高亮。即使是 Prism 所支持的语言,渲染出来的语法高亮效果也不尽如人意。

直到后来了解到了 https://shiki.style/,一个较新的代码高亮库,基于与 VSCode 同源的 TextMate 代码高亮引擎,渲染出来的代码效果与 VSCode 一致,并且支持现在主流语言和框架代码的高亮。我认为这几乎是当前各方面最优的代码高亮库。后面我们也为 Halo 开发了 Shiki 代码高亮插件,表现十分良好。因此最近也考虑将 Halo 文档中的代码高亮渲染改为使用 Shiki。好在搜索一番后,已经有了一个广泛讨论的 issue:https://github.com/facebook/docusaurus/issues/9122,并且里面也有人提供了可行的方案。于是我按照这个方案并添加了一些额外功能,本文将记录实现的完整过程。如果你也在用 Docusaurus,并且对 Shiki 有需求,可以参考本文。

背景

  • 目前使用的 Docusaurus 版本是 3.8.0

安装依赖

Docusaurus 的 Markdown 引擎核心为 https://mdxjs.com/,而 MDX 内部基于 remarkrehype,因此 Docusaurus 实际上预留了可以添加 rehype 插件的配置,所以可以直接使用 Shiki 官方的 rehype 插件

pnpm add @shikijs/rehype shiki -D

添加配置

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
// [!code ++:11]
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
                // or
                // langs: ['js', 'ts']
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

其中,langs 可以只填写所需的语言列表,我这里为了省事直接添加 Shiki 所有语言,主要是因为文档太多,已经懒得去统计用到了哪些语言。

此外,theme 也可以指定多主题,如果需要让文档的暗色和亮色模式下代码块的主题不同,可以按照下面的方式更改:

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
// [!code --]
                theme: "catppuccin-mocha",
// [!code ++:4]
                themes: {
                  light: "github-light",
                  dark: "github-dark"
                },
                langs: Object.keys(bundledLanguages),
                // or
                // langs: ['js', 'ts']
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

module.exports = config;

然后在 custom.css 中添加:

[data-theme="dark"] pre {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
}
[data-theme="dark"] pre span {
  color: var(--shiki-dark) !important;
}

由于我期望亮色和暗色模式下都使用暗色的代码块主题,所以没有添加多主题配置。

组件覆盖

由于需要完全让 @shikijs/rehype 接管 Markdown 文档中的代码块渲染,我们需要覆盖 Docusaurus 内部 Pre/Code 的组件,避免被默认的 Prism 处理。Docusaurus 默认提供了 CLI 用于导出 Docusaurus 主题中的组件。

npx docusaurus swizzle @docusaurus/theme-classic MDXComponents/Code --typescript --eject

然后打开 src/theme/MDXComponents/Code.tsx 并修改为:

import type { ComponentProps, ReactNode } from "react";
import React from "react";
import CodeInline from "@theme/CodeInline";
import type { Props } from "@theme/MDXComponents/Code";

function shouldBeInline(props: Props) {
  return (
    // empty code blocks have no props.children,
    // see https://github.com/facebook/docusaurus/pull/9704
    typeof props.children !== "undefined" &&
    React.Children.toArray(props.children).every(
      (el) => typeof el === "string" && !el.includes("\n")
    )
  );
}

// [!code ++:3]
function CodeBlock(props: ComponentProps<"code">): JSX.Element {
  return <code {...props} />;
}

export default function MDXCode(props: Props): ReactNode {
  return shouldBeInline(props) ? (
    <CodeInline {...props} />
  ) : (
    <CodeBlock {...(props as ComponentProps<typeof CodeBlock>)} />
  );
}
npx docusaurus swizzle @docusaurus/theme-classic MDXComponents/Pre --typescript --eject

然后打开 src/theme/MDXComponents/Pre.tsx 并修改为:

import React, { type ReactNode } from "react";
import type { Props } from "@theme/MDXComponents/Pre";
export default function MDXPre(props: Props): ReactNode | undefined {
  return <pre {...props} />;
}

小插曲:当时到了这一步的时候,突然意识到似乎可以复用之前为 Halo 开发 Shiki 插件时发布的 NPM 包(@halo-dev/shiki-code-element)。因为这个包封装了一个 Web Component,所以肯定可以用在这里,只需要在 pre 标签外包裹一个 shiki-code 即可。尝试了一下确实可行,但这样就必须在客户端渲染了。虽然可行,但始终不如在构建阶段就渲染好。虽然可以尝试使用 Lit SSR,但考虑到文档中有一些代码块使用了 Title Meta,而目前 @halo-dev/shiki-code-element 还不支持,所以放弃了这个方案。

完成这一步之后,就可以尝试启动开发服务器了。不出意外的话,代码块就可以正常使用 Shiki 来渲染了。

添加标题支持

原来 Docusaurus 的默认方案是支持为代码块添加顶部标题的,切换到 Shiki 之后,这一部分需要自行实现,以下是具体步骤:

首先为 Shiki 添加一个自定义的 Transformer,用于解析标题的书写语法和添加代码块参数:

创建 src/shiki/meta-transformer.js

function parseTitleFromMeta(meta) {
  if (!meta) {
    return "";
  }
  const kvList = meta.split(" ").filter(Boolean);
  for (const item of kvList) {
    const [k, v = ""] = item.split("=").filter(Boolean);
    if (k === "title" && v.length > 0) {
      return v.replace(/["'`]/g, "");
    }
  }
  return "";
}

export function transformerAddMeta() {
  return {
    name: "shiki-transformer:add-meta",
    pre(pre) {
      const title = parseTitleFromMeta(this.options.meta?.__raw);
      if (title.length > 0) {
        pre.properties = {
          ...pre.properties,
          "data-title": title,
        };
      }
      return pre;
    },
  };
}

然后修改配置:

const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");
const { transformerAddMeta } = require("./src/shiki/meta-transformer");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
// [!code ++:3]
                transformers: [
                  transformerAddMeta(),
                ]
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

修改 Pre.tsx 显示标题:

import React, { type ReactNode } from "react";
import type { Props } from "@theme/MDXComponents/Pre";

type PreWithDataTitle = Props & { "data-title"?: string };

export default function MDXPre(props: Props): ReactNode | undefined {
  const title = props["data-title"];
  return (
    <div
      style={{
        ...props.style,
        borderRadius: "var(--ifm-pre-border-radius)",
      }}
      className="shiki-code-wrapper"
    >
      {title && (
        <div className="shiki-code-header">
          <span>{title}</span>
        </div>
      )}
      <div className="shiki-code-content">
        <pre {...props} ref={preRef} />
      </div>
    </div>
  );
}

这里还修改了 MDXPre 标签的组件结构,为后续的功能做准备,其中最外层的 div 添加的 style 属性来自于 Shiki 渲染结果的 pre 标签的样式,包含背景色和字体默认颜色。

最后我们需要为自定义的 MDXPre 组件结构添加样式,这里为了让结构看起来更清晰,我引入了 SASS 插件:

# 安装所需依赖
pnpm add docusaurus-plugin-sass sass -D

修改配置文件:

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        // ...
        theme: {
// [!code --]
          customCss: require.resolve("./src/css/custom.css"),
// [!code ++:4]
          customCss: [
            require.resolve("./src/css/custom.css"),
            require.resolve("./src/css/shiki.scss"),
          ],
        },
      }),
    ],
  ],
// [!code ++]
  plugins: [require.resolve("docusaurus-plugin-sass")],
};

然后创建 src/css/shiki.scss

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }
}

这样就支持使用原有的语法为代码块添加标题了。

显示行号

Shiki 原生并不支持在渲染的 HTML 结果中包含行号信息,但社区中有人提供了一种使用纯 CSS 实现的方案,详见:https://github.com/shikijs/shiki/issues/3#issuecomment-830564854

修改 shiki.scss

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }
// [!code ++:36]
  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end
      }
    }
  }
}

这样就可以默认为所有代码块添加行号显示了。

复制按钮

Docusaurus 默认的代码块有复制按钮,改为 Shiki 之后这部分也需要自行实现,以下是具体步骤:

修改 src/theme/MDXComponents/Pre.tsx

import React, { type ReactNode, useRef, useState } from "react";
import type { Props } from "@theme/MDXComponents/Pre";

type PreWithDataTitle = Props & { "data-title"?: string };

export default function MDXPre(props: PreWithDataTitle): ReactNode | undefined {
  const title = props["data-title"];
// [!code ++:11]
  const preRef = useRef<HTMLPreElement>(null);
  const [copied, setCopied] = useState(false);

  const handleCopy = () => {
    const code = preRef.current?.innerText || preRef.current?.textContent || "";

    copyText(code, () => {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    });
  };

  return (
    <div
      style={{
        ...props.style,
        borderRadius: "var(--ifm-pre-border-radius)",
      }}
      className="shiki-code-wrapper"
    >
      {title && (
        <div className="shiki-code-header">
          <span>{title}</span>
        </div>
      )}
      <div className="shiki-code-content">
// [!code ++:8]
        <button
          className="shiki-code-copy-button"
          onClick={handleCopy}
          title={copied ? "已复制!" : "复制代码"}
          style={{ ...props.style }}
        >
          <i className={copied ? "tabler--check" : "tabler--copy"}></i>
        </button>
        <pre {...props} ref={preRef} />
      </div>
    </div>
  );
}

// [!code ++:24]
export function copyText(text: string, cb: () => void) {
  if (navigator.clipboard) {
    navigator.clipboard.writeText(text).then(() => {
      cb();
    });
  } else {
    const textArea = document.createElement("textarea");
    textArea.value = text;
    textArea.style.position = "fixed";
    textArea.style.opacity = "0";
    document.body.appendChild(textArea);
    textArea.focus();
    textArea.select();
    try {
      const successful = document.execCommand("copy");
      if (successful) {
        cb();
      }
    } catch (err) {
      console.error("Fallback: Oops, unable to copy", err);
    }
    document.body.removeChild(textArea);
  }
}

添加样式:

.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }

  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end
      }
    }
// [!code ++:15]
    .shiki-code-copy-button {
      position: absolute;
      top: 0.5rem;
      right: 0.5rem;
      opacity: 0;
      z-index: 2;
      width: 2rem;
      height: 2rem;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0.4rem;
      border: none;
      cursor: pointer;
    }
  }
// [!code ++:33]
  &:hover {
    .shiki-code-copy-button {
      opacity: 1;
    }
  }

  .tabler--copy {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z'/%3E%3Cpath d='M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1'/%3E%3C/g%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }

  .tabler--check {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m5 12l5 5L20 7'/%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }
}

这样就可以在鼠标悬停到代码块时显示复制按钮了。

集成必要的 Transformer

Shiki 官方提供了一些非常有用的 Transformers,比如行高亮、代码对比等,这里根据自身需要添加即可。

pnpm add -D @shikijs/transformers
const { bundledLanguages } = require("shiki");
const { default: rehypeShiki } = require("@shikijs/rehype");
const { transformerAddMeta } = require("./src/shiki/meta-transformer");
// [!code ++:4]
const { transformerMetaHighlight } = require("@shikijs/transformers");
const { transformerNotationDiff } = require("@shikijs/transformers");
const { transformerNotationFocus } = require("@shikijs/transformers");
const { transformerNotationErrorLevel } = require("@shikijs/transformers");

/** @type {import('@docusaurus/types').Config} */
const config = {
  // ...
  presets: [
    [
      "classic",
      /** @type {import('@docusaurus/preset-classic').Options} */
      ({
        docs: {
          // ...
          beforeDefaultRehypePlugins: [
            [
              rehypeShiki,
              {
                theme: "catppuccin-mocha",
                langs: Object.keys(bundledLanguages),
                transformers: [
// [!code ++:8]
                  // 行高亮,使用 Meta 信息的方式,比如 ```java {1}
                  transformerMetaHighlight(),
                  // 代码对比,使用注释的方式
                  transformerNotationDiff(),
                  // 行聚焦,使用注释的方式
                  transformerNotationFocus(),
                  // 行高亮的错误和警告变体,使用注释的方式
                  transformerNotationErrorLevel(),
                  transformerAddMeta(),
                ]
              },
            ],
          ],
          // ...
        },
      }),
    ],
  ],
  // ...
};

这些 Transformers 只是为对应的行添加了 class,我们需要自行实现样式。以下是完整的 shiki.scss

点击查看 shiki.scss
.shiki-code-wrapper {
  overflow: hidden;
  margin-bottom: var(--ifm-leading);
  color-scheme: dark;

  .shiki-code-header {
    padding: 0.5rem 0.75rem;
    border-bottom: 1px solid var(--ifm-color-gray-700);
    font-size: var(--ifm-code-font-size);
  }

  .shiki-code-content {
    position: relative;

    pre {
      position: relative;
      padding: 0.75rem;
      margin: 0;
      border-radius: initial;
      code {
        z-index: 1;
        display: block;
        width: max-content;
        position: relative;
        min-width: 100%;

        counter-reset: step;
        counter-increment: step 0;

        .line {
          position: relative;
        }

        // line numbers start
        .line::before {
          content: counter(step);
          counter-increment: step;
          width: 0.6rem;
          margin-right: 1.1rem;
          display: inline-block;
          text-align: right;
          color: rgba(115, 138, 148, 0.5);
          user-select: none;
        }

        .line:last-child:empty::before {
          content: none;
          counter-increment: none;
        }
        // line numbers end

        // highlighted lines start
        .highlighted {
          width: 100%;
          display: inline-block;
          position: relative;
        }

        .highlighted::after {
          content: "";
          position: absolute;
          top: 0;
          bottom: 0;
          left: -0.75rem;
          right: -0.75rem;
          background: rgba(101, 117, 133, 0.16);
          border-left: 1px solid rgba(34, 197, 94, 0.8);
          z-index: 0;
        }

        .highlighted.error::after {
          background: rgba(244, 63, 94, 0.16) !important;
        }

        .highlighted.warning::after {
          background: rgba(234, 179, 8, 0.16) !important;
        }
        // highlighted lines end
      }

      // focus line start
      &.has-focused .line:not(.focused) {
        opacity: 0.7;
        filter: blur(0.095rem);
        transition: filter 0.35s, opacity 0.35s;
      }

      &.has-focused:hover .line:not(.focused) {
        opacity: 1;
        filter: blur(0);
      }
      // focus line end

      // diff start
      &.has-diff .diff {
        width: 100%;
        display: inline-block;
        position: relative;
      }

      &.has-diff .diff.remove::before {
        content: "-";
      }

      &.has-diff .diff.add::before {
        content: "+";
      }

      &.has-diff .diff.remove::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: -0.75rem;
        right: -0.75rem;
        background: rgb(239 68 68 / 0.15);
        border-left: 1px solid rgb(239 68 68 / 0.8);
        z-index: -1;
      }

      &.has-diff .diff.add::after {
        content: "";
        position: absolute;
        top: 0;
        bottom: 0;
        left: -0.75rem;
        right: -0.75rem;
        background: rgb(34 197 94 / 0.15);
        border-left: 1px solid rgb(34 197 94 / 0.8);
        z-index: -1;
      }
      // diff end
    }

    .shiki-code-copy-button {
      position: absolute;
      top: 0.5rem;
      right: 0.5rem;
      opacity: 0;
      z-index: 2;
      width: 2rem;
      height: 2rem;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0.4rem;
      border: none;
      cursor: pointer;
    }
  }

  &:hover {
    .shiki-code-copy-button {
      opacity: 1;
    }
  }

  .tabler--copy {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cg fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M7 9.667A2.667 2.667 0 0 1 9.667 7h8.666A2.667 2.667 0 0 1 21 9.667v8.666A2.667 2.667 0 0 1 18.333 21H9.667A2.667 2.667 0 0 1 7 18.333z'/%3E%3Cpath d='M4.012 16.737A2 2 0 0 1 3 15V5c0-1.1.9-2 2-2h10c.75 0 1.158.385 1.5 1'/%3E%3C/g%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }

  .tabler--check {
    display: inline-block;
    width: 28px;
    height: 28px;
    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m5 12l5 5L20 7'/%3E%3C/svg%3E");
    background-color: currentColor;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
  }
}

至此,一个比较好看、功能丰富的代码高亮改造方案就完成了,具体修改代码也可以查阅 Halo 文档的 PR:https://github.com/halo-dev/docs/pull/521,也可以访问 Halo 文档查看修改之后的效果。

参考资料