Slidev 代码片段导入(Import Code Snippets):从外部文件引码、按 Region 取片段与源码实现解析
【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev
本篇围绕 Slidev 的Import Code Snippets特性(v0.47.0 起引入)展开:讲解如何在 Markdown 幻灯片里用一行<<<语法把项目中的现成代码文件引入为代码块,如何通过 VS Code 风格的 Region 标记只提取文件中的某个片段、如何显式指定语言与叠加行高亮 / Monaco 编辑器等全部代码块能力。读完你不仅能直接使用这套语法,还能从 解析器源码 层面理解路径解析、Region 匹配、HMR 监听与安全边界的完整实现。
基础语法:一行<<<引入外部文件
官方文档 docs/features/import-snippet.md 中给出的核心语法如下:
<<< @/snippets/snippet.js在幻灯片中写这一行后,@/snippets/snippet.js文件的完整内容会被替换为该位置的代码块,并自动获得语法高亮。
<<<后面的路径有两种写法:
@/别名路径:@指向当前 Slidev 包的根目录(即包含slides.md的目录)。官方建议把片段放在@/snippets下,这样能与 Monaco 编辑器(侧边编辑器、可写编辑器)保持兼容;- 相对路径:也可以从幻灯片文件所在目录出发写相对路径导入。
仓库中的可运行示例见 demo/starter/slides.md(其中的<<< @/snippets/external.ts#snippet),对应片段文件为 demo/starter/snippets/external.ts:
// #region snippet // Inside ./snippets/external.ts export function emptyArray<T>(length: number) { return Array.from<T>({ length }) } // #endregion snippetRegion:只引入文件中的某个片段
借助 VS Code 的 Region 折叠注释(#region/#endregion),可以让幻灯片只展示文件中的指定部分:
<<< @/snippets/snippet.js#region-name从源码看,Slidev 对 Region 的支持比 VS Code 默认注释风格更广。snippet.ts 中定义了 8 组区域标记正则,覆盖多种语言注释语法:
| 注释风格 | 起始标记 | 适用场景 |
|---|---|---|
// #region name | 单行斜杠注释 | JS / TS / C# 等 |
<!-- #region name --> | HTML 注释 | Markdown / HTML |
/* #region name */ | 块注释 | C / Java / CSS 等 |
#region name/# #region name | 裸#行 | Python / Shell / R |
-- #region name/:: #region name/REM #region name | SQL / 批处理风格 | SQL、Bash、Windows 批处理 |
#pragma region name | pragma 形式 | 部分编译系统 |
(* #region name *) | 圆括号注释 | Pascal 系语言 |
对应地,结束标记为同风格下的#endregion name。findRegion函数(snippet.ts)的行为细节值得注意:
- 同名嵌套可被正确配对:扫描过程中遇到同名 start 标记会累加计数器,遇到 end 标记时递减,计数器归零才认定区域结束;
- end 标记允许省略区域名:
endRegion === regionName || endRegion === ''都视为有效闭合,这是一个兜底容错; - 结果会做去缩进(dedent):提取出的片段会先剥掉标记行本身,再按 dedent 函数 去掉共同的前导缩进,保证引入幻灯片的代码块左对齐;
- 找不到区域时不报错:若
findRegion返回null,则回退为引入整个文件内容。
显式指定语言
如果不希望按文件扩展名推断语言,可以在路径后追加一个语言标识符:
<<< @/snippets/snippet.js ts从 resolveSnippetImport 的实现看,语言解析逻辑为:优先取命令行中显式给出的lang,若为空则取filepath的扩展名(path.extname(filepath).slice(1))作为兜底。也就是说snippet.test.ts未指定语言时会自动按ts高亮。
与其他代码块特性完全兼容
文档明确说明,导入的片段支持所有普通代码块特性,包括行高亮与 Monaco 编辑器:
<<< @/snippets/snippet.js {2,3|5}{lines:true} <<< @/snippets/snippet.js ts {monaco}{height:200px}其中:
{2,3|5}是逐步点击高亮的行号序列(|分隔各点击步);{lines:true}显示行号;{monaco}把该代码块升级为 Monaco 编辑器渲染;{height:200px}是传递给组件的额外属性。
另外,可以用{*}作为行高亮的占位符,表示"高亮当前点击步所对应的行":
<<< @/snippets/snippet.js {*}{lines:true}从源码看,实现方式很直接:插件把解析出的lang与meta拼进一个标准fencetoken 的info字段(snippet.ts),后续交给 Slidev 既有的代码块处理管线(shiki 高亮、click marker 解析、Monaco 变换等),因此导入片段与手写的 ``` 代码块在能力上完全等价。
源码实现解析:markdown-it 块级规则
整个特性由 packages/slidev/node/syntax/snippet.ts 中的一个 markdown-it 块级规则实现,关键实现点如下:
1. 语法入口正则
// packages/slidev/node/syntax/snippet.ts#L109 export const RE_SNIPPET_IMPORT = /^<<<[ \t]*(\S.*?)(#[\w-]+)?[ \t]*(?: \t)?[ \t]*(\{.*)?$/四个捕获组分别对应:文件路径、#region名、语言标识、{meta}参数。规则通过md.block.ruler.before('fence', 'snippet_import', ...)注册(snippet.ts),并在{ alt: ['paragraph', 'reference', 'blockquote', 'list'] }中声明替代块级类型——这意味着<<<也可以出现在列表项等缩进块内。测试 snippet.test.ts 专门验证了"snippet in indented block"场景:列表项内缩进的<<<能正确渲染成<li>内的代码块;同时验证了位于 ``` 围栏内部的<<<行不会被转换(snippet.test.ts)。
2. 路径解析与安全边界
// packages/slidev/node/syntax/snippet.ts#L118-L129 const src = slash( filepath.startsWith('@/') ? path.resolve(userRoot, filepath.slice(2)) : path.resolve(dir, filepath), // dir 为当前幻灯片文件所在目录 ) // ... if (!isPathInsideRoots(src, allowedRoots)) throw new Error(`Code snippet path escapes the project root: ${src}`)两个安全约束值得记录:
- 解析后的真实路径必须落在项目根(
userRoot/userWorkspaceRoot及额外roots)之内,否则抛出Code snippet path escapes the project root,防止通过../../逃逸读取项目外文件; - 文件必须真实存在且是普通文件,否则抛出
Code snippet path not found: <path>。
对应测试见 snippet.test.ts("resolves a snippet path that stays inside the allowed roots" 与 "throws when a snippet path escapes the allowed roots")。
3. 文件监听与 HMR
普通引入路径会执行:
// packages/slidev/node/syntax/snippet.ts#L195-L196 watchFiles[src] ??= new Set() watchFiles[src].add(slide.index)即把"源文件 → 引用它的幻灯片下标集合"登记进data.watchFiles。Vite 加载器 在updateServerWatcher中调用server.watcher.add(Object.keys(data.watchFiles))把这些外部文件纳入 Vite watcher;当文件变更时,handleHotUpdate 通过data.watchFiles[ctx.file]反查出受影响的幻灯片并强制刷新。因此修改 snippets 目录下的文件后,引用它的幻灯片会自动热更新,无需重启服务。
4.{monaco-write}的特殊处理
若meta中包含{monaco-write}(对应 Monaco 可写编辑器),插件不走 fence 路径,而是:
- 把文件路径加入
monacoWriterWhitelist白名单(snippet.ts); - 用 lz-string 把文件内容压缩为 Base64 内联进
<Monaco writable="..." code-lz="..." />组件; - 运行时写回的白名单校验与路径防逃逸在 monacoWrite.ts 中完成:非白名单文件直接拒绝,
path.relative(userRoot, filepath)以..开头或为绝对路径时同样拒绝。
5. 作用域限制
规则在解析时会从state.env.id提取幻灯片下标(regexSlideSourceId)定位到具体SlideInfo;若来源不是可识别的幻灯片(如非幻灯片 Markdown 源),会打印警告Snippet syntax is not supported in ...并跳过转换(snippet.ts)。
进阶:Magic Move 块中也支持片段导入
在 Shiki Magic Move 的md magic-move四反引号块中,<<<语法同样可用:magic-move.ts 的resolveMagicMoveSnippetImports会逐行识别<<<行(跳过内层围栏内的行),把每处导入展开为内联的代码块,并把源文件登记进watchFiles保证 HMR。这样即可用外部文件的多个 Region 版本驱动跨步骤的代码动画。
使用建议与排错小结
| 场景 | 说明 |
|---|---|
| 片段放哪里 | 建议统一放在@/snippets下,兼顾 Monaco 侧边/可写编辑器的兼容性 |
| 只展示文件局部 | 在源文件用#region 名字/#endregion 名字包裹,导入时写路径#名字;end 标记可省略名字 |
| 扩展名不能代表语言 | 在路径后追加语言标识,如<<< @/snippets/notes.ts md |
报Code snippet path escapes the project root | 路径解析后越出了项目根,检查是否误用过多../ |
报Code snippet path not found | 确认@/指向包根目录、相对路径相对于幻灯片所在目录 |
| 改了片段文件幻灯片没变 | 正常应通过watchFiles触发 HMR;确认该文件确实以普通<<<(非{monaco-write})方式被引入过 |
在围栏代码块里出现<<< | 不会被转换,按原文本渲染(见 snippet.test.ts) |
综上,<<<导入把"幻灯片里的代码"与"仓库里的真实代码"解耦:代码只需维护一份,配合 Region 取片段、配合 meta 叠加全部代码块能力、配合watchFiles获得热更新,是 Slidev 面向真实项目代码做演示时的核心工作流。
【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考