NotepadNext 词法测试剖析:以 HeaderEOLFill_1.md 验证 lexer.markdown.header.eolfill 属性的实现与预期输出
【免费下载链接】NotepadNextA cross-platform, reimplementation of Notepad++项目地址: https://gitcode.com/GitHub_Trending/no/NotepadNext
本文以 Lexilla 测试套件中的 Markdown 测试样例 HeaderEOLFill_1.md 为主体,完整讲解它如何通过 Lexilla TestLexers 框架验证 Markdown 词法器的lexer.markdown.header.eolfill属性;读完后你能掌握该属性的行为语义、.styled预期输出文件的读法,以及 LexMarkdown.cxx 中对应的源码级实现路径,从而理解 NotepadNext 中 Scintilla/Lexilla 词法引擎的测试组织方式。
一、HeaderEOLFill_1.md:一个词法器属性回归测试的输入文件
HeaderEOLFill_1.md 是 Lexilla 测试目录thirdparty/lexilla/test/examples/markdown/下的一个 Markdown 样例文件,它不是给人阅读的文档,而是 TestLexers 词法测试程序的"输入"。根据 test/README 的说明,examples目录下每个子目录包含样例文件、一个控制文件SciTE.properties,以及带.styled(预期着色结果)和.folded(预期折叠结果)后缀的期望文件;程序对样例执行词法分析后与.styled文件逐字符比对,不一致时会生成.styled.new文件供人工检查。
该样例专门服务于一个主题:Markdown 词法器的lexer.markdown.header.eolfill(header EOL fill,标题行行尾填充)属性,对应 Lexilla 上游 issue #60("Markdown: Optionally style all of Markdown header lines",见 LexillaHistory.html 中的版本记录)。目录下存在成对的两个测试用例:
- HeaderEOLFill_0.md:在
lexer.markdown.header.eolfill=0(默认值)下运行; - HeaderEOLFill_1.md:在
lexer.markdown.header.eolfill=1下运行。
两者的文件内容完全相同,差异只在于SciTE.properties按文件名条件下发的属性值,从而把同一个输入在两种属性取值下的着色结果分别固化为两个.styled期望文件,形成对属性行为的"双重回归验证"。
二、样例全文:两种 Markdown 标题语法的穷举组合
HeaderEOLFill_1.md 全文共 20 行,其内容刻意覆盖了两类 Markdown 标题语法在"有/无空行分隔"下的全部组合,如下所示(原样引用):
H1 == H2 -- # H1 ## H2 H1 == H2 -- # H1 ## H2 ### H3 #### H4 ##### H5 ###### H6可以将其拆成两个测试区块:
- 带空行的规范写法(第 1–9 行):Setext 风格的一级标题(
H1+==)、二级标题(H2+--),以及 ATX 风格的# H1、## H2。其中 Setext 标题要求上一行是正文内容,==/--仅是下划线行,这正好考验词法器"回溯上一行是否有内容"的判断逻辑。 - 无空行连写的紧凑写法(第 11–20 行):
H1/==、H2/--紧贴,随后# H1到###### H6六级 ATX 标题连续排列,用于验证在标题状态尚未回到LINE_BEGIN的情况下,后续行能否被正确重新识别为新的标题。
三、SciTE.properties:按文件名条件化下发属性
该目录下的 SciTE.properties 全文如下,它同时展示了测试框架的"条件属性"语法(此条件语法正是为 issue #62 在 TestLexers 中引入的if $(=与match表达式,见 LexillaHistory.html):
code.page=65001 lexer.*.md=markdown fold=1 # Tests for the lexer.markdown.header.eolfill property, issue #62 if $(= $(FileNameExt);HeaderEOLFill_0.md) lexer.markdown.header.eolfill=0 if $(= $(FileNameExt);HeaderEOLFill_1.md) lexer.markdown.header.eolfill=1逐项解读:
| 配置项 | 含义 |
|---|---|
code.page=65001 | 以 UTF-8 编码读取样例文件 |
lexer.*.md=markdown | 对所有.md样例启用markdown词法器(Lexilla 中由lmMarkdown模块注册,见 LexMarkdown.cxx 末尾的LexerModule lmMarkdown(SCLEX_MARKDOWN, ColorizeMarkdownDoc, "markdown")) |
fold=1 | 启用折叠测试,与.folded期望文件配套 |
if $(= $(FileNameExt);...) | TestLexers 的条件表达式:当"文件名(含扩展名)"等于右侧字符串时,对当前文件追加设置其下方的属性行 |
按 test/README 的说明,if $(=与match(glob 匹配,如match Header*1.md)是等价的两种按文件下发属性的写法;此外 README 还指出,其他既非 lexer/keywords 前缀的语句会被原样转发为词法器属性,这正是lexer.markdown.header.eolfill能够直达LexMarkdown.cxx的通道。
四、预期着色输出:.styled 文件逐行解读
.styled文件的格式是"原文 + 花括号中的样式号切换标记"(如{5}function{0},说明见 test/README)。本测试用到的样式号来自 SciLexer.h 中的 Markdown 样式定义:
| 样式号 | 宏 | 含义 |
|---|---|---|
| 0 | SCE_MARKDOWN_DEFAULT | 默认正文 |
| 1 | SCE_MARKDOWN_LINE_BEGIN | 行起始(逻辑状态) |
| 6 | SCE_MARKDOWN_HEADER1 | 一级标题 |
| 7 | SCE_MARKDOWN_HEADER2 | 二级标题 |
| 8–11 | SCE_MARKDOWN_HEADER3–HEADER6 | 三至六级标题 |
eolfill=0 的期望结果
HeaderEOLFill_0.md.styled 关键行(节选,{n}表示"从此处起样式切换为 n"):
{0}H1{1} {6}=={1} {0}H2{1} {7}--{1} {6}#{0} H1{1} {7}##{0} H2{1} ... {6}#{0} H1{1} {7}##{0} H2{1} {8}###{0} H3{1}默认行为是只着色语法标记本身:ATX 标题中#、##等井号取HEADER1/HEADER2(6/7),而标题文字H1回落到SCE_MARKDOWN_DEFAULT(0);Setext 的下划线行==/--整行为 6/7。注意==行末尾出现的{1}:它是紧接其后的LINE_BEGIN状态的标记,按.styled的紧凑写法附在前一行行尾。
eolfill=1 的期望结果
HeaderEOLFill_1.md.styled 对同一输入的输出(关键行节选):
{0}H1{1} {6}== {1} {0}H2{1} {7}-- {1} {6}# H1 {1} {7}## H2 {1} {0}H1{1} {6}== {0}H2{1} {7}-- {6}# H1 {7}## H2 {8}### H3 {9}#### H4 {10}##### H5 {11}###### H6与eolfill=0相比,差异正是该属性的全部语义:
- ATX 标题整行(井号 + 空格 + 标题文字)都取标题样式:
{6}# H1、{7}## H2,一直到{11}###### H6,而不是只有井号着色; - Setext 下划线行
==/--在两种模式下都整行着色,但eolfill=1时LINE_BEGIN({1})被推迟到下一行的行首才出现(如==行末不再有{1},而是下一空行以{1}开头),因为词法器在整个标题行内保持HEADER1/HEADER2状态直到换行。
这正是 LexillaHistory.html 中 "Optionally style all of Markdown header lines. Enabled withlexer.markdown.header.eolfill=1" 的可执行定义。
五、源码级实现:LexMarkdown.cxx 中属性如何改变状态机
属性在词法器中的解析只有一处,位于 LexMarkdown.cxx:
// property lexer.markdown.header.eolfill // Set to 1 to highlight all ATX header text. const bool headerEOLFill = styler.GetPropertyInt("lexer.markdown.header.eolfill", 0) == 1;从源码结构看,ColorizeMarkdownDoc主循环中该布尔值在三类分支上生效:
- ATX 标题识别(
#…######):见 LexMarkdown.cxx。eolfill=1时直接sc.SetState(SCE_MARKDOWN_HEADERn),让标题状态覆盖整行;eolfill=0时改走SetStateAndZoom(...),只对井号段赋标题样式、其余回落默认样式——对应.styled中{6}#{0} H1与{6}# H1的差别。 - Setext 下划线行(
=/-):见 LexMarkdown.cxx。两个分支都先用HasPrevLineContent(sc)确认上一行有正文、再用FollowToLineEnd(...)把下划线整行设为HEADER1/HEADER2;差异仅在后处理:eolfill=0时立刻sc.SetState(SCE_MARKDOWN_LINE_BEGIN)(对应==行尾的{1}),eolfill=1时保留标题状态跨到换行。 - 标题状态的行尾收束:见 LexMarkdown.cxx。
eolfill=1时,词法器在标题状态内每遇到行首(sc.atLineStart)才置回SCE_MARKDOWN_LINE_BEGIN并通过freezeCursor让当前循环不前进,重新走一遍行首识别;eolfill=0时则遇到换行符即收束。这段逻辑解释了紧凑区块(第 11–20 行)为何在eolfill=1下每一行仍能独立成为新标题。
另有一个细节值得注意:#分支里优先处理#.这种"井号点"有序列表的特殊情形(LexMarkdown.cxx),避免把列表项误判为一级标题;而-分支则要先排除单横线+空格的无序列表(SCE_MARKDOWN_PRECHAR路径)。这些判断保证了HeaderEOLFill样例中--与H2的组合不会触发列表语义。
六、测试如何运行与校验:整篇与逐行双通道
按 test/README 的描述,TestLexers 对每个样例执行两轮校验:先对整文件做词法/折叠,再逐行(line-by-line)重做;两者结果不一致时输出per-line is different提示,这通常暴露词法器里未正确初始化的局部状态。对 Markdown 词法器而言,eolfill=1模式恰好依赖"标题状态跨行保持 + 行首重新进入识别"的跨行状态机,因此HeaderEOLFill_1.md这个用例实际上同时承担了整篇一致性与逐行一致性的双重压力测试。
构建与运行方式(以当前仓库自带文件为准):先构建 Lexilla 共享库,再进入lexilla/test目录,Linux/macOS 下用make test(Clang 可加CLANG=1),Windows 下用nmake -f testlexers.mak test或加载TestLexers.vcxproj;要求 C++20 编译器,README 列出 MSVC 2019.4、GCC 9.0、Clang 9.0、Apple Clang 11.0 为已验证版本。若结果变化,程序会写出.styled.new/.folded.new,人工核对无误后可将其提升(promote)为正式的期望文件提交。
七、小结
HeaderEOLFill_1.md 这 20 行看似简陋的样例,完整编码了lexer.markdown.header.eolfill属性的验收标准:Setext 与 ATX 两种标题语法 × 规范/紧凑两种排版 × 属性开/关两个取值。它与 HeaderEOLFill_0.md、条件化的 SciTE.properties 及两份.styled期望文件共同构成 Lexilla 测试框架内一个自洽的回归测试单元,而其行为最终锚定在 LexMarkdown.cxx 对 ATX 识别、Setext 下划线行与标题状态行尾收束三处分支的实现之上。在 NotepadNext 这类基于 Scintilla 的编辑器中,理解这条"样例 → SciTE.properties → 词法器属性 → 样式号"的链路,是定制 Markdown 高亮行为(例如希望标题整行高亮)与阅读其测试体系的共同基础。
【免费下载链接】NotepadNextA cross-platform, reimplementation of Notepad++项目地址: https://gitcode.com/GitHub_Trending/no/NotepadNext
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考