首页
/ Slidev 代码块行高亮实战:`{行号|阶段}` 语法解析、点击动画联动与源码实现

Slidev 代码块行高亮实战:`{行号|阶段}` 语法解析、点击动画联动与源码实现

2026-09-07 17:36:50作者:宣利权Counsellor

本文讲解 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))
}
```

效果是:

  1. 初始(未点击):先高亮第 2–3 行,即 a: Ref<number> | numberb: Ref<number> | number 两个参数声明;
  2. 点击一次:改为只高亮第 5 行 return computed(() => unref(a) + unref(b))
  3. 再点击一次:all 表示整块代码全部高亮。

这种写法是“渐进式讲解代码”的标准套路:先展示函数签名,再点击聚焦到关键实现行,最后展开全貌。每一段阶段之间是完全替换关系,而不是累加——当前点击落在第几个阶段,就显示第几个阶段的高亮集合。

特殊值:hidenone

行号列表里还可以写入两个特殊关键字:

  • 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 给组件
其余部分 titleregion:name 透传回 Shiki 的 info

其中 normalizeRangeStrpackages/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 类名切换

CodeBlockWrapperpackages/client/builtin/CodeBlockWrapper.vue)是自动注入的组件,用户无需手动使用。挂载后,它做三件关键事:

  1. 注册点击clicks.calculateSince(props.at, props.ranges.length - 1) 把该代码块的高亮阶段注册进幻灯片的点击上下文,at 属性(默认 '+1')决定从第几个点击开始;
  2. 计算当前阶段index = Math.max(0, clicks.current - clicksInfo.start + 1),取 ranges[index];超出阶段数后回落到 finallyRange(默认取最后一个阶段,可用 finally 属性覆盖,默认值 'last');
  3. 切换类名:调用 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 补充背景或边框。

与其他行号参数的协作:startLinemaxHeightlines

除了 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},行号支持 na-ball / *none 以及逗号/分号分隔的混合写法;
  • 动态演进:{2-3|5|all}| 切分阶段,与点击次数一一对应,可用 hide / none 控制整块显隐;
  • 进阶控制:第二个 {} 可透传 atfinallystartLinelinesmaxHeight 等 props;
  • 实现链路:围栏 info 由 wrapper 转换器 解析为 <CodeBlockWrapper :ranges="...">,客户端 CodeBlockWrapper.vue 将每个阶段注册进点击系统,再通过 updateCodeHighlightRange 切换 slidev-code-highlighted / slidev-code-dishonored 类名,样式由 code.css 提供默认压暗效果。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388