跳到正文

Code 代码

Code 用等宽字体展示源代码,可以是代码块,也可以嵌在一行文字里。高亮用的是 Shiki,但 raft-ui 不打包 Shiki:由你创建高亮器并传入,正则引擎由你选,也只会带上用到的语言。没有传入时,Code 以纯文本显示。

配置 Shiki

安装 shiki,用它的细粒度 core 版本创建高亮器:一个正则引擎、需要的语言,以及 raft-ui 的 codeTheme。然后传给应用根部附近的 ConfigProvider。它下面的每个 Code 都会使用它;单个 Code 上的 highlighter 属性会覆盖它。

tsx
import { ConfigProvider, codeTheme } from "raft-ui"; import { createHighlighterCoreSync } from "shiki/core"; import { createJavaScriptRegexEngine } from "shiki/engine/javascript"; import javascript from "shiki/langs/javascript.mjs"; const highlighter = createHighlighterCoreSync({ engine: createJavaScriptRegexEngine(), langs: [javascript], themes: [codeTheme], }); export function App() { return <ConfigProvider highlighter={highlighter}>{/* … */}</ConfigProvider>; }

Code 在渲染时调用高亮器的 codeToTokens,把得到的 token 渲染成 <span>,所以只要高亮器已经就绪,服务端渲染时也会高亮。

颜色

codeTheme 是 Shiki 的 CSS 变量主题,和 shiki/core 里 createCssVariablesTheme({ name: "raft-ui", variablePrefix: "--r-code-" }) 返回的对象相同。它的 token 颜色是 var(--r-code-token-keyword)、var(--r-code-token-string) 等,每个 raft-ui 主题都用自己的颜色角色定义了这些变量。因此代码会跟随主题和亮暗模式变化,不需要重新高亮,也不需要 Shiki 的配色主题或样式表。想给某个角色换色,用 themeOverrides 覆盖它的变量,例如 --r-code-token-keyword。

变量Shiki token颜色角色
--r-code-foreground普通文本label-primary
--r-code-token-keyword关键字、存储类型、运算符text-accent
--r-code-token-string、--r-code-token-string-expression字符串、模板、标签text-success
--r-code-token-constant数字、常量、属性text-warning
--r-code-token-function函数、类型、属性名text-info
--r-code-token-parameter参数text-primary
--r-code-token-comment注释label-tertiary(Brutal:label-secondary)
--r-code-token-punctuation标点label-secondary
--r-code-token-inserted、-deleted、-changeddiff 行text-success、text-error、text-warning

Brutal 还会把关键字和函数加粗,注释保持正体;Elegant 的注释用斜体。

引擎与语言

Code 只高亮已经加载进高亮器的 language,用语言名或它的别名都可以(javascript 也可以写 js)。其他语言以纯文本显示,没有加载 codeTheme 的高亮器也一样。每种语法从 shiki/langs/<name>.mjs 引入。

JavaScript 正则引擎是同步的,所以 createHighlighterCoreSync 能立刻得到高亮器。Oniguruma 引擎(shiki/engine/oniguruma 配合 import("shiki/wasm"))和 VS Code 的结果完全一致,但要加载 WebAssembly,只能用异步的 createHighlighterCore:在你传入高亮器之前,Code 以纯文本显示。请在传入高亮器之前加载好语言:Code 在 highlighter、code 或 language 变化时重新高亮,之后再往同一个高亮器里加载语言不会触发重新高亮。

预先高亮

静态站点生成器(Astro、配合 @shikijs/rehype 的 MDX)和服务端可以在构建时运行 Shiki,浏览器就完全不用加载 Shiki。Code 可以用两种方式展示它们的输出,都不需要高亮器:

  • tokens:codeToTokens(code, { lang, theme: codeTheme.name }).tokens 返回的各行 token。它们是纯数据,构建步骤可以写成 JSON,服务端组件也可以传给客户端。Code 渲染它们的方式和渲染高亮器的结果完全相同。
  • children:已经渲染成元素的各行,例如 MDX 把 @shikijs/rehype 输出的 <pre><code> 编译后的结果。Code 原样渲染它们,外框不变。

children 优先于 tokens,tokens 优先于高亮器。复制按钮复制的、行号栏计数的,是 token 或 children 的文本:children 的文本从 React 树里的字符串读出,所以服务端渲染时也一样。如果这段文本不是源代码,例如 children 是自己渲染文本的组件,就同时传入 code,复制的是 code。trim 只作用于 code。

构建时的主题颜色

要让构建时的输出跟随 raft-ui 主题,同样用 codeTheme 高亮:每个颜色都是 var(--r-code-*) 引用,由当前主题填入。再加上 codeTransformer,这是一个 Shiki transformer,给每个 token 的 <span> 加上 Code 给自己的 token 加的类名(r-code__token r-code__token--keyword),各主题的字重和字形(Brutal 的粗体关键字、Elegant 的斜体注释)都挂在这些类名上。两者都从 raft-ui/shiki 引入:它不含 React 和 CSS,Node 配置文件也能加载;而 raft-ui 会引入自己的样式表。

ts
import { codeTheme, codeTransformer } from "raft-ui/shiki"; import { createHighlighter } from "shiki"; const highlighter = await createHighlighter({ langs: ["tsx"], themes: [codeTheme] }); // 用于 `tokens`: const { tokens } = highlighter.codeToTokens(source, { lang: "tsx", theme: codeTheme.name }); // 用于 `children`,经由 HAST 或 HTML: const html = highlighter.codeToHtml(source, { lang: "tsx", theme: codeTheme.name, transformers: [codeTransformer], });

使用 @shikijs/rehype 时传入同样的选项:{ theme: codeTheme, transformers: [codeTransformer] }。

MDX

Vite 上的 MDX(@mdx-js/rollup)在编译页面时运行 @shikijs/rehype。把 raft-ui 的主题和 transformer 传给它:

ts
// vite.config.ts import mdx from "@mdx-js/rollup"; import rehypeShiki from "@shikijs/rehype"; import { codeTheme, codeTransformer } from "raft-ui/shiki"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ mdx({ providerImportSource: "@mdx-js/react", rehypePlugins: [[rehypeShiki, { theme: codeTheme, transformers: [codeTransformer] }]], }), ], });

然后把 MDX 的 pre 映射到 Code。它的子元素是 <code>,<code> 的子元素就是高亮好的各行:

tsx
import { MDXProvider } from "@mdx-js/react"; import { isValidElement } from "react"; import type { ReactNode } from "react"; import { Code } from "raft-ui"; function Pre({ children }: { children?: ReactNode }) { const lines = isValidElement<{ children?: ReactNode }>(children) ? children.props.children : children; return <Code>{lines}</Code>; } const components = { pre: Pre }; export function Page({ children }: { children: ReactNode }) { return <MDXProvider components={components}>{children}</MDXProvider>; }

本站用自己的 rehype 步骤做了同样的事。Astro(markdown.shikiConfig)和 Next.js MDX(rehypePlugins 里的 @shikijs/rehype)接受同样的 theme 和 transformers。

演示

基础用法

ConfigProvider 把高亮器传给下面的每个 Code。开头和结尾的空行会被去掉;设置 trim={false} 可以保留。

行内

inline 让代码嵌在一行文字里,字号随周围文字缩放。

自动换行

长行在 ScrollArea 里横向滚动。wordWrap 让它们折行显示。

行号

showLineNumbers 加上行号栏,长行横向滚动时它保持不动。行号不会被读屏读出,也不会随代码一起复制。

复制

代码块右上角有一个小号 CopyButton,悬停或聚焦代码块时显示,触屏设备上一直显示。它复制源代码文本(经过 trim),并显示对勾两秒。设置 copyable={false} 去掉它;行内代码没有复制按钮。

异步加载 Shiki

使用 Oniguruma 引擎的高亮器是异步创建的,这里 Go 的语法也是按需加载的。就绪之前,Code 显示纯文本。

预先高亮的 token

tokens 接收在别处由 Shiki 的 codeToTokens 得到的各行 token,这里按构建步骤保存的样子直接写出。不涉及高亮器,行号和复制都和使用 code 时一样。

预先渲染的 children

children 接收已经是元素的各行,这里把一个 pre 映射到 Code,就像 MDX 站点映射构建时由 Shiki 生成的 <pre><code> 那样。复制按钮复制它们的文本。

不使用高亮器

没有高亮器,或者 language 没有加载时,代码以纯等宽文本显示。

Props

名称类型默认值说明版本
childrenReactNodeundefined预先高亮并已渲染的各行,例如来自 @shikijs/rehype。原样渲染,优先于 tokens 和高亮器。未发布
codestringundefined源代码文本。有 children 或 tokens 时只用于复制。未发布
copyablebooleantrue显示复制源代码文本(经过 trim)的按钮。inline 时无效。未发布
highlighterCodeHighlighterundefined只给这个 Code 用的 Shiki 高亮器,需加载 codeTheme。默认取 ConfigProvider 上的。未发布
inlinebooleanfalse嵌在一行文字里,而不是显示为代码块。未发布
languagestringundefined已加载进 highlighter 的语言,语言名或别名。未发布
showLineNumbersbooleanfalse显示行号栏。inline 或 wordWrap 时无效。未发布
tokensCodeToken[][]undefined用 codeTheme 经 codeToTokens 预先高亮的 Shiki token。优先于高亮器。未发布
trimbooleantrue去掉 code 开头和结尾的空白。children 和 tokens 原样渲染。未发布
wordWrapbooleanfalse长行折行显示,而不是横向滚动。未发布

CodeHighlighter 是 Shiki 的 HighlighterCore 中 Code 用到的部分(codeToTokens),createHighlighterCore 或 createHighlighterCoreSync 创建的高亮器都符合。raft-ui 只导出它的类型,运行时从不引入 Shiki。

raft-ui/shiki 导出 codeTheme 和 codeTransformer(raft-ui 也导出它们),不含 React 和 CSS,供构建步骤使用。

安全性

源代码只会作为文本放进 <span>,无论是否高亮,都不会以 HTML 插入。任何 code 字符串或 tokens 都可以安全展示,包括不可信的输入。children 是 React 元素,按元素渲染:请用可信的输出构造它们,例如你自己的构建步骤。