Introduction
【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum
This file should flag 3 errors due to the "Lesson overview", "Knowledge check", and "Additional resources" sections not containing unordered lists.
Lesson overview
This section contains a general overview of topics that you will learn in this lesson.
Custom section
Text content
Assignment
Knowledge check
The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.
Additional resources
This section contains helpful links to related content. It isn't required, so consider it supplemental.
从文件自身注释即可看出设计意图:它"应当报出 3 个错误",原因是 "Lesson overview"、"Knowledge check"、"Additional resources" 三个小节没有包含无序列表。 ### 2.2 测试断言:精确到行号的错误 该测试用例在 [TOP003.test.js](https://link.gitcode.com/i/8d8551c34478d2c4a3ba7f815efc414d) 中对应如下断言: ```js it("Flags when a section does not have a required unordered list", async () => { const filePath = "./missing_list.md"; const errorPath = join(pathInRepo, filePath); const lintErrors = await getLintErrors(filePath); assert.deepEqual(lintErrors, [ `${errorPath}:7 error ${expected.name} ${expected.description} [Must include an unordered list of lesson overviews in the "lesson overview" section]`, `${errorPath}:23 error ${expected.name} ${expected.description} [Must include an unordered list of knowledge checks in the "knowledge check" section]`, `${errorPath}:27 error ${expected.name} ${expected.description} [Must include an unordered list of additional resources in the "additional resources" section]`, ]); });对照文件行号:
- L7是
### Lesson overview之后的第一个内容行(默认内容文本所在行),报错:Must include an unordered list of lesson overviews in the "lesson overview" section; - L23是
### Knowledge check之后的第一个内容行,报错:Must include an unordered list of knowledge checks in the "knowledge check" section; - L27是
### Additional resources之后的第一个内容行,报错:Must include an unordered list of additional resources in the "additional resources" section。
注意两个细节:
- 报错定位在"小节内第一个内容 token"所在行,而不是小节标题行。这是源码中
tokensAfterFirstContent[0] || tokensAfterHeading[0]的取值逻辑(见 TOP003_defaultSectionContent.js); - 列表名称做了复数化处理:源码中
listItemsName = section + (section.endsWith("s") ? "" : "s"),于是lesson overview→lesson overviews、knowledge check→knowledge checks、additional resources→additional resources(本身以 s 结尾,不再追加)。
2.3 修复方式:手动补一个无序列表
missing_list.md没有对应的自动修复用例(fixLintErrors只覆盖ordered_list.md、incorrect_content.md、content_around_list.md三个文件,见 TOP003.test.js)。要人工修复,只需在每个小节标题与默认内容之后补充一个无序列表,例如将Lesson overview小节改为:
### Lesson overview This section contains a general overview of topics that you will learn in this lesson. - LO item.对照valid.md(tests/valid.md)可知,符合规范的小节结构是"标题 → 空行 → 默认内容 → 空行 → 无序列表项"。
三、规则源码结构:从 token 流到错误集合
3.1 整体执行流程
TOP003 的主函数定义在 TOP003_defaultSectionContent.js:
function: function TOP003(params, onError) { const { tokens } = params.parsers.markdownit; const headingTokenIndices = tokens .filter((token) => token.type === "heading_open") .map((headingToken) => tokens.indexOf(headingToken)); const totalErrors = []; headingTokenIndices.forEach((tokenIndexValue, arrIndex, tokenIndicesArr) => { const headingContent = tokens[tokenIndexValue].line .replace(/\#+\s/g, "") .toLowerCase(); if (!Object.values(sectionsWithDefaultContent).includes(headingContent)) { return; } // ...按小节类型分发检查 }); totalErrors.forEach((error) => { onError(error); }); }执行链路可概括为四步:
- 取 token 流:从
params.parsers.markdownit取出 markdown-it 解析后的完整 token 数组; - 定位所有标题:筛选
heading_open类型 token,记录其在 token 数组中的下标; - 匹配目标小节:将标题行内容去掉
#前缀并转小写,与sectionsWithDefaultContent中的四个值比对,不匹配(如Introduction、Custom section)直接跳过; - 按小节类型分发:对目标小节取"从该标题到下一个标题之间"的 token 切片,若小节为空则报"cannot be empty",否则按类型进入
getListSectionErrors(Lesson overview / Knowledge check / Additional resources)或getAssignmentSectionErrors(Assignment)。
3.2 目标小节与默认内容的集中定义
规则顶部集中维护了两张表(TOP003_defaultSectionContent.js):
const sectionsWithDefaultContent = { lessonOverview: "lesson overview", assignment: "assignment", knowledgeCheck: "knowledge check", additionalResources: "additional resources", }; const listSectionsDefaultContent = { [sectionsWithDefaultContent.lessonOverview]: "This section contains a general overview of topics that you will learn in this lesson.", [sectionsWithDefaultContent.knowledgeCheck]: "The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge.", [sectionsWithDefaultContent.additionalResources]: "This section contains helpful links to related content. It isn't required, so consider it supplemental.", };这里有两个容易被忽略的细节:
assignment小节没有默认文本,它只做 div 包装器检查,不参与默认内容匹配(因为不同课程的 Assignment 内容差异很大,无法统一);- 三个列表小节的默认文本在
valid.md与missing_list.md中逐字一致,必须完全匹配,任何措辞改动(哪怕只是把isn't改成is not)都会触发"默认内容不正确"类错误(该分支的判例见 tests/incorrect_content.md 与对应的 fixed_incorrect_content.md)。
四、getListSectionErrors:列表小节的六类检查
getListSectionErrors(TOP003_defaultSectionContent.js)是 TOP003 中最复杂的函数,对每个列表小节依次执行以下六类检查,累积返回错误数组。
4.1 检查一:禁止嵌套列表
const listItemTokens = tokensAfterHeading.filter( (token) => token.type === "list_item_open", ); const nestedListItemTokens = listItemTokens.filter( (token) => token.level > 1, ); nestedListItemTokens.forEach((nestedListItemToken) => { listSectionErrors.push( createErrorObject( nestedListItemToken.lineNumber, `The ${section} section must not contain nested lists.`, ), ); });实现要点:取标题之后所有list_item_opentoken,凡是token.level > 1(markdown-it 中嵌套层级大于 1)即视为嵌套列表项。该分支不提供自动修复,因为仅"取消缩进"未必能解决问题,可能需要整体删除某些列表项。
测试样例 tests/nested_list.md 在 L10 和 L36 分别构造了 Lesson overview 与 Additional resources 的嵌套列表项,对应断言见 TOP003.test.js:
[The lesson overview section must not contain nested lists.] [The additional resources section must not contain nested lists.]4.2 检查二:禁止有序列表
const orderedListItemRegex = /^\d+\.\s*/; const orderedListItemTokens = listItemTokens.filter((token) => orderedListItemRegex.test(token.line), ); orderedListItemTokens.forEach((orderedListItemToken) => { listSectionErrors.push( createErrorObject( orderedListItemToken.lineNumber, `The ${section} section must not include any ordered lists.`, { lineNumber: orderedListItemToken.lineNumber, deleteCount: orderedListItemToken.line.match(orderedListItemRegex)[0].length, insertText: "- ", }, ), ); });实现要点:用正则^\d+\.\s*匹配以数字加点号开头的列表项(如1. KC item),命中即报错,并附带自动修复:删除数字编号前缀(deleteCount为匹配到的数字编号长度),替换为-,从而把有序列表转换为无序列表。
源码注释特别提到(TOP003_defaultSectionContent.js):该正则只对顶层列表项做测试,因为嵌套列表项在 token 对象中的line前面带有缩进空格,^\d+\.锚点会失配——这是有意为之,避免与"禁止嵌套列表"的检查产生重复报错。
测试样例 tests/ordered_list.md 在 L27、L28 构造了两个有序列表项,其 lint 断言(TOP003.test.js)会同时报出两类错误:
[The knowledge check section must not include any ordered lists.] [Must include an unordered list of knowledge checks in the "knowledge check" section]而 fix 测试(Converts ordered lists to unordered lists,TOP003.test.js)验证npm run lint -- --format修复后输出与 tests/fixed_ordered_list.md 完全一致。
4.3 检查三:默认内容必须紧跟标题
const defaultContentOpenTokenIndex = tokensAfterHeading.findIndex( (token) => token.line === listSectionsDefaultContent[section], ); if (defaultContentOpenTokenIndex > 0) { listSectionErrors.push( createErrorObject( defaultContentToken.lineNumber, `Expected default section content to come immediately after the ${section} heading.`, ), ); }实现要点:在标题之后的 token 中查找与默认内容文本逐字相等的行;若它不是第一个内容 token(index > 0),说明标题与默认内容之间插入了其他内容,报错 "Expected default section content to come immediately after the {section} heading."。
4.4 检查四:默认内容缺失或被替换
if (defaultContentOpenTokenIndex === -1) { const sectionStartsWithList = tokensAfterHeading[0].line.startsWith("- "); const errorDetail = sectionStartsWithList ? `Expect default content to precede unordered list of ${listItemsName}: "${listSectionsDefaultContent[section]}"` : `Expected: "${listSectionsDefaultContent[section]}"; Actual: "${tokensAfterHeading[0].line}"`; let replacementText = listSectionsDefaultContent[section]; if (sectionStartsWithList) { replacementText += `\n\n${tokensAfterHeading[0].line}`; } // ... }实现要点:当整个小节都找不到默认内容文本时:
- 若小节以列表开头,报错信息提示"期望默认内容位于 {listItemsName} 无序列表之前",修复策略是把默认内容插入到列表之前(
replacementText = 默认内容 + "\n\n" + 原列表首行); - 若小节以其他文本开头,则报错
Expected: "默认内容"; Actual: "实际首行文本",修复策略是整体替换首个内容 token。
incorrect_content.md的 L7 正是"默认内容被错误文本替换"的判例,其断言(TOP003.test.js)为:
[Expected: "This section contains a general overview of topics that you will learn in this lesson."; Actual: "This section has the wrong text following the heading that should flag an error."]而incorrect_content.md的 L25 则是"小节以列表开头、缺少默认内容"的判例:
[Expect default content to precede unordered list of knowledge checks: "The following questions are an opportunity to reflect on key topics in this lesson. ..."]对应的 fix 测试Inserts/replaces missing or incorrect default section content(TOP003.test.js)验证修复结果与 tests/fixed_incorrect_content.md 逐字节一致——在该文件中,Knowledge check 小节被修复为"默认内容 + 空行 + 列表项"的结构。
4.5 检查五:默认内容之后只允许无序列表
if ( defaultContentOpenTokenIndex === 0 && tokensAfterFirstContent.length && !tokensAfterFirstContent[0].type.endsWith("_list_open") ) { listSectionErrors.push( createErrorObject( tokensAfterFirstContent[0].lineNumber, `Only an unordered list of ${listItemsName} can follow the default content.`, { lineNumber: ..., deleteCount: WHOLE_LINE }, ), ); }实现要点:当默认内容位于小节开头(index === 0)且其后还有内容时,紧跟默认内容的必须是无序列表;若是段落等其他 token 类型,则报错 "Only an unordered list of {listItemsName} can follow the default content.",并整行删除该非法内容(deleteCount: WHOLE_LINE,即 -1)。
4.6 检查六:列表之后禁止追加内容
const lastBulletListCloseIndex = sectionTokens.findLastIndex( (token) => token.type === "bullet_list_close", ); if ( bulletListOpenTokenIndex !== -1 && lastBulletListCloseIndex !== sectionTokens.length - 1 ) { const tokensAfterBulletListClose = sectionTokens.slice(lastBulletListCloseIndex + 1); listSectionErrors.push( createErrorObject( tokensAfterBulletListClose[0].lineNumber, `There should be no additional content after the unordered list of ${listItemsName}`, { lineNumber: ..., deleteCount: WHOLE_LINE }, ), ); }实现要点:找出小节内最后一个bullet_list_closetoken,如果其后还有内容(即它不是小节的最后一个 token),则报错并整行删除多余内容。
检查五与检查六合在一起,共同保证 "Lesson overview"、"Knowledge check"、"Additional resources" 三个小节的规范形态是唯一且确定的:标题 → 默认内容 → 无序列表 → 小节结束。
这两个检查的判例集中在 tests/content_around_list.md(L9 在列表前插入文本、L13 在列表后插入文本),断言见 TOP003.test.js:
[Only an unordered list of lesson overviews can follow the default content.] [There should be no additional content after the unordered list of lesson overviews]对应的 fix 测试Removes flagged content around default section lists(TOP003.test.js)验证修复结果与 tests/fixed_content_around_list.md 一致,即删除列表前后的多余内容。
4.7 "缺少列表"主错误的判定逻辑
回到missing_list.md触发的主错误,其判定位于 TOP003_defaultSectionContent.js:
const tokensAfterFirstContent = tokensAfterHeading.slice( tokensAfterHeading.findIndex( (token, _index, arr) => token.type === arr[0].type.replace("_open", "_close"), ) + 1, ); const bulletListOpenTokenIndex = sectionTokens.findIndex( (token) => token.type === "bullet_list_open", ); if ( (defaultContentOpenTokenIndex === 0 && !tokensAfterFirstContent.length) || bulletListOpenTokenIndex === -1 ) { const tokenLineNumber = ( tokensAfterFirstContent[0] || tokensAfterHeading[0] ).lineNumber; const errorDetail = `Must include an unordered list of ${listItemsName} in the "${section}" section`; listSectionErrors.push(createErrorObject(tokenLineNumber, errorDetail)); }逻辑拆解:
tokensAfterFirstContent是"第一个内容 token 闭合之后"的剩余 token;若默认内容恰好在标题后第一项且其后没有内容,说明"只有默认内容、没有列表";bulletListOpenTokenIndex === -1表示整个小节内不存在任何无序列表(bullet_list_opentoken),这是missing_list.md中三个小节共同的情形;- 只要满足任一条件,就报 "Must include an unordered list of {listItemsName} in the "{section}" section",错误定位行取
tokensAfterFirstContent[0](有后续内容时)或tokensAfterHeading[0](无后续内容时,即默认内容所在行)。
这正是missing_list.md中 L7、L23、L27 三个报错点位的由来:默认内容行就是标题之后的第一个内容 token 所在行,且小节内没有bullet_list_open,因此三条错误全部落位在默认内容文本行。
五、getAssignmentSectionErrors:Assignment 的 div 包装器检查
getAssignmentSectionErrors(TOP003_defaultSectionContent.js)只做一件事:确认 Assignment 小节内存在带正确属性的 HTML div:
const divBlockTokens = sectionTokens.filter( (token) => token.type === "html_block" && token.content.startsWith("<div"), ); const hasAssignmentDiv = divBlockTokens.some( (token) => token.content.includes(`class="lesson-content__panel"`) && token.content.includes(`markdown="1"`), ); if (!divBlockTokens || !hasAssignmentDiv) { assignmentErrors.push( createErrorObject( sectionTokens[0].lineNumber, `Assignment sections must include an HTML div element with class="lesson-content__panel" and markdown="1" attributes`, ), ); }要点:
- 该规则依赖 markdown-it 将
<div ...>...</div>解析为html_blocktoken(仓库中的合法写法参见 tests/valid.md L17-L21,即<div class="lesson-content__panel" markdown="1">包裹的块); - 判定条件非常严格:
class="lesson-content__panel"与markdown="1"两个属性必须同时存在; - 报错文案为 "Assignment sections must include an HTML div element with class="lesson-content__panel" and markdown="1" attributes",错误定位在小节标题行;
- 该分支没有自动修复——TOP003 的 docs 也明确指出并非所有 Assignment 内容都必须包在这个 div 里,但它必须存在于该小节中("it must at least exist in this section")。
判例 tests/missing_wrapper.md 在 L17 直接书写普通段落文本,缺少 div 包装器,断言见 TOP003.test.js。
六、空小节与"不适用内容"的边界
6.1 空小节单独报错
主函数中有一段独立于上述检查的分支(TOP003_defaultSectionContent.js):
const isSectionEmpty = tokensBetweenHeadings.at(-1).type === "heading_close"; if (isSectionEmpty) { totalErrors.push( createErrorObject( tokensBetweenHeadings[0].lineNumber, `The ${headingContent} section cannot be empty`, ), ); }实现要点:若"标题到下一标题之间"的最后一个 token 是heading_close,说明该小节没有任何内容,直接报 "The {heading} section cannot be empty",不再进入后续细分检查。
判例 tests/empty_section.md 构造了三个空小节,断言见 TOP003.test.js:
[The lesson overview section cannot be empty] [The knowledge check section cannot be empty] [The additional resources section cannot be empty]6.2 非目标小节完全不受影响
Introduction、Custom section等不在sectionsWithDefaultContent中的小节会被return提前跳过。这也是missing_list.md刻意保留### Custom section(L9-L10)的原因:证明规则只作用于四个内置小节,普通小节无论写什么都不会被 TOP003 报错。
6.3 合规样例:valid.md 的结构模板
tests/valid.md 是"零错误"的黄金标准(对应断言Does not flag any errors if no violations,TOP003.test.js),其结构可直接作为课程作者与贡献者的模板:
### Introduction Text content ### Lesson overview This section contains a general overview of topics that you will learn in this lesson. - LO item. ### Custom section Text content ### Assignment <div class="lesson-content__panel" markdown="1"> Assignment content </div> ### Knowledge check The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge. - KC item对照可见四个内置小节各自的合规形态:
- Lesson overview:默认内容 +
- LO item.无序列表; - Assignment:
<div class="lesson-content__panel" markdown="1">包裹内容; - Knowledge check:默认内容 +
- KC item无序列表(在 valid.md 中该小节后无 Additional resources,但该小节本身就是文档结尾,列表之后无多余内容,因此通过检查)。
七、运行与验证方式
7.1 手动运行 lint / fix
在仓库根目录执行:
# 对单个文件做 lint 检查(返回退出码非 0 表示存在违规) npm run lint -- "markdownlint/TOP003_defaultSectionContent/tests/missing_list.md" # 以 --format 模式输出修复后的内容(不落盘,供测试比对) npm run lint -- --format # 自动修复所有可修复问题(如有序列表转无序、默认内容替换/插入、删除列表前后多余内容) npm run fixnpm run lint实际执行markdownlint-cli2(见 package.json 的 scripts);- 测试工具 test_utils/lint.js 封装了
npm run lint -- "<file>",并将stderr按行拆分返回错误数组;test_utils/fix.js 则以npm run lint -- --format拿到修复后的内容并剥离 markdownlint-cli2 的横幅输出; - 需要说明:仓库是只读的,以上命令用于本地查看与验证;对仓库内容的任何修改请通过贡献流程(见 CONTRIBUTING.md)进行。
7.2 运行规则测试
使用 Node 内置测试运行器执行 TOP003 的全部 lint 与 fix 用例:
npm test【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考