Angular 文档流水线<docs-code>元素解析:从测试夹具到 marked 扩展的完整实现
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
Angular 官方文档站(angular.dev)的内容渲染并非直接依赖原生 Markdown,而是基于adev/shared-docs/pipeline下的一条 Markdown 预处理流水线,其中<docs-code>是最核心的自定义代码块元素。本文以仓库中的测试夹具 docs-code.md 为主体,逐一剖析其 7 种用法的语法与渲染行为,并结合 docs-code 扩展源码、格式化模块 与 单元测试,讲清每个属性(path、header、language、region、hideDollar等)在流水线中的解析路径与底层原理。
一、测试夹具文档:<docs-code>的完整语法面
docs-code.md 是docs-code扩展的"活文档"——它本身既是语法示例,又是 docs-code.spec.mts 的测试输入。文档中的 7 段示例按出现顺序对应 spec 中querySelectorAll('code')索引 0~4 及.docs-code索引 5~6 的断言,覆盖如下能力:
- 内联代码体:
<docs-code>this is code</docs-code>,直接以标签内容作为代码; path引用 + ESLint 注释剥离:<docs-code path="./example-with-eslint-comment.ts" />;path引用 + region 提取:<docs-code path="./example-with-region.ts" />;- 自定义
header标题:<docs-code header="src/locale/messages.fr.xlf (<trans-unit>)" path="./messages.fr.xlf" />; - API 符号链接控制:
language="ts"的内联对象字面量,验证属性名不会被误链; hideDollar属性:<docs-code hideDollar code="echo 'hello world'" />;language属性 + 内联去缩进:language="typescript"的多行代码块。
其中示例 2、3、4 引用的资源文件均位于同一测试目录下:
- example-with-eslint-comment.ts:仅含一行
// eslint-disable-next-line与const x = 1;; - example-with-region.ts:含一对
// #docregion something标记包裹const x = 'within the region';; - messages.fr.xlf:一个含多层嵌套
#docregion注释的 XLIFF 翻译文件。
二、tokenizer 层:属性如何被解析
docs-code.mts 实现了 marked 的 block 级扩展。触发规则为start(src)匹配^<docs-code\s,即标签必须独占行首且后跟空白;整体结构由正则singleFileCodeRule捕获:
const singleFileCodeRule = /^\s*<docs-code((?:\s+[\w-]+(?:="[^"]*"|='[^']*'|=[^\s>]*)?)*)\s*(?:\/>|>(.*?)<\/docs-code>)/s;捕获组 1 是开标签上的全部属性串,捕获组 2 是开闭标签之间的内容。随后 tokenizer 用一组独立正则逐一提取属性(见pathRule、headerRule、linenumsRule、highlightRule、languageRule、visibleLinesRule、regionRule、previewRule、hideCodeRule、hideDollarRule、classRule、preferRule),组装成DocsCodeToken:
| 属性 | 类型 | 说明(对照源码注释) |
|---|---|---|
path | string | 示例文件路径,按工作区相对路径加载,覆盖标签内容 |
header | string | 代码块上方显示的<h3>标题 |
linenums | bool | 渲染时插入shiki-ln-number行号 |
highlight | string | 需高亮的行,支持区间字符串,经expandRangeStringValues展开 |
language | string | 语法高亮语言;mermaid/shell/bash有特殊处理 |
visibleLines | string | 折叠视图中可见的行;与region互斥 |
region | string | 只显示源码中指定#docregion区域 |
preview/hideCode/hideCopy/hideDollar | bool | 渲染行为开关,hideDollar用于隐藏 shell 代码中的$前缀 |
class | string | 附加 CSS 类,按空格切分 |
prefer/avoid | string | 代码风格标记,容器附加docs-code-prefer类并在 header 显示 "Prefer"/"Avoid" |
两个关键行为值得注意:
path优先:若指定了path且非空,tokenizer 调用loadWorkspaceRelativeFile(path[1])读取文件,忽略标签内联内容;并立即按文件扩展名调用removeEslintComments(code, fileType)剥离 ESLint 指令注释(见第四节);- 互斥校验前置:format/index.mts 的
formatCode第一行即throw Error('Cannot define visible lines and region at the same time'),visibleLines与region不允许同时出现——因为 region 提取本质上就是"按注释计算 visibleLines"。
三、渲染层:deindent、region 提取、Shiki 高亮与 API 链接
renderer 阶段由formatCode(token, context)驱动,处理顺序是:extractRegions→deindent→trim→highlightCode→ 构建<div class="docs-code">容器 → 应用属性与类 →processForApiLinks。
3.1 region 提取:从注释到行区间
format/region.mts 中extractRegions调用 regions/region-parser.mts 的regionParser:
- 按
token.path的扩展名选择匹配器(REGION_MATCHERS映射:ts/js/mjs/es6用inlineC(// #docregion风格)、html/svg用html匹配器(<!-- #docregion -->)、conf/yaml/sh用inlineHash等); - 逐行扫描,遇到
#docregion name打开区域、#enddocregion name关闭,支持嵌套区域与逗号分隔的多区域名(getRegionNames按逗号拆分); - 区域标记行本身会被从内容中过滤掉(
countOfRegionLines++并return false),因此输出 HTML 中不会出现docregion字样——这正是 spec 中not.toContain('docregion')断言的保证; - 若 token 指定了
region,取regionMap[token.region]并以其lines.join('\n')替换整段代码;找不到则抛Cannot find ${token.region} in ${token.path}!; - 无区域名的整文件内容会落入名为空字符串的
WHOLE_FILE_REGION_NAME,使region=""也能工作。
夹具 example-with-region.ts 正验证了这一点:spec 断言渲染结果含const x = 'within the region';而不含docregion。而 messages.fr.xlf 中大量translated-hello、custom-id、generated-id等嵌套区域,展示了 HTML 注释匹配器处理多层#docregion/#enddocregion的能力(如translated-hello内再嵌custom-id,可分别引用)。
3.2 deindent:内联代码块的正确缩进
format/index.mts 的deindent计算所有非空行的最小公共前导空白并整体裁掉。夹具末尾的示例:
<docs-code language="typescript"> if (foo) { // bar } </docs-code>渲染后// bar仍保留 2 个缩进空格(相对缩进不变),对应 spec 断言codeBlock?.textContent toMatch(/^ \/\/ bar/m)——这就是"should deindent inline code blocks correctly"要验证的行为:裁掉的是公共前缀,而非全部缩进。
3.3 高亮与语言推断
format/highlight.mts 中highlightCode的要点:
language === 'none'或'file'时跳过高亮;- 未写
language时按guessLanguageFromPath推断:.ts/.js→typescript,.html→angular-html,.css→css,.json→json,其余回退angular-ts——这解释了夹具中messages.fr.xlf不带language也能获得合理高亮; highlight属性经expandRangeStringValues展开为行集合后传入 Shiki(codeToHtml),并携带apiEntries上下文;linenums为真时,用 JSDOM 遍历 Shiki 输出的.line元素,在每行前插入<span class="shiki-ln-number">N</span>;language为mermaid时容器设置mermaid="true"属性,为shell/bash时追加shell类(供前端渲染$提示符,与hideDollar联动)。
3.4 API 符号自动链接,但排除对象属性名
format/index.mts 的processForApiLinks遍历 Shiki 生成的叶子<span>,用getSymbolUrl(symbol, apiEntries)匹配 API 词条表,命中则把符号替换为指向 API 文档的<a>。夹具中的反例专门防止误伤:
<docs-code header="Property names should not be linked" language="ts"> const form = { state: [''] }; </docs-code>spec 断言渲染后的innerHTML中不存在<a href="/api/animations/state">state</a>——即state虽是 API 词条,但作为对象字面量的属性键不应生成链接。这保证了文档中的代码块既保留符号可点击性,又不破坏语法语义。
3.5 容器属性回写
applyContainerAttributesAndClasses会把path、visibleLines(展开后的数字数组)、header回写为容器属性,布尔开关preview/hideCode/hideCopy/hideDollar写为"true"属性——hideDollar的 spec 断言getAttribute('hideDollar') === 'true'正是验证这一步,前端脚本据此决定是否为 shell 代码渲染$提示符。
四、ESLint 注释剥离:path加载的隐藏清洗步骤
regions/remove-eslint-comments.mts 只对ts/js/html生效,用合并正则删除eslint-disable、eslint-disable-next-line、eslint-disable-line、eslint-enable四类指令(ts/js的正则同时包含 HTML 注释形式,以覆盖@Component内联模板)。注意 TS 正则不覆盖块注释/* ... */之外的其他代码注释,仅精准命中 eslint 前缀行。
夹具 example-with-eslint-comment.ts 首行// eslint-disable-next-line因此被剥离,spec 断言渲染文本not.toContain('// eslint')。设计意图很直接:示例源码里的 lint 抑制指令对读者毫无价值,流水线统一清洗。
五、端到端验证与真实使用场景
5.1 测试如何接线
docs-code.spec.mts 的执行流程:setHighlighter()初始化 Shiki 高亮器 → 读取./docs-code.md→parseMarkdown(content, rendererContext)(入口在 marked/parse.mts,先validatePairedTags校验标签配对,再经marked.use({extensions, walkTokens})注册全部扩展,docsCodeExtension是其中之一)→ JSDOM 片段化后用 7 条断言逐段验证上述行为。path为相对路径(./example-with-region.ts),说明loadWorkspaceRelativeFile以 Markdown 文件所在目录为基准解析示例文件,文档与示例同目录共置。
5.2 正式文档中的用法
该夹具展示的语法在 angular.dev 内容库中大量复用,例如:
- service worker 通信指南 中
<docs-code header="log-update.service.ts" path="adev/src/content/examples/service-worker-getting-started/src/app/log-update.service.ts" region="sw-update"/>——header给出面向读者的文件名,path指向示例工程源码,region只截取sw-update片段; - 动画复合序列指南 与 CSS 动画指南 中同样以
header+path+region三件套引用adev/src/content/examples/animations下的示例源码。
从源码结构看,正式文档使用仓库绝对相对路径 +region,而测试夹具使用同目录短路径,二者走的是同一条docsCodeExtension解析链路。
六、小结:属性速查与适用边界
| 需求 | 写法 | 流水线落点 |
|---|---|---|
| 展示代码片段(带语言) | <docs-code language="ts">…</docs-code> | tokenizer 取捕获组 2 → Shiki 高亮 |
| 引用仓库/示例文件 | path="…" | loadWorkspaceRelativeFile+ ESLint 清洗 |
| 只展示文件片段 | region="name" | regionParser提取,标记行被过滤 |
| 指定显示标题 | header="file.ts (excerpt)" | 容器内<h3> |
| 行号 | linenums | 插入shiki-ln-numberspan |
| 高亮特定行 | highlight="3,7-9" | 区间展开后传入 Shiki |
| 折叠默认视图可见行 | visibleLines="1-5" | 与region互斥 |
隐藏 shell$ | hideDollar | 容器属性"true" |
| 好/坏示例对比 | prefer/avoid | docs-code-prefer/avoid类 + 头部标签 |
适用前提:该流水线服务于 Angular 官方文档站构建(Bazel 下的adev工程),<docs-code>仅在此 Markdown 渲染链中生效,普通 Markdown 阅读器不识别这些标签;path解析依赖工作区文件布局,region提取依赖示例源码中按 region-parser.mts 规范书写的#docregion/#enddocregion配对注释。理解这套机制后,无论是阅读 angular.dev 文档背后的生成逻辑,还是在仓库内新增/修改文档示例,都能准确预测<docs-code>的最终渲染结果。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考