Skip to content

代码高亮

Velite 不内置代码高亮功能,因为并非所有内容都包含代码,而且语法高亮通常需要自定义样式。但您可以使用构建时插件或客户端高亮库轻松实现。

TIP

推荐使用构建时代码高亮方案,因为这种方式更快更稳定。

@shikijs/rehype

shiki 是一款为代码块设计的精美语法高亮器。

sh
$ npm install @shikijs/rehype shiki
sh
$ pnpm add @shikijs/rehype shiki
sh
$ yarn add @shikijs/rehype shiki

velite.config.ts 中配置:

ts
import rehypeShiki from '@shikijs/rehype'
import { defineConfig } from 'velite'

export default defineConfig({
  // 如果使用 mdx 则改为 `mdx`
  markdown: {
    rehypePlugins: [
      [
        rehypeShiki as any, // eslint-disable-line @typescript-eslint/no-explicit-any
        { theme: 'one-dark-pro' }
      ]
    ]
  }
})

Transformers(转换器)

Shiki 提供了 transformers 选项来自定义语法高亮的输出。您可以使用它来添加行高亮、行号等功能。

sh
$ npm install @shikijs/transformers
sh
$ pnpm add @shikijs/transformers
sh
$ yarn add @shikijs/transformers
ts
import rehypeShiki from '@shikijs/rehype'
import { transformerNotationDiff, transformerNotationErrorLevel, transformerNotationFocus, transformerNotationHighlight } from '@shikijs/transformers'
import { defineConfig } from 'velite'

export default defineConfig({
  // 如果使用 mdx 则改为 `mdx`
  markdown: {
    rehypePlugins: [
      [
        rehypeShiki as any, // eslint-disable-line @typescript-eslint/no-explicit-any
        {
          transformers: [
            transformerNotationDiff({ matchAlgorithm: 'v3' }),
            transformerNotationHighlight({ matchAlgorithm: 'v3' }),
            transformerNotationFocus({ matchAlgorithm: 'v3' }),
            transformerNotationErrorLevel({ matchAlgorithm: 'v3' })
          ]
        }
      ]
    ]
  }
})

复制按钮

Shiki 默认不提供复制按钮,但您可以通过构建时插件添加。

ts
import rehypeShiki from '@shikijs/rehype'
import { defineConfig } from 'velite'

const transformerCopyButton = (): ShikiTransformer => ({
  name: 'copy-button',
  pre(node) {
    node.children.push({
      type: 'element',
      tagName: 'button',
      properties: {
        type: 'button',
        className: 'copy',
        title: 'Copy to clipboard',
        onclick: `
          navigator.clipboard.writeText(this.previousSibling.textContent),
          this.className='copied',
          this.title='Copied!',
          setTimeout(()=>this.className='copy',5000)`.replace(/\s+/g, '')
      },
      children: [
        {
          type: 'element',
          tagName: 'svg',
          properties: {
            viewBox: '0 0 24 24',
            fill: 'none',
            stroke: 'currentColor',
            strokeWidth: '1.5',
            strokeLinecap: 'round',
            strokeLinejoin: 'round'
          },
          children: [
            {
              type: 'element',
              tagName: 'rect',
              properties: {
                width: '8',
                height: '4',
                x: '8',
                y: '2',
                rx: '1',
                ry: '1'
              },
              children: []
            },
            {
              type: 'element',
              tagName: 'path',
              properties: {
                d: 'M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'
              },
              children: []
            },
            {
              type: 'element',
              tagName: 'path',
              properties: {
                class: 'check',
                d: 'm9 14 2 2 4-4'
              },
              children: []
            }
          ]
        }
      ]
    })
  }
})

export default defineConfig({
  // 如果使用 mdx 则改为 `mdx`
  markdown: {
    rehypePlugins: [
      [
        rehypeShiki as any, // eslint-disable-line @typescript-eslint/no-explicit-any
        {
          transformers: [transformerCopyButton()]
        }
      ]
    ]
  }
})
css
pre.shiki {
  @apply max-h-(--max-height,80svh) relative flex flex-col overflow-hidden p-0;

  code {
    @apply grid overflow-auto py-5;
  }

  .line {
    @apply relative px-5;
  }

  button {
    @apply hover:opacity-100! absolute right-3 top-3 flex cursor-pointer select-none items-center justify-center rounded-md bg-slate-600 text-sm font-medium text-white opacity-0 shadow outline-0 transition;

    svg {
      @apply m-2 size-5;
    }

    .check {
      @apply opacity-0 transition-opacity;
    }

    &.copied {
      @apply opacity-100!;

      &::before {
        @apply border-r border-[#0002] p-2 px-2.5 content-['Copied!'];
      }

      .check {
        @apply opacity-100;
      }
    }
  }

  &:hover {
    button {
      @apply opacity-80;
    }
  }
}

如需更多实用转换器,请参阅 shiki 转换器文档。

rehype-pretty-code

rehype-pretty-code 是一个用于格式化代码块的 rehype 插件。

sh
$ npm install rehype-pretty-code shiki
sh
$ pnpm add rehype-pretty-code shiki
sh
$ yarn add rehype-pretty-code shiki

velite.config.js 中配置:

js
import rehypePrettyCode from 'rehype-pretty-code'
import { defineConfig } from 'velite'

export default defineConfig({
  // 如果使用 mdx 则改为 `mdx`
  markdown: {
    rehypePlugins: [rehypePrettyCode]
  }
})

rehype-pretty-code 会为语法高亮创建适当的 HTML 结构,您随后可以按需添加样式。以下是一个样式表示例:

css
[data-rehype-pretty-code-figure] pre {
  @apply px-0;
}

[data-rehype-pretty-code-figure] code {
  @apply text-sm !leading-loose md:text-base;
}

[data-rehype-pretty-code-figure] code[data-line-numbers] {
  counter-reset: line;
}

[data-rehype-pretty-code-figure] code[data-line-numbers] > [data-line]::before {
  counter-increment: line;
  content: counter(line);
  @apply mr-4 inline-block w-4 text-right text-gray-500;
}

[data-rehype-pretty-code-figure] [data-line] {
  @apply border-l-2 border-l-transparent px-3;
}

[data-rehype-pretty-code-figure] [data-highlighted-line] {
  background: rgba(200, 200, 255, 0.1);
  @apply border-l-blue-400;
}

[data-rehype-pretty-code-figure] [data-highlighted-chars] {
  @apply rounded bg-zinc-600/50;
  box-shadow: 0 0 0 4px rgb(82 82 91 / 0.5);
}

[data-rehype-pretty-code-figure] [data-chars-id] {
  @apply border-b-2 p-1 shadow-none;
}

更多细节请参考示例

@shikijs/rehype

sh
$ npm install @shikijs/rehype
sh
$ pnpm add @shikijs/rehype
sh
$ yarn add @shikijs/rehype

velite.config.js 中配置:

js
import rehypeShiki from '@shikijs/rehype'
import { defineConfig } from 'velite'

export default defineConfig({
  // 如果使用 mdx 则改为 `mdx`
  markdown: {
    rehypePlugins: [[rehypeShiki, { theme: 'nord' }]]
  }
})

TIP

Velite 打包了大多数类型的第三方模块,这会导致 @shikijs/rehype 的类型声明不兼容,但您可以放心使用

velite.config.ts 中:

js
import rehypeShiki from '@shikijs/rehype'
import { defineConfig } from 'velite'

  markdown: {// 如果使用 mdx 则改为 `mdx`
export default defineConfig({
    rehypePlugins: [[rehypeShiki as any, { theme: 'nord' }]]
  }
})

rehype-highlight

使用 lowlight 为代码提供语法高亮。

sh
$ npm install rehype-highlight
sh
$ pnpm add rehype-highlight
sh
$ yarn add rehype-highlight

velite.config.js 中配置:

js
import rehypeHighlight from 'rehype-highlight'
import { defineConfig } from 'velite'

export default defineConfig({
  // 如果使用 mdx 则改为 `mdx`
  markdown: {
    rehypePlugins: [rehypeHighlight]
  }
})

客户端方案

您可以使用 prismjsshiki 在客户端高亮代码。客户端高亮不会给 Velite 增加构建开销。

例如:

js
import { codeToHtml } from 'https://esm.sh/shikiji'

Array.from(document.querySelectorAll('pre code[class*="language-"]')).map(async block => {
  block.parentElement.outerHTML = await codeToHtml(block.textContent, { lang: block.className.slice(9), theme: 'nord' })
})

TIP

如果您有大量需要语法高亮的文档,推荐使用客户端方案。因为语法高亮和解析可能非常耗时,会极大影响 Velite 的构建速度。

Distributed under the MIT License.