news 2026/10/19 12:42:36

Vale 源码剖析:reStructuredText 标题(heading)作用域如何被 lint 与测试夹具验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vale 源码剖析:reStructuredText 标题(heading)作用域如何被 lint 与测试夹具验证
  • 开发工具
  • 代码质量
  • CLI

【免费下载链接】vale

:pencil: A markup-aware linter for prose built with speed and extensibility in mind.

项目地址:https://gitcode.com/gh_mirrors/va/vale
点击查看免费下载

导读

本文以 Vale 开源仓库中的测试夹具 testdata/fixtures/scopes/heading/test.rst 为核心入口,拆解 Vale 如何识别 reStructuredText(reST)文档的标题层级、将其归入heading作用域体系,并驱动scope: heading等规则精确命中标题文本。读完本文,你将掌握 Vale 中 heading 作用域的完整取值表(heading.h1至heading.h6)、基于包含关系的匹配语义、doc(...)选择器对标题结构树的利用方式,以及一套可直接复用的规则与 e2e 测试写法。

关联文档解读:一个「一鱼多吃」的标题测试夹具

夹具的内容与意图

test.rst 只有 9 行,是一份刻意构造的 reST 文档骨架:

==================== This is XXX heading ==================== Section 1.1 Title TODO ---------------------- TODO

它用 reST 的 overline + underline 标题语法定义了两个层级:This is XXX heading是文档级标题(相当于 HTML 的h1),Section 1.1 Title TODO是一级小节标题(相当于h2)。正文区留有TODO占位符,同时标题文本中混入了XXX、TODO这类「遗留标记词」——这正是用于验证「规则能否命中标题文本」的最小样本。

值得注意的是,同目录下还并列着 test.md、test.adoc、test.html、test.dita 和 test.xml 五份同主题文件,它们把 Markdown 的#/##/###/####、AsciiDoc 的===/==、HTML 的<h1>–<h2>、DITA 的<title>、DocBook XML 的<title>全部映射到同一套heading作用域上。这个目录的定位是「跨格式的 heading 行为测试矩阵」:无论源格式是什么,只要元素被识别为标题,它就必须落入同一套 heading 作用域体系。而test.rst是其中 reST 格式的代表。

驱动夹具运行的配置

目录下的 _vale 文件定义了本目录专属的运行配置:

StylesPath = ../../scopes MinAlertLevel = suggestion [*] rules.Heading = YES rules.H2 = YES rules.H3 = YES [*.md] rules.MinH2 = YES rules.HN = YES [*.xml] Transform = ../../XSL/docbook-xsl-snapshot/html/docbook.xsl

它指向上级目录 testdata/fixtures/scopes 下的样式规则集,并按文件类型差异化启用规则:[*.md]额外启用MinH2与HN,[*.xml]则通过 DocBook XSL(../../XSL/docbook-xsl-snapshot/html/docbook.xsl)把 XML 先变换成 HTML 再交给解析器。由于test.rst不在任何[*.xxx]专属段内,它只受[*]段约束,即Heading、H2、H3三条规则。这也说明:reST 解析是 Vale 的原生能力,不需要像 XML 那样借助外部变换。

heading 作用域体系:从元素到作用域名的映射

作用域命名规则

Vale 把每个被解析出的块元素标注为一个点分作用域名。对标题来说,其命名规律是:

text.heading.<level>.<file-ext>

其中<level>对应标题层级数字 1–6。这个结构在 internal/lint/ast.go 中清晰可见:遍历元素标签历史时,若命中 HTML 标题标签(h1–h6),则把作用域记为text.heading.+ 标签名(例如h2),再拼接元作用域与真实扩展名;若命中非行内标签的映射标签,则使用tagToScope表给出的作用域名(如blockquote、list)。标题识别的正则定义在 internal/lint/html.go:

var heading = regexp.MustCompile(`^h\d$`)

任何h后跟一位数字的标签都被视为标题。同目录的 test.html 正是以真实 HTML 的<h1 class="title">、<h2 class="title">结构来验证这一点。

匹配语义:按包含关系,而非严格相等

scope: heading并不是要求块作用域恰好等于heading,而是按部分包含(containment)匹配。在 internal/check/scope_test.go 的注释中明确写道:

Matching is by containment: a rule matches a block when every section of the rule's scope appears somewhere in the block's.

即:规则声明的每个作用域段(如heading、h2)只要都出现在块作用域的段集合中,就命中。因此:

  • heading命中text.heading.h2.md(包含heading段);
  • heading.h2命中text.heading.h2.md,但不命中text.heading.h3.md(缺少h2段);
  • heading.h1、heading.h2、heading.h3的列表声明(union)分别命中对应层级。

这一点由 TestScopeMatches 中的测试用例矩阵逐行固定,例如:

{[]string{"heading"}, "text.heading.h2.md", true}, {[]string{"heading.h2"}, "text.heading.h2.md", true}, {[]string{"heading.h3"}, "text.heading.h2.md", false},

标题也是「散文」:参与分句与词性标注

一个容易忽略的事实是:标题不是孤立的块,它内部同样要做句子切分(segmentation)与词性标注(POS tagging)。在 internal/lint/ast.go 中有一段关键注释(关联 issue #1124 与 #1132):

  • 标题、列表项、表格单元与段落一样由句子构成,只有进入lintProse路径才会生成sentence.*块;
  • 若把标题直接交给lintBlock,则sentence作用域(所有sequence规则依赖它)会静默跳过标题中的大部分散文;
  • 反之,paragraph作用域专门指代段落,不能波及标题与表格单元。

因此test.rst标题里的XXX、TODO之所以能被命中并报告,正是因为这夹具同时验证了「标题文本已进入句子级处理管线」这一底层事实。

规则实战:用 heading 作用域编写可复用检查规则

夹具通过_vale配置启用的规则全部定义在 testdata/fixtures/scopes/rules 下,这些规则就是「heading 作用域」最直接的实战样例。

全层级命中:rules/Heading.yml

Heading.yml 面向全部标题层级:

message: "'%s' left in text" extends: existence ignorecase: false scope: heading level: error tokens: - XXX - FIXME - TODO - NOTE

scope: heading通过包含匹配命中所有text.heading.*块,用于在任意层级标题中拦截遗留标记词。根据 scopes.yaml 的 e2e 断言,该规则在六种格式中全部生效,reST 样本的输出为test.rst:2:9(XXX)与test.rst:5:19(TODO),行号、列号与标题文本位置一一对应。

层级定向命中:H2.yml / H3.yml

H2.yml 与 H3.yml 演示了「只查某一级标题」:

message: "'%s' left in text" extends: existence ignorecase: false scope: heading.h2 level: error tokens: - TWO

scope: heading.h2只命中text.heading.h2.*。在 reST 中,h2对应二级标题(即test.rst中Section 1.1 Title TODO这类用------下划线标记的标题);而在 test.adoc 中对应== Level 2 Section Title,在 test.md 中对应##标题——不同源格式的相同层级汇聚到同一个作用域名,这正是跨格式一致性的体现。

多层级联合声明:HN.yml

HN.yml 展示了scope的列表形式(union):

scope: - heading.h1 - heading.h2 - heading.h3

同一规则可以声明多个作用域,任一命中即生效。它在 Markdown 夹具上把FOUR从 h1–h3 三层标题中全部找出(test.md:1:35、test.md:3:29、test.md:9:26),但对只有两级的 reST 样本则不产生告警。

计数型规则的延伸:MinH2.yml

MinH2.yml 把 heading 作用域与occurrence规则结合,实现「标题长度门槛」:

message: "Headings should have at least 100 characters." extends: occurrence scope: heading.h4 level: suggestion min: 100 token: '[\w\s]'

它对heading.h4内的字符([\w\s])计数,不足 100 则报告「标题太短」。在 Markdown 样本中触发test.md:11:6:rules.MinH2,而test.rst没有h4级标题,故不受影响——再次印证层级作用域会精确地「挑」出目标标题。

在真实样式中更精细的用法

仓库的真实样式集 testdata/styles/demo 中还有更贴近生产环境的例子:

  • SentenceCase.yml:scope: heading+extends: capitalization+match: $sentence,实现「标题采用句首大写(sentence case)」检查,并提供exceptions、indicators、prefix等修饰项;
  • ScopedHeading.yml:scope: heading.h2+match: $title,仅要求二级标题采用标题式大写;
  • HeadingStartsWithCapital.yml:scope: heading.h1,配合raw正则'^(?:#+\s)?[a-z].+',检查一级标题是否以小写开头(如# intro→ 告警'intro' should be capitalized)。

这三条规则说明:heading 作用域是标题大小写策略类规则的天然载体,因为标题的文本内容已被完整、独立地暴露给规则引擎。

作用域的高级组合:doc(...)选择器与标题结构树

标题如何构建「文档结构树」

doc(...)选择器是 Vale 中把标题提升到「文档结构」层面的机制。其实现位于 internal/lint/selection.go,核心是wrapSections(internal/lint/selection.go):

  • 遍历 HTML 节点树,遇到h1–h6时新建<section>元素包裹该标题及其后的同级兄弟节点;
  • 小节按层级嵌套:h3出现在h2之后时,会落入h2的 section 内部;
  • 包裹只在同级节点间进行,因此列表项或块引用内部的标题只会切分其所在容器。

headingLevel(internal/lint/selection.go)正是从h后第一位数字解析出 1–6 的层级。之后,每个被选择器命中的元素都会被打上data-vale-doc标记(internal/lint/selection.go),该标记最终转化为块作用域上的in.<id>段。

在标题上使用 doc(...) 的实战形态

从 e2e 用例 scopes.yaml 可以整理出针对标题的典型选择器:

选择器语义
doc(h2)以 h2 标题的文本作为唯一块(该块不存在时为 line 1 的空块)
text & doc(h2, h3)只检查 h2/h3 标题内的文本
doc(section > :is(h2, h3))检查 section 的直接子标题
doc(section:has(> h2))以「含 h2 的 section」为块(可用于存在性/计数)
doc(section:has(> h2 + p))定位「紧跟在 h2 之后的段落」

其中doc(h2)的「叶子选择器」行为由doc-leaf用例(scopes.yaml)固定:对标题这类无子元素的叶子节点,选择器直接取其全部文本作为一个块来 lint。此外,命名作用域(config/scopes)可以把doc(section:has(> h2:contains("Methods")))这样的长表达式压缩为一个名字,供多条规则复用(见named用例,scopes.yaml)。

排除语义:~heading

作用域前的~表示排除。例如scope: ~blockquote & ~heading会命中除 blockquote 与 heading 外的所有文本(见 TestScopeMatches 与 TestScopeExcluded)。这在「标题与正文采用不同大小写规范」的场景下尤其有用:正文用小写风格,标题保留标题式大写,就可以用~heading把标题排除在外。

e2e 断言:heading 行为如何被验证

测试套件的组织方式

testdata/e2e/scopes.yaml 是 scope 行为的端到端测试定义。文件头部声明:

name: scopes dir: fixtures/scopes/*

即:每个fixtures/scopes/下的子目录构成一个独立测试目录。其中heading用例(scopes.yaml)这样运行:

- name: heading args: . requires: asciidoctor, dita, rst2html, xsltproc exit: 1 want: | test.adoc:1:20:rules.Heading:'XXX' left in text ... test.rst:2:9:rules.Heading:'XXX' left in text test.rst:5:19:rules.Heading:'TODO' left in text ...

其requires字段列出外部依赖:rst2html(Docutils 提供的 reST→HTML 转换器)、asciidoctor、DITA 工具与xsltproc。这直接印证了reST 的读取依赖rst2html——Vale 通过把 reST 文档交给 Docutils 渲染成 HTML,再对渲染结果做块解析。

reST 渲染管线的底层细节

reST 的处理实现在 internal/lint/rst.go,几个关键点:

  • 固定参数(internal/lint/rst.go):每次渲染都带上--no-section-numbering(保证标题不带自动编号,以免干扰检测)、--no-toc-backlinks、--no-footnote-backlinks、--no-file-insertion、--quiet、--halt=5等;
  • 标题去重处理(internal/lint/rst.go):reST 的.. contents::指令会重复文档标题,因此用reSphinx正则把这类指令按代码块处理;
  • Sphinx 兼容:[sphinx]配置段可注入额外的指令/角色(internal/lint/rst.go),Docutils 不认识的指令体按散文保留,只有少数命名指令例外;
  • 性能优化:每份文档单独rst2html的进程启动开销大,Vale 用常驻 Python 进程池(newProcPool)批量转换(internal/lint/rst.go),并设置PYTHONUTF8=1、PYTHONIOENCODING=utf-8避免 Windows 下控制台代码页导致的乱码(internal/lint/rst.go)。

断言结果说明了什么

从heading用例的输出可以看到作用域行为的几个事实:

  1. 层级归属正确:test.rst的文档标题(This is XXX heading,第 2 行)与Section 1.1 Title TODO(第 5 行)分别被rules.Heading命中,行号与XXX/TODO的实际位置一致;
  2. 跨格式一致:同一批规则在 adoc/dita/html/md/rst/xml 六种格式上全部生效;
  3. 格式差异被抹平:DocBook XML 经 XSL 变换为 HTML 后同样进入heading作用域,说明XML 格式的标题识别依赖Transform配置(见 heading/_vale 的[*.xml]段)。

从「测试夹具」到「生产规则」:可复用的操作清单

结合上述源码与配置,如果你要在自己的项目中为标题编写风格规则,推荐按以下步骤落地:

  1. 确定目标层级:先用scope: heading命中所有标题,或用scope: heading.h1(直到.h6)精确到某一级;需要多级时用列表scope: [heading.h1, heading.h2];
  2. 选择规则扩展类型:
    • 遗留标记/禁用词 →extends: existence+tokens(参考 Heading.yml);
    • 标题大小写 →extends: capitalization+match: $title/$sentence(参考 ScopedHeading.yml 与 SentenceCase.yml);
    • 标题长度 →extends: occurrence+min/max(参考 MinH2.yml);
  3. 处理正文与标题的差异:标题用标题式大写、正文用句首大写时,给正文规则加scope: ~heading;
  4. 按文档结构定向检查:需要「Methods 小节必须出现且必须提及某词」时,用doc(section:has(> h2:contains("Methods")))+extends: occurrence+min: 1;
  5. 分格式配置:reST 文件直接受 heading 作用域管理,无需额外变换;XML/DITA 类格式则需要在[*.xml]段配置Transform(参考_vale中的 XSL 路径写法)。

需要特别说明的是运行前提:对 reST 文件执行 lint 时,系统 PATH 中必须有rst2html,否则会报错rst2html not found; it's needed to read reStructuredText(对应源码 internal/lint/rst.go)。这与scopes.yaml中heading用例声明requires: rst2html的事实完全一致。

小结

test.rst虽只占 9 行,却是理解 Vale 标题作用域体系的「最小标本」:它以 reST 原生标题语法驱动完整的渲染管线,产出与 Markdown、AsciiDoc、HTML、DITA、DocBook 完全统一的text.heading.<level>作用域;与之配套的 heading/_vale 配置、rules 规则集和 scopes.yaml 的 e2e 断言共同构成了一条可复现的验证链路。无论你是想为标题写一条大小写规则,还是计划把doc(...)选择器用于结构化文档检查,这条链路都可以作为起点。

  • 开发工具
  • 代码质量
  • CLI

【免费下载链接】vale

:pencil: A markup-aware linter for prose built with speed and extensibility in mind.

项目地址:https://gitcode.com/gh_mirrors/va/vale
点击查看免费下载

相关推荐

上一篇:5个iTerm2触控板手势,让终端操作效率翻倍
下一篇:Xenia Canary 指令追踪(Instruction Tracing)实战指南:用 ITRACE / DTRACE 宏与断点剖析 PPC 指令与 HIR 数据

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 15:08:46

TensorFlow 2.x实战指南:从张量原理到模型训练与部署

1. 项目概述&#xff1a;TensorFlow到底是什么&#xff0c;值得花时间学吗说实话&#xff0c;刚接触深度学习的朋友问我推荐哪个框架&#xff0c;我第一个想到的还是TensorFlow。这倒不是说它一定比所有框架都好&#xff0c;而是它在工程化、部署生态、社区积累上的成熟度&…

作者头像 李华
网站建设 2026/10/18 20:37:56

TensorFlow 2.x实战经验:迁移、训练、部署一站式指南

坦白说&#xff0c;看到"tensorflow"这个标题&#xff0c;我第一反应不是"该从什么基础概念讲起"&#xff0c;而是想聊聊这两年我自己从TensorFlow 1.x迁到2.x的真实经历。2019年那会儿我还在维护着一套用1.x写的图像分类系统&#xff0c;每次训练启动要写…

作者头像 李华
网站建设 2026/10/19 5:04:15

拆解ThreadLocal性能困境:数组索引方案设计与实践

1. 先说结论&#xff1a;ThreadLocal 没那么神&#xff0c;真正快的是数组索引方案最近我把网关里一组高频读写的 ThreadLocal 变量换成了数组索引型本地线程变量&#xff0c;单线程内 set/get 混合压测下来大概快了 2 到 3 倍&#xff0c;接口整体延时也降了一截。这不是什么黑…

作者头像 李华
网站建设 2026/10/19 8:08:23

测试工程师KPI绩效查询与整理指南:指标拆解到汇报实战

测试工程师的绩效考评&#xff0c;向来是团队管理里最微妙的一环。大家平时都在写用例、提缺陷、跑测试&#xff0c;但真要到了要做绩效对齐、季度盘点的时候&#xff0c;往往发现自己手上的记录不是零散在这那的Excel里&#xff0c;就是淹没在各类管理工具的链接海洋里&#x…

作者头像 李华
网站建设 2026/10/18 0:10:27

基于MATLAB的电磁散射RCS特性计算:Mie级数与矩量法全流程解析

做电磁散射特性计算的人都知道&#xff0c;最后拿出来的东西不能是"一堆场图"&#xff0c;得是能对得上实验、能指导设计的定量结论。我入坑这条路就是从MATLAB起步的——从最经典的金属球验证&#xff0c;到后来用矩量法算任意形状目标的雷达散射截面&#xff08;RC…

作者头像 李华
网站建设 2026/10/18 20:47:29

PyTorch深度学习实战:动态图调试到模型部署的完整指南

和PyTorch打交道这些年&#xff0c;我的感受是&#xff1a;它早就不是那个"研究圈专用框架"了&#xff0c;而是深度学习从入门到落地绕不开的主力工具。无论是快速跑通一个图像分类Demo、微调一个大模型&#xff0c;还是把训练好的模型导出成可部署的格式&#xff0c…

作者头像 李华