Slidev 代码块行高亮实战:`{行号|阶段}` 语法解析、点击动画联动与源码实现
本文讲解 Slidev 中代码块的“行高亮”(Line Highlighting)能力:如何在 Markdown 代码围栏上用 {行号}、{2-3|5|all} 等语法静态或随点击动态地强调特定行、如何用 hide / none 控制代码块显隐,以及高亮如何在服务端被解析、在客户端与点击动画系统联动。读完后,你可以直接在自己幻灯片的代码块中编写可点击演进的行高亮脚本,并理解其从 Markdown 解析到 DOM 类名切换的完整链路。
基础用法:用 {} 指定高亮行号
在代码围栏的语言标识后面,把行号写在方括号 {} 内即可高亮对应行。行号默认从 1 开始计数:
```ts {2,3}
function add(
a: Ref<number> | number,
b: Ref<number> | number
) {
return computed(() => unref(a) + unref(b))
}
```
渲染后,第 2、3 行(两个参数声明)会被高亮。行号列表支持多种写法,从源码 packages/parser/src/utils.ts 中的 parseRangeString 函数可以看到完整规则:
1,3-5,8:单个行号用逗号(也接受分号)分隔,3-5表示行号区间,最终展开为[1, 3, 4, 5, 8];all或*:高亮全部行(等价于不写行号);none:不 high 亮任何一行;- 区间可以省略结尾,如
3-表示从第 3 行一直高亮到最后(实现中end为空时取total + 1); - 所有结果会去重、过滤掉超过总行数的行号并按升序排序(
uniq(indexes).filter(i => i <= total).sort(...))。
值得注意的是,parseRangeString 是 Slidev 的公共工具:除了代码行高亮,它还被用于打印/导出时的页面范围(packages/slidev/node/commands/export.ts)、演示者预览范围(packages/client/composables/useNav.ts)等,同一套 1,3-5,8 / all / none 语法在整个项目中是统一语义。
动态行高亮:用 | 切分点击阶段
想让高亮内容随演讲者点击而逐段变化时,用 | 把每个阶段分开,每一段就是一个独立的高亮集合:
```ts {2-3|5|all}
function add(
a: Ref<number> | number,
b: Ref<number> | number
) {
return computed(() => unref(a) + unref(b))
}
```
效果是:
- 初始(未点击):先高亮第 2–3 行,即
a: Ref<number> | number与b: Ref<number> | number两个参数声明; - 点击一次:改为只高亮第 5 行
return computed(() => unref(a) + unref(b)); - 再点击一次:
all表示整块代码全部高亮。
这种写法是“渐进式讲解代码”的标准套路:先展示函数签名,再点击聚焦到关键实现行,最后展开全貌。每一段阶段之间是完全替换关系,而不是累加——当前点击落在第几个阶段,就显示第几个阶段的高亮集合。
特殊值:hide 与 none
行号列表里还可以写入两个特殊关键字:
hide:让代码块整体隐藏,直到下一阶段再显示;none:不 high 亮任何一行。
```ts {hide|none}
function add(
a: Ref<number> | number,
b: Ref<number> | number
) {
return computed(() => unref(a) + unref(b))
}
```
初始状态代码块被隐藏,点击一次后进入 none 阶段——代码块出现但不高亮任何行,方便先让听众看到完整代码、再用后续阶段逐步聚焦。
从源码实现看(packages/client/builtin/CodeBlockWrapper.vue),hide 并不是简单地把 display 设为 none,而是复用点击动画系统的隐藏类 CLASS_VCLICK_HIDDEN 在包裹元素上切换,并且只有在 hide 阶段自己设置过隐藏后才由自己负责恢复(hiddenByRange 标记),以保证同一元素上的 v-click 指令仍能保持控制权。当当前阶段是 hide 时,高亮范围会自动“借用”下一阶段的 range 继续计算,避免出现闪烁的中间态。
从 Markdown 到组件:围栏语法的解析链路
{...} 语法为什么能生效?核心在服务端的代码块转换器 packages/slidev/node/syntax/codeblock/wrapper.ts。每个代码围栏的 info 字符串(语言 + 附加参数)都会匹配正则 RE_BLOCK_INFO:
// packages/slidev/node/syntax/codeblock/wrapper.ts
const RE_BLOCK_INFO = /^([\w'-]+)?(?:[ \t]*|[ \t][ \w\t'-]*)(?:\[([^\]]*)\])?[ \t]*(?:\{([\w,|\-*]+)\})?[ \t]*(\{[^}]*\})?(.*)$/
export default defineCodeblockTransformer(async ({ info, renderHighlighted }) => {
const [, lang = '', title = '', rangeStr = '', options, rest = ''] = info.match(RE_BLOCK_INFO) ?? []
const ranges = normalizeRangeStr(rangeStr)
const optionsProp = options ? `v-bind="${options}"` : ''
const code = await renderHighlighted({ info: `${lang} ${rest}` })
return `<CodeBlockWrapper ${optionsProp} title=${JSON.stringify(title)} :ranges='${JSON.stringify(ranges)}'>${escapeVueInCode(code)}</CodeBlockWrapper>`
})
这条链路把围栏行拆成五个部分,一次解释了 {...} 之外其他写法的位置关系:
| 语法片段 | 示例 | 含义 |
|---|---|---|
| 第一段 | ts |
语言标识,交给 Shiki 高亮 |
[...] |
[src/main.ts] |
代码块标题(title),渲染在代码块上方 |
{...}(第一个) |
{2,3}、`{2-3 |
5 |
{...}(第二个) |
{at:1}、{maxHeight:'200px'} |
Vue 属性,透传 v-bind 给组件 |
| 其余部分 | title、region:name 等 |
透传回 Shiki 的 info |
其中 normalizeRangeStr(packages/slidev/node/syntax/utils.ts)负责把 2-3|5|all 按 | 切分并 trim 成 ['2-3', '5', 'all'];而 Shiki 的真正语法高亮由 packages/slidev/node/syntax/shiki.ts 中的 markdown-it 插件完成,转换器链在 packages/slidev/node/syntax/codeblock/index.ts 中按 mermaid → plantUml → magicMove → monaco → wrapper 的顺序执行,普通代码块最终落到 wrapperTransformer 上,被包裹成 <CodeBlockWrapper> 组件。
客户端实现:点击系统驱动的 DOM 类名切换
CodeBlockWrapper(packages/client/builtin/CodeBlockWrapper.vue)是自动注入的组件,用户无需手动使用。挂载后,它做三件关键事:
- 注册点击:
clicks.calculateSince(props.at, props.ranges.length - 1)把该代码块的高亮阶段注册进幻灯片的点击上下文,at属性(默认'+1')决定从第几个点击开始; - 计算当前阶段:
index = Math.max(0, clicks.current - clicksInfo.start + 1),取ranges[index];超出阶段数后回落到finallyRange(默认取最后一个阶段,可用finally属性覆盖,默认值'last'); - 切换类名:调用 packages/client/logic/utils.ts 中的
updateCodeHighlightRange,对 Shiki 输出的每一行code > .line元素切换类名。
updateCodeHighlightRange 的实现值得注意——它对每一行同时切换新旧两套类名:
// packages/client/logic/utils.ts
token.classList.toggle('slidev-code-highlighted', isHighlighted)
token.classList.toggle('slidev-code-dishonored', !isHighlighted)
// for backward compatibility
token.classList.toggle('highlighted', isHighlighted)
token.classList.toggle('dishonored', !isHighlighted)
新旧类名并存是为了向后兼容:老幻灯片或第三方主题可能依赖 .highlighted / .dishonored。默认样式定义在 packages/client/styles/code.css:未高亮的行(dishonored)会被压暗到 opacity: 0.3 并禁止交互(pointer-events: none),而高亮行本身没有额外颜色——这意味着视觉上实际是“其余行变淡、目标行保持原色”,你可以在主题样式中自行给 .slidev-code .slidev-code-highlighted 补充背景或边框。
与其他行号参数的协作:startLine、maxHeight、lines
除了 ranges,CodeBlockWrapper 还接收几个与行相关的 props(packages/client/builtin/CodeBlockWrapper.vue),可以在第二个 {} 里透传:
// 组件 props 摘录
ranges: string[] // 行高亮阶段
finally: string | number // 超出阶段后的兜底 range,默认 'last'
startLine: number // 行号起始值,默认 1
lines: boolean // 是否显示行号,默认取 frontmatter 的 lineNumbers
at: string | number // 点击定位,默认 '+1'
maxHeight: string // 最大高度,设置后代码块可滚动
startLine:高亮计算是isHighlighted = highlights.includes(line + startLine),即行号从startLine开始。配合 Shiki 的行偏移配置,即使代码块视觉上从第 10 行开始显示,{10}依然指向代码的第一行逻辑位置;lines:控制.slidev-code-line-numbers类,默认继承全局lineNumbers配置,可用{lines:false}逐块关闭;maxHeight:设置后代码块出现滚动条,且客户端会自动把被高亮的行滚动进视野(scrollIntoView,高亮总高度超出容器时对齐顶部,否则居中)。这在大代码块 + 行高亮的组合场景下非常实用:
```ts {1|5|8-9}{maxHeight:'300px'}
// ... 较长的代码
```
与点击动画的定位系统联动
代码块高亮不是孤立功能,它嵌在 Slidev 统一的点击动画系统里。正如 docs/guide/animations.md 所述,at 支持相对值('+1')与绝对值(1),两者可以与 v-click 混合使用来控制触发时序。官方文档给出的典型用例是同步高亮两个代码块:
```js {1|2}{at:1}
1 + 1
'a' + 'b'
```
```js {1|2}{at:1}
= 2
= 'ab'
```
两个代码块都从第 1 次点击开始各走两阶段,左块显示表达式、右块显示结果,逐行同步演进。同理,{hide|...}{at:'3'} 可以让代码块先隐藏、在整页第三次点击时才登场。关于 v-click 的相对/绝对定位、enter/leave 区间等更完整的点击动画机制,可继续阅读 动画指南。
小结
Slidev 的行高亮把“演讲节奏”写进了 Markdown 围栏本身:
- 静态强调:
```ts {2,3},行号支持n、a-b、all/*、none以及逗号/分号分隔的混合写法; - 动态演进:
{2-3|5|all}用|切分阶段,与点击次数一一对应,可用hide/none控制整块显隐; - 进阶控制:第二个
{}可透传at、finally、startLine、lines、maxHeight等 props; - 实现链路:围栏 info 由 wrapper 转换器 解析为
<CodeBlockWrapper :ranges="...">,客户端 CodeBlockWrapper.vue 将每个阶段注册进点击系统,再通过 updateCodeHighlightRange 切换slidev-code-highlighted/slidev-code-dishonored类名,样式由 code.css 提供默认压暗效果。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00