开发工具代码质量CLI【免费下载链接】vale:pencil: A markup-aware linter for prose built with speed and extensibility in mind.项目地址https://gitcode.com/gh_mirrors/va/vale点击查看免费下载导读Vale 是面向散文prose的 markup-aware 命令行检查器linter。本文围绕 testdata/fixtures/scopes/paragraph 这一测试夹具展开深入讲解 Vale 规则中的scope: paragraph它精确覆盖什么文本、刻意排除什么文本以及它与sentence、heading、blockquote等兄弟作用域的分工。读完本文你将能写出只针对正文段落生效的 Vale 规则并能从源码层面理解其匹配原理。一、从一个测试夹具看paragraph作用域的定义1.1 待检查的文档paragraph/test.md 是一份刻意构造的 Markdown 文档依次放入了正文段落、标题、表格、列表和引用块并让ZQX这个占位词出现在每一种结构中--- title: T description: ZQX in frontmatter. --- Body paragraph ZQX here. ## ZQX in a heading | ZQX header | b | | --- | --- | | ZQX body cell | d | - ZQX list item ZQX blockquote注意这个文件的第一段 YAML frontmatterdescription字段里也含有ZQX。也就是说ZQX一共出现在 frontmatter、正文段落、标题、表头、表体、列表项和引用块七个位置而paragraph作用域的规则应当只命中正文段落中的那一次。1.2 配套的检查规则与测试文档同目录的样式 styles/T/Para.yml 定义了本次检查extends: existence message: %s in a paragraph level: error nonword: true scope: paragraph raw: - ZQX关键配置项extends: existence——以正则/短语存在性检查为基底凡是raw中列出内容出现即报错scope: paragraph——把检查范围限定在段落块raw: [ZQX]——直接以ZQX为匹配词raw按字面正则处理不做词形还原nonword: true——允许匹配不被当作单词边界的文本片段level: error——命中的警报级别为 error。1.3 期望结果paragraph只看正文段落配套的端到端用例定义在 testdata/e2e/scopes.yaml 的paragraph一节该用例注释明确指出它对应问题 #1132headings, table cells, list items, blockquotes, and frontmatter are not paragraphs期望输出只有一行test.md:6:16:T.Para:ZQX in a paragraph也就是说第 6 行Body paragraph ZQX here.中的ZQX被命中frontmatter 中的ZQX第 3 行、标题中的ZQX、表格表头与表体中的ZQX、列表项中的ZQX、引用块中的ZQX全部被忽略。这精确定义了 Vale 中paragraph一词的语义它只指文档正文中以空行分隔的散文段落不包含任何看似段落却不是段落的结构。二、paragraph的边界能命中什么、绝不命中什么2.1 作用域匹配矩阵internal/check/scope_test.go 中的TestScopeMatches用一个矩阵把每种作用域的命中范围钉死。针对paragraph的断言是规则作用域块的作用域是否命中paragraphparagraph.text.md✅ 是paragraphtext.md❌ 否paragraphtext.heading.h2.md❌ 否paragraphtext.table.cell.md❌ 否paragraphtext.list.md❌ 否paragraphtext.blockquote.md❌ 否paragraphsentence.text.md❌ 否测试注释点明了根因块作用域的命名规则是text.元素来自解析器paragraph.scope来自段落拆分splittingsentence.scope来自句子切分segmentation。paragraph是一个包装wrapper作用域——它只由正文文本持有标题、表格单元格、列表项和引用块都不会携带paragraph.*这个前缀。作为对照sentence作用域则覆盖所有散文heading、list、table cell 都能被拆成句子这正是 #1124 修复的内容。2.2 与sentence、text的精确分工同一测试文件还揭示了三层作用域的分工text——最宽泛的散文块作用域涵盖几乎所有正文块sentence——对任意散文块做句子切分后生成的子块前缀为sentence.paragraph——只对真正的段落做按空行拆分生成的子块前缀为paragraph.。因此一个规则若希望覆盖列表项、标题、表格单元格中的句子应声明scope: sentence若只想覆盖正文段落才声明scope: paragraph。二者不可互相替代。2.3 注意段落没有句子子块在 internal/lint/ast.go 附近源码注释明确写道Prose, but not paragraphs: a rule scoped toparagraphmust not reach a heading or a table cell. See #1132。也就是说规则声明了paragraph时看到的就是整个段落块本身它不会进一步拆成句子碎片与之相对sentence规则的子块由切分器生成。三、源码级的实现原理3.1 作用域匹配包含关系而非相等internal/check/scope.go 实现了作用域解析与匹配。核心是Selector的包含判断作用域字符串如text.comment.line.py会被拆成若干段sections见Sections()与split()Contains判断规则作用域的每一段是否都出现在块作用域中——只要包含即命中所以paragraph可以命中paragraph.text.md但不会命中不带paragraph.前缀的块。此外Matches里有一个对片段块的特别处理当块的作用域以sentence.开头即它是一个句子碎片而规则并没有点名要sentence时该碎片会被跳过规则去匹配它的父块。这样scope: heading的规则就不会把a.这样的句子片段误当成整个标题来报对应 #1150。3.2paragraph与 NLP 流水线的按需开关Vale 的 NLP 工作不是无脑全开而是按需分配。关键链路在 internal/check/manager.goSplitting: mgr.HasScope(paragraph), Segmentation: mgr.HasScope(sentence),即只有当某条已加载规则声明了paragraph作用域文件才做段落拆分声明了sentence才做句子切分。随后 internal/lint/lint.go 会再做一次闸门file.NLP.Segmentation file.NLP.Segmentation l.runsScoped(file, sentence) file.NLP.Splitting file.NLP.Splitting l.runsScoped(file, paragraph)这意味着你的样式里如果只有scope: paragraph的规则Vale 就只做段落拆分不做句子切分——NLP 工作量被压到最小两种作用域可以共存如scope: paragraph sentence或列表写多个作用域互不干扰。3.3 段落拆分到底怎么做internal/nlp/provider.go 的doNLP展示了段落拆分的具体实现if n.Splitting split { for _, p : range strings.SplitAfter(blk.Text, \n\n) { b : NewLinedBlock(ctx, p, paragraph.blk.Scope, idx) ... } }即按\n\n空行对块文本切分每个片段生成作用域为paragraph.原作用域的子块。而Compute的split参数正是该块是否持有段落的判断——标题、列表项、表格单元格会被传false于是不会产生paragraph.*子块规则自然够不着它们对应 #1132。internal/nlp/provider_test.go 的测试也专门验证了heading 或 table cell 不会被拆出 paragraph 块。3.4 作用域是保留字在 internal/core/scopes.go 中paragraph与text、sentence、heading、blockquote等一起被列为 Vale 内置的保留作用域。这意味着自定义命名作用域config/scopes下的 YAML 映射不能占用paragraph这个名字否则加载时会被拒绝报错信息为heading is a scope Vale defines这类提示见同名 e2e 用例named-reserved。四、实战如何编写和应用paragraph作用域规则4.1 最小可用示例把 styles/T/Para.yml 放入你的样式目录StylesPath指向的目录并在.vale.ini中启用StylesPath styles MinAlertLevel error [*.md] BasedOnStyles T然后对包含多种结构的 Markdown 文档运行vale test.md在 testdata/fixtures/scopes/paragraph/test.md 这样的文档上你只会得到一行错误ZQX in a paragraph且位置指向正文段落所在行6:16标题、frontmatter、表格、列表、引用块里的同名词都不会触发。4.2 常见组合用法paragraph作为块级作用域可以与其他作用域组合成与链scope: paragraph ~heading——命中正文段落且排除标题scope: sentence类规则若想覆盖所有散文包括标题、列表、表格单元格请使用sentence而非paragraph需要同时两种处理时可写scope: paragraph sentence此时段落与句子子块都会被生成。关于与链的语义可参考 testdata/e2e/scopes.yaml 的chained用例#1133它验证了paragraph ~heading、sentence ~blockquote这类链式作用域能正确生效。4.3 与doc(...)选区作用的对照paragraph关注的是Markdown 结构层面的正文段落若你希望按文档语义选区例如h1 之后的第一个段落则应使用doc(...)选择器如scope: doc(h1 p)见 e2e 用例doc-leaf与selectors。两者思路不同前者是解析器的块分类后者是文档视图上的 CSS 选择器可互为补充。五、总结paragraph作用域的语义要点问题结论paragraph覆盖什么以空行分隔的正文段落块作用域带paragraph.前缀paragraph排除什么frontmatter、标题、表格单元格、列表项、引用块见 testdata/e2e/scopes.yaml 的paragraph用例#1132匹配规则段包含关系规则作用域每一段都出现在块作用域中即命中internal/check/scope.goNLP 开关仅在存在paragraph规则时启用段落拆分internal/check/manager.go拆分实现按\n\n切分生成paragraph.scope子块internal/nlp/provider.go与sentence的关系sentence覆盖所有散文的句子paragraph只覆盖真正的段落二者不可互相替代保留字约束paragraph是 Vale 保留作用域不可用作自定义命名作用域internal/core/scopes.go掌握了paragraph与sentence、text等作用域的边界你就能写出只盯着正文段落、不误伤标题和代码块的高精度 Vale 规则——这正是 testdata/fixtures/scopes/paragraph 这套夹具想要固化的行为契约。赞分享开发工具代码质量CLI【免费下载链接】vale:pencil: A markup-aware linter for prose built with speed and extensibility in mind.项目地址https://gitcode.com/gh_mirrors/va/vale点击查看免费下载相关推荐Biome Markdown 格式化器如何规范段落、引用与列表上下文中的 GFM 表格基于 table-after-paragraph 测试用例的源码级解析Biome Markdown 格式化器如何规范段落、引用与列表上下文中的 GFM 表格基于 table after paragraph 测试用例的源码级解析开发工具Lint格式化静态分析代码质量前端Keystone Classic 列表注册详解keystone.List.register() 的工作原理与正确用法Keystone Classic 列表注册详解 keystone.List.register 的工作原理与正确用法 导读 在 Keystone Classic后端Vale 的 list 作用域scope深度解析让规则只检查列表项文本Vale 的 list 作用域scope深度解析让规则只检查列表项文本 Vale 是一款了解标记结构的散文风格检查器它不把文档当作普通字符串去匹配开发工具代码质量CLI上一篇Semantica本体建模实战4段代码跑出可过SHACL校验的.ttl本体下一篇Disruptor-rs与TLA验证如何确保并发算法的正确性创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考