Slidev Code Groups 详解:基于 Comark 语法的标签式代码分组与图标自动匹配
本文围绕 Slidev 的 Code Groups(代码分组)功能展开:如何在一张幻灯片中用 ::code-group 指令把多个代码块组织成可点击切换的标签页(例如 npm / yarn / pnpm 三种安装方式),以及如何通过标题自动匹配图标、使用 ~icon~ 语法自定义图标。读完后你可以直接在 Slidev 演示中编写带标签栏与品牌图标的多版本代码示例,并从 CodeGroup.vue 与 TitleIcon.vue 等源码理解其标签渲染与图标匹配的底层机制。
前提条件:启用 Comark 语法
Code Groups 依赖 Slidev 的 Comark 语法——一种基于指令(Directive)的增强 Markdown 扩展,允许在文档中书写块级组件指令(::block-component / :::)。因此使用 Code Groups 之前,必须先在幻灯片文件的 frontmatter 中开启该选项:
---
comark: true
---
未开启 comark 时,::code-group 指令不会被解析为组件,也就无法渲染标签栏。该开关是文件级的,只作用于当前启用了它的幻灯片入口(详见 Comark 文档)。
基本用法:用 ::code-group 包裹多个代码块
开启 Comark 后,就可以用 ::code-group 开、::: 闭的形式把任意数量的代码块分组。每个代码块的标准标题(即信息字符串中的 [标题])会成为对应的标签页名称:
::code-group
```sh [npm]
npm i @slidev/cli
```
```sh [yarn]
yarn add @slidev/cli
```
```sh [pnpm]
pnpm add @slidev/cli
```
::
渲染效果是:顶部生成一排标签(npm、yarn、pnpm),点击某个标签即切换下方展示的代码块;默认激活第一个带标题的代码块。这一交互由 CodeGroup.vue 实现,其源码逻辑非常直观:
onMounted时通过querySelectorAll('.slidev-code-wrapper')收集分组内所有代码块包装元素;- 读取每个元素的
data-title属性(该属性由代码块包装组件 CodeBlockWrapper.vue 在渲染时写入,值即代码块的[标题]); - 第一个带标题的块成为初始激活项(
activeTitle),所有标题依次填入tabs数组; - 标签文案在展示前会执行
tab.replace(/~([^~]+)~/g, '').trim(),即自动剥除~icon~部分(后文详述),保证标签上只显示干净的名称。
配合 code.css 中 .slidev-code-group-tabs / .slidev-code-wrapper.active 等样式,激活的标签会显示主题色边框,未激活的块则被隐藏,从而实现"标签页"式的切换体验。
一个值得注意的细节:当代码块处于 code-group 内部时,单个代码块自身不会再重复渲染标题栏。从 CodeBlockWrapper.vue 源码可以看到,它通过 inject('activeTitle') 注入父级 CodeGroup 提供的激活标题引用;只有当 activeTitle 为 null(即不在 code-group 内)且自身有标题时,才会渲染 slidev-code-block-title 标题条。这保证了标题只在标签栏出现一次,视觉上更整洁。
标题自动匹配图标
除了纯文字标签,Code Groups 还支持根据标题名称自动匹配图标。例如标题为 pnpm 时,标签左侧会自动出现 pnpm 的 logo。文档中说明,同样的图标匹配能力也适用于单个带标题的 code block 以及 Shiki Magic Move,因为三者的图标解析都收敛到同一个 TitleIcon.vue 组件。
TitleIcon 的匹配算法(见 matchIcon 函数)分两步:
- 显式图标优先:若标题中匹配到
~icon~语法(title.match(/~([^~]+)~/g)),直接取第一个匹配项作为图标类名使用,不再查内置表; - 内置表最长匹配:否则将内置图标表的所有键按长度降序排序,遍历查找第一个被标题"包含"(大小写不敏感)的键并返回对应图标。长度降序保证了像
package.json、tsconfig.json这类完整文件名的键优先于.json这类扩展名键命中,避免误匹配。
内置图标表完整定义在 TitleIcon.vue 的 builtinIcons 中,覆盖包管理器、框架、打包器、常见配置文件与文件扩展名:
const builtinIcons = {
// package managers
'pnpm': 'i-vscode-icons:file-type-light-pnpm',
'npm': 'i-vscode-icons:file-type-npm',
'yarn': 'i-vscode-icons:file-type-yarn',
'bun': 'i-vscode-icons:file-type-bun',
'deno': 'i-vscode-icons:file-type-deno',
// frameworks
'vue': 'i-vscode-icons:file-type-vue',
'svelte': 'i-vscode-icons:file-type-svelte',
'angular': 'i-vscode-icons:file-type-angular',
'react': 'i-vscode-icons:file-type-reactjs',
'next': 'i-vscode-icons:file-type-light-next',
'nuxt': 'i-vscode-icons:file-type-nuxt',
'solid': 'logos:solidjs-icon',
'astro': 'i-vscode-icons:file-type-light-astro',
// bundlers
'rollup': 'i-vscode-icons:file-type-rollup',
'webpack': 'i-vscode-icons:file-type-webpack',
'vite': 'i-vscode-icons:file-type-vite',
'esbuild': 'i-vscode-icons:file-type-esbuild',
// configuration files
'package.json': 'i-vscode-icons:file-type-node',
'tsconfig.json': 'i-vscode-icons:file-type-tsconfig',
'.npmrc': 'i-vscode-icons:file-type-npm',
'.editorconfig': 'i-vscode-icons:file-type-editorconfig',
'.eslintrc': 'i-vscode-icons:file-type-eslint',
'.eslintignore': 'i-vscode-icons:file-type-eslint',
'eslint.config': 'i-vscode-icons:file-type-eslint',
'.gitignore': 'i-vscode-icons:file-type-git',
'.gitattributes': 'i-vscode-icons:file-type-git',
'.env': 'i-vscode-icons:file-type-dotenv',
'.env.example': 'i-vscode-icons:file-type-dotenv',
'.vscode': 'i-vscode-icons:file-type-vscode',
'tailwind.config': 'vscode-icons:file-type-tailwind',
'uno.config': 'i-vscode-icons:file-type-unocss',
'unocss.config': 'i-vscode-icons:file-type-unocss',
'.oxlintrc': 'i-vscode-icons:file-type-oxlint',
'vue.config': 'i-vscode-icons:file-type-vueconfig',
// filename extensions
'.mts': 'i-vscode-icons:file-type-typescript',
'.cts': 'i-vscode-icons:file-type-typescript',
'.ts': 'i-vscode-icons:file-type-typescript',
'.tsx': 'i-vscode-icons:file-type-typescript',
'.mjs': 'i-vscode-icons:file-type-js',
'.cjs': 'i-vscode-icons:file-type-js',
'.json': 'i-vscode-icons:file-type-json',
'.js': 'i-vscode-icons:file-type-js',
'.jsx': 'i-vscode-icons:file-type-js',
'.md': 'i-vscode-icons:file-type-markdown',
'.py': 'i-vscode-icons:file-type-python',
'.ico': 'i-vscode-icons:file-type-favicon',
'.html': 'i-vscode-icons:file-type-html',
'.css': 'i-vscode-icons:file-type-css',
'.scss': 'i-vscode-icons:file-type-scss',
'.yml': 'i-vscode-icons:file-type-light-yaml',
'.yaml': 'i-vscode-icons:file-type-light-yaml',
'.php': 'i-vscode-icons:file-type-php',
}
其中绝大多数键值来自 vscode-icons 图标集合。从源码结构看,该组件默认依赖 @iconify-json/vscode-icons 提供的图标数据(以及 logos 集合中的 Solid 图标),因此要实际看到这些内置图标,需要按官方说明在项目中安装 @iconify-json/vscode-icons(即执行 npm add @iconify-json/vscode-icons 等对应包管理器的安装命令)。与文档列表相比,当前仓库的 TitleIcon.vue 实现中还额外内置了 .svg 扩展名到 i-vscode-icons:file-type-svg 的映射,以仓库源码为准。
匹配结果最终通过 UnoCSS 的图标类名生效:TitleIcon 的模板仅渲染一个 <div>,其 :class 绑定为匹配到的图标类(如 i-vscode-icons:file-type-npm),未匹配到标题时不渲染任何内容。
自定义图标:~icon~ 语法
内置表覆盖不到的品牌或场景,可以使用任意 Iconify 集合中的图标,写法是在标题里用 ~图标类名~ 包裹,例如:
```js [npm ~i-uil:github~]
console.log('Hello, GitHub!')
```
此时标题为 npm,但标签上显示的图标是 i-uil:github 指定的图标,而不是内置表匹配到的 npm 图标——~icon~ 优先级高于内置匹配。
要让自定义图标正常工作,需要做两步:
- 安装图标所属的 Iconify 集合。例如使用
uil集合时:
:::code-group
npm add @iconify-json/uil
yarn add @iconify-json/uil
pnpm add @iconify-json/uil
bun add @iconify-json/uil
:::
- 将图标类名加入
uno.config.ts的safelist,确保该类名会被 UnoCSS 收集生成:
import { defineConfig } from 'unocss'
export default defineConfig({
safelist: [
'i-uil:github',
],
})
如果不写入 safelist,动态拼接的图标类名可能不会进入最终的样式产物,图标将不显示。参考 UnoCSS 配置文档可了解 uno.config.ts 的其余可选项。
小结
Code Groups 的核心脉络可以概括为三条线索:
- 语法层:
comark: true开启 Comark 后,::code-group/:::指令把若干带[标题]的代码块组合成标签页; - 渲染层:CodeGroup.vue 读取各块的
data-title生成标签并通过provide('activeTitle')与 CodeBlockWrapper.vue 协作完成激活态切换与标题去重; - 图标层:TitleIcon.vue 先解析
~icon~显式图标,再按"长度降序、大小写不敏感的包含匹配"命中内置图标表,从而让npm、pnpm、vite、package.json等标题自动带上对应的品牌图标。
这一套机制同样服务于带标题的单个代码块与 Shiki Magic Move,因此在编写 Slidev 演示时,只要给代码块起一个规范的名字,就能获得一致的图标与视觉风格。
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
