news 2026/10/19 10:18:15

Vale 行内 `vale off` / `vale on` 注释指令:原理、实测与源码剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vale 行内 `vale off` / `vale on` 注释指令:原理、实测与源码剖析
  • 开发工具
  • 代码质量
  • CLI

【免费下载链接】vale

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

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

<!-- vale off -->与<!-- vale on -->是 Vale(markup-aware 散文 linter)提供的行内指令,用于在 Markdown 等文档中精准屏蔽或恢复对某个片段的检查。本指南围绕仓库中的测试夹具 testdata/fixtures/comments/issue1001.md 展开,结合端到端用例 testdata/e2e/comments.yaml 与核心实现 internal/core/file.go、internal/lint/ast.go,讲清该指令在加粗、斜体等行内元素包裹下的行为边界,并给出可复现的验证方法。读完你将掌握 Vale 注释指令的完整语法族、位置感知的区域抑制机制,以及如何在自己的文档中正确使用行内vale off/vale on。

一、问题背景:行内注释指令为何需要特殊处理

Vale 的注释指令(comment directive)用来在文档中就地控制检查的开关。最常用的形式是:

<!-- vale off --> <!-- vale on -->

对于独立成段(位于独立行)的指令,Vale 在遍历块级元素时即可识别。但实际文档中,指令常常出现在行内——紧跟一段加粗、斜体或链接文本之后,例如*Note*: <!-- vale off -->This is a sentence of.<!-- vale on -->。

行内指令给解析带来两个经典难题:

  1. 块级滞后期(block latency):HTML 块通常在闭合标签处才被整体提交给 lint 流程(见 internal/lint/ast.go),而vale off/vale on一开一关往往已经抵消,若只依赖"当前状态"标志,就无法抑制两个指令之间那一段文本产生的告警。
  2. 行内元素包裹(inline wrapping):当<!-- vale off -->...<!-- vale on -->被*Note*:、**Note**:或_Note_:等行内标记包围时,指令的字节位置必须被精确记录,才能和告警的(行,列)坐标一一比对。

仓库中的夹具 issue1001.md 正是针对问题 #1001 构造的最小复现用例,用于验证"行内vale off/vale on可以正确抑制片段告警"这一行为。

二、关联文档精读:issue1001.md 到底在测什么

夹具全文仅 5 行(原文 163 字节),内容如下:

*Note*: <!-- vale off -->This is a sentence of.<!-- vale on --> **Note**: <!-- vale off -->This is a sentence of.<!-- vale on --> _Note_: This is a sentence of.

它构造了三类 Markdown 行内格式,构成一组严格的对照实验:

行号写法预期行为
1*Note*: <!-- vale off -->This is a sentence of.<!-- vale on -->斜体前缀 + 行内关闭指令:of.不应触发告警
3**Note**: <!-- vale off -->This is a sentence of.<!-- vale on -->加粗前缀 + 行内关闭指令:of.不应触发告警
5_Note_: This is a sentence of.斜体前缀、无指令:of.必须触发告警

第 5 行是"对照组",用于证明整个文件并没有被整体关闭——第 1、3 行的文本被抑制,不是因为文件失效,而是因为行内指令精确地覆盖了它们。这也正是该夹具"能测出真问题"的关键:一旦实现退化,要么第 1/3 行误报,要么第 5 行漏报。

端到端用例的期望输出

在 testdata/e2e/comments.yaml 中,该夹具被注册为inline-vale-off用例(标注about: "#1001"):

- name: inline-vale-off about: "#1001" args: issue1001.md exit: 0 want: | issue1001.md:5:28:demo.Ending-Preposition:Don't end a sentence with 'of.'

三段语义:

  • args: issue1001.md:将 testdata/fixtures/comments/issue1001.md 作为被测文件(e2e 套件默认dir: fixtures/comments,见文件头部配置);
  • exit: 0:整个运行以零告警级别退出(即没有 error 级别告警);
  • want:期望的唯一告警位于5 行 28 列——demo.Ending-Preposition规则报告Don't end a sentence with 'of.',恰好落在未加指令的对照组_Note_: This is a sentence of.上。

换言之,这个测试断言的是精确位置:第 1、3 行的of.完全不出现,第 5 行的of.必须出现在5:28。位置信息是验证行内指令区域抑制正确性的核心证据。

三、指令语法族:从 off/on 到按规则、按匹配精确控制

夹具只用到vale off/vale on这一对全局开关,但同目录下的综合夹具 testdata/fixtures/comments/test.md 展示了完整的指令语法族。Vale 支持四类指令(对应 internal/core/file.go 中的三个正则):

1. 全局开关

<!-- vale off --> <!-- vale on -->

关闭/打开整个文件的检查,作用于所有规则、所有样式。

2. 按规则开关(vale 规则 = YES|NO)

<!-- vale vale.Redundancy = NO --> <!-- vale demo.Ending-Preposition = YES -->

NO关闭、YES打开某个具体规则。正则:^vale (.+\..+|[^.]+) = (YES|NO|on|off)$,即右侧也接受on/off。

3. 按"匹配文本"开关(vale 规则["片段"] = YES|NO)

<!-- vale vale.Redundancy["ACT test"] = NO --> <!-- vale vale.Redundancy["ACT test","OTHER"] = NO -->

只对规则命中指定匹配文本的告警生效,支持 JSON 数组形式的多个匹配项。正则:^vale (.+\..+)(\[.+\]) = (YES|NO)$。test.md第 15–33 行演示了["ACT test"] = NO抑制、["ACT test"] = YES恢复、["OTHER"] = NO不影响ACT test的精细行为。

4. 按样式开关(vale style(s) = ...)

<!-- vale style = vale --> <!-- vale styles = vale, demo -->

对整个样式(style)生效,可逗号分隔多个。正则:^vale styles? = (.*)$,其中style单数会匹配规则^vale (.+\..+|[^.]+) = (YES|NO|on|off)$,styles复数走专用分支。

各分支的处理逻辑见 internal/core/file.go 的UpdateCommentsAt:指令解析后统一交给setComment(key, disabled, off)写入f.Comments状态表并维护位置区域。

四、核心原理:位置感知的区域(region)机制

vale off/vale on的行内行为之所以可靠,是因为 Vale 不只是翻转一个布尔值,而是把每次指令的字节偏移换算成(行,列)坐标,记录为一段区域(region),最终按坐标过滤告警。三个关键实现如下。

1.setComment:开启与闭合区域

internal/core/file.go:

func (f *File) setComment(key string, disabled bool, off int) { f.Comments[key] = disabled if off < 0 { return } line, span := locFromByteOffset(f.Content, f.lineStarts(f.Content), off, off, 0) at := [2]int{line, span[0]} if disabled { ... f.regions[key] = append(f.regions[key], commentRegion{begin: at, open: true}) } else if n := len(f.regions[key]); n > 0 && f.regions[key][n-1].open { f.regions[key][n-1].end = at f.regions[key][n-1].open = false } }
  • off >= 0时把指令位置换算为 1-based 的(line, column);
  • 关闭指令(如vale off)打开一段open: true的区域,记录begin;
  • 恢复指令(如vale on)把最近的 open 区域闭合,记录end;
  • 若区域一直未闭合(open保持为真),则按注释所述"一直延伸到文件末尾"。

结构体定义在 internal/core/file.go:commentRegion携带begin/end两个[2]int坐标和一个open标志,注释明确指出"一个打开的 region 一直作用到文件结束"。

2.RegionDisabled:按坐标判定告警是否被覆盖

internal/core/file.go:

func (f *File) RegionDisabled(check, match string, line, col int) bool { ... keys := []string{"off", check} if style := StyleName(check); style != check { keys = append(keys, style) } if match != "" { keys = append(keys, check+"["+match+"]") } at := [2]int{line, col} for _, key := range keys { for _, r := range f.regions[key] { after := r.begin[0] < at[0] || (r.begin[0] == at[0] && r.begin[1] <= at[1]) before := r.open || at[0] < r.end[0] || (at[0] == r.end[0] && at[1] < r.end[1]) if after && before { return true } } } return false }
  • 判定优先级依次为off(全局)、规则名(check)、样式名(StyleName推导)、check[匹配文本];
  • after判断告警坐标在区域起点之后,before判断在终点之前(开放区域恒为"之前");
  • 一旦命中任一区域即返回true,表示该告警应被丢弃。

3.DropDisabled:最终过滤告警

internal/core/file.go 的DropDisabled遍历f.Alerts,调用RegionDisabled过滤后原地保留未命中区域的告警。单元测试 internal/core/file_test.go 直接验证了RegionDisabled在区域内外、边界列、多规则名下的判定结果。

五、行内场景为何需要位置:块级解析的"迟到"问题

单纯的状态布尔值(f.Comments["off"])在行内场景是不够的,原因见 internal/lint/ast.go 的注释:

Text inside an activevale offregion is excluded from the block entirely. The whole paragraph is linted at once (on the closing tag), by which point theofftoggle has flipped back, so gating shouldRun isn't enough to suppress inlinevale off...vale onspans -- e.g.*Note*: <!-- vale off -->TODO<!-- vale on -->. See #1001.

翻译过来即:段落整体是在闭合标签处一次性 lint 的,此时vale off/vale on已经"正负抵消",仅凭shouldRun/Comments["off"]这类全局门控无法抑制段落内部夹着的指令片段。因此必须退回到位置记录:

  • 在 HTML token 遍历中,每当遇到注释 token(html.CommentToken),立即调用 internal/lint/ast.go 的f.UpdateCommentsAt(txt, walker.commentAt(txt)),把指令连同其在原文中的字节偏移一起记录(commentAt给出偏移,供setComment换算坐标);
  • 文本追加到块缓冲区时,仅当不在块内且f.Comments["off"]为假才写入(internal/lint/ast.go);行内片段(inline fragment)的捕获同样受f.Comments["off"]门控(internal/lint/ast.go);
  • 最终,在告警集合上执行DropDisabled,用告警自身携带的(行,列)坐标与区域比对,把落在begin与end之间的告警剔除。

这就是issue1001.md中第 1、3 行的of.被抑制、第 5 行的of.保留(精确出现在5:28)的完整机制。

六、周边相关的边界行为(同套件其他用例)

comments.yaml中与行内注释相邻的用例,进一步划清了行为边界:

  • inline-comment-spacing(#882):夹具 testdata/fixtures/comments/issue882.md 表明,注释后的文本保留自己的前导空格,不会与注释前的文本粘连——如sona<em><!--c-->ta</em>中sona与ta不融合成sonata,从而Vale.Spelling能正确命中sona。相关逻辑见 internal/lint/ast.go(注释作为"忠实于源码的边界")。
  • inline-comment-keeps-space:注释从段落中被屏蔽(masked)后,源码中注释前的那段空白仍然分隔两侧单词,因此firstt, <!-- c -->then读作firstt, then而非firstt,then。

这些用例共同验证了"注释既是控制指令,也是文本边界"的双重角色。

七、复现与验证:跑一遍 e2e 用例

仓库的 e2e 套件由 internal/testsuite 驱动,comments.yaml通过dir: fixtures/comments指向夹具目录。可参考 internal/e2e/e2e_test.go 理解套件加载方式。复现思路:

# 在仓库根目录下运行测试套件(需预先配置好 Go 环境) go test ./internal/e2e/...

若只关心本用例,也可直接用 Vale 对夹具跑一次,观察输出是否与want一致:

vale testdata/fixtures/comments/issue1001.md

预期:仅有issue1001.md:5:28:demo.Ending-Preposition:Don't end a sentence with 'of.'一条告警(需要.vale.ini中启用demo.Ending-Preposition对应样式;demo为测试用样式命名空间)。零条或出现 1/3 行的告警,都意味着行内区域抑制回归。

八、实战建议与适用前提

结合本仓库实现,给出编写文档时的注意事项:

  1. 行内指令务必成对:vale off之后的开放区域默认延伸到文件末尾,漏写vale on会让后续整段文档失去检查(见commentRegion.open语义)。
  2. 同一行内不要嵌套多层指令:UpdateCommentsAt对同一 key 维护一个区域栈,成对使用最稳妥;多规则可用vale 规则 = NO替代全局 off,缩小影响面。
  3. 精确到匹配文本:只想放过某个固定措辞时,优先vale 规则["片段"] = NO,而不是关闭整条规则。
  4. 行内元素包裹是安全场景:*Note*:/**Note**:前缀 + 行内vale off...vale on正是 #1001 修复后验证过的组合,可以放心使用。
  5. 位置敏感:RegionDisabled按告警的(行,列)坐标做闭区间判定(起点含、终点不含),因此指令与文本之间的空格位置会直接影响覆盖范围,保持<!-- vale off -->与文本紧邻更可预期。

适用前提说明:本文所述行为以当前仓库(Go 实现、基于 HTML token 遍历 + 区域坐标过滤)为准;不同语言格式(RST 的.. vale ... = NO、MDX 的{/* vale off */}、AsciiDoc 的pass:[<!-- vale off -->])使用各自格式的注释语法,但底层UpdateCommentsAt/RegionDisabled机制一致,可参见 testdata/fixtures/comments/test.rst、testdata/fixtures/comments/test.mdx、testdata/fixtures/comments/test.adoc 等同类夹具。

总结

issue1001.md虽然只有三行,却是 Vale 行内注释指令行为的最小精确验证:它以*Note*/**Note**/_Note_三种行内格式证明,<!-- vale off -->...<!-- vale on -->可以被位置感知的区域机制准确覆盖、按坐标过滤告警,同时不影响同一文件内未加指令文本的正常检查。从 internal/core/file.go 的setComment/RegionDisabled/DropDisabled到 internal/lint/ast.go 的注释 token 捕获,整条调用链展示了"状态布尔值 + 位置区域"双轨设计的必要性——这也解释了为什么行内vale off/vale on在段落整体 lint 的场景下依然可靠。

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

【免费下载链接】vale

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

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

相关推荐

上一篇:TimesFM革命性突破:时间序列预测的范式转变与200M参数模型的颠覆性价值
下一篇:3个终极场景掌握SymPy:让Python成为你的数学计算神器

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

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

康明斯发动机CAN诊断协议实战:从J1939/UDS到可运行脚本

简介&#xff1a;本资源是康明斯Quantum系列发动机专用的诊断与CAN通信技术指南&#xff0c;面向整车厂&#xff08;OEM&#xff09;、工程机械及发电设备集成工程师&#xff0c;解决发动机嵌入式系统对接、故障诊断逻辑理解与J1587/J1939协议落地实施等核心问题。文档为单文件…

作者头像 李华
网站建设 2026/10/19 1:35:55

基于Django的研招管理平台:从需求拆解到部署的全过程复盘

每年9月到次年6月&#xff0c;研招办都是学校里最忙的部门之一。为了把校园研究生招生研招网平台做好&#xff0c;我从Python技术栈入手&#xff0c;先后对比了Django和Flask两套框架&#xff0c;最终用Django完成了一套可稳定运行的招生管理平台。这篇文章就是对这个项目完整过…

作者头像 李华
网站建设 2026/10/18 13:03:30

回放学习的局限性:为什么AI在复杂游戏规则下会卡壳

1. 项目概述&#xff1a;当AI智能体开始“打游戏学做人”最近在几个技术社区刷到一个挺有意思的说法&#xff1a;“清华AAArena里&#xff0c;AI智能体正靠回放学游戏”。标题里那个“Pacman登顶但复杂规则卡壳”&#xff0c;听着像段子&#xff0c;细琢磨却戳中了当前AI智能体…

作者头像 李华
网站建设 2026/10/19 7:26:47

AI工程体系构建:Python+Rust+TypeScript+Julia协同实践

1. 从零构建AI工程体系&#xff1a;这不是写几个模型脚本&#xff0c;而是重建你的技术地基“ai-engineering-from-scratch”这个标题乍看像一句口号&#xff0c;但在我过去十年带过三十多个AI落地项目、亲手重构过七套生产级AI平台之后&#xff0c;我越来越确信&#xff1a;它…

作者头像 李华
网站建设 2026/10/18 19:52:18

FPGA时序建模与硬件映射:从Verilog代码到门级电路的工程实践

简介&#xff1a;本资源是一份面向FPGA初学者与中级工程师的实战经验总结文档&#xff0c;聚焦数字电路设计思维转型、HDL编码规范与系统级优化技巧。内容源自一线工程师十年开发实践&#xff0c;深入剖析Verilog并行思维建模、组合逻辑中case与if-else的综合差异、数学思维简化…

作者头像 李华
网站建设 2026/10/18 4:58:48

AnyPS5跨设备图形串流:低延迟架构与编码优化实战

1. 从“AnyPS5”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“AnyPS5”这个标题&#xff0c;我脑子里冒出来的第一个念头是&#xff1a;这大概率不是一个单纯的硬件改装项目&#xff0c;而是一个围绕“跨平台、跨设备、跨场景”做文章的工具型项目。为什么这么判断…

作者头像 李华