Code 代码
Code 用等宽字体展示源代码,可以是代码块,也可以嵌在一行文字里。高亮用的是 Shiki,但 raft-ui 不打包 Shiki:由你创建高亮器并传入,正则引擎由你选,也只会带上用到的语言。没有传入时,Code 以纯文本显示。
配置 Shiki
安装 shiki,用它的细粒度 core 版本创建高亮器:一个正则引擎、需要的语言,以及 raft-ui 的 codeTheme。然后传给应用根部附近的 ConfigProvider。它下面的每个 Code 都会使用它;单个 Code 上的 highlighter 属性会覆盖它。
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、-changed | diff 行 | 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 会引入自己的样式表。
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 传给它:
// 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> 的子元素就是高亮好的各行:
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
| 名称 | 类型 | 默认值 | 说明 | 版本 |
|---|---|---|---|---|
children | ReactNode | undefined | 预先高亮并已渲染的各行,例如来自 @shikijs/rehype。原样渲染,优先于 tokens 和高亮器。 | 未发布 |
code | string | undefined | 源代码文本。有 children 或 tokens 时只用于复制。 | 未发布 |
copyable | boolean | true | 显示复制源代码文本(经过 trim)的按钮。inline 时无效。 | 未发布 |
highlighter | CodeHighlighter | undefined | 只给这个 Code 用的 Shiki 高亮器,需加载 codeTheme。默认取 ConfigProvider 上的。 | 未发布 |
inline | boolean | false | 嵌在一行文字里,而不是显示为代码块。 | 未发布 |
language | string | undefined | 已加载进 highlighter 的语言,语言名或别名。 | 未发布 |
showLineNumbers | boolean | false | 显示行号栏。inline 或 wordWrap 时无效。 | 未发布 |
tokens | CodeToken[][] | undefined | 用 codeTheme 经 codeToTokens 预先高亮的 Shiki token。优先于高亮器。 | 未发布 |
trim | boolean | true | 去掉 code 开头和结尾的空白。children 和 tokens 原样渲染。 | 未发布 |
wordWrap | boolean | false | 长行折行显示,而不是横向滚动。 | 未发布 |
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 元素,按元素渲染:请用可信的输出构造它们,例如你自己的构建步骤。