Markdownlint自定义规则创建Skill markdownlint-custom-rules

这个技能专注于创建Markdownlint的自定义规则,包括定义规则对象结构、集成markdown-it和micromark解析器、报告错误并提供自动修复功能。它用于增强Markdown文档的linting能力,支持项目特定的文档标准、验证内容模式、确保一致性,并可用于自动化文档质量检查和团队编码标准实施。关键词:Markdownlint, 自定义规则, 规则开发, 解析器集成, 错误报告, 自动修复, 文档质量, 软件开发, 自动化测试。

测试 0 次安装 0 次浏览 更新于 3/25/2026

name: markdownlint-custom-rules user-invocable: false description: 创建自定义的markdownlint规则,包括规则结构、解析器集成、错误报告和自动修复。 allowed-tools: [Bash, Read]

Markdownlint 自定义规则

掌握创建自定义markdownlint规则,包括规则结构、markdown-it和micromark解析器集成、带fixInfo的错误报告以及异步规则开发。

概述

Markdownlint允许您创建自定义规则,以适应项目的特定文档要求。自定义规则可以强制执行项目特定的约定、验证内容模式并确保超出内置规则提供的一致性。

规则对象结构

基本规则定义

每个自定义规则必须是一个具有特定属性的JavaScript对象:

module.exports = {
  names: ["rule-name", "RULE001"],
  description: "此规则检查的描述",
  tags: ["custom", "style"],
  parser: "markdownit",
  function: function(params, onError) {
    // 规则实现
  }
};

必需属性

{
  names: Array<String>,        // 规则标识符(必需)
  description: String,         // 规则检查内容(必需)
  tags: Array<String>,         // 分类标签(必需)
  parser: String,              // "markdownit"、"micromark"或"none"(必需)
  function: Function          // 规则逻辑(必需)
}

可选属性

{
  information: URL,           // 规则文档链接
  asynchronous: Boolean      // 如果为true,函数返回Promise
}

解析器选择

markdown-it 解析器

最适合基于令牌的解析,具有丰富的元数据:

module.exports = {
  names: ["any-blockquote-markdown-it"],
  description: "报告任何块引用的错误的规则",
  information: new URL("https://example.com/rules/any-blockquote"),
  tags: ["test"],
  parser: "markdownit",
  function: (params, onError) => {
    const blockquotes = params.parsers.markdownit.tokens
      .filter((token) => token.type === "blockquote_open");

    for (const blockquote of blockquotes) {
      const [startIndex, endIndex] = blockquote.map;
      const lines = endIndex - startIndex;

      onError({
        lineNumber: blockquote.lineNumber,
        detail: `块引用跨越${lines}行。`,
        context: blockquote.line
      });
    }
  }
};

micromark 解析器

最适合详细的令牌分析和精确定位:

module.exports = {
  names: ["any-blockquote-micromark"],
  description: "报告任何块引用的错误的规则",
  information: new URL("https://example.com/rules/any-blockquote"),
  tags: ["test"],
  parser: "micromark",
  function: (params, onError) => {
    const blockquotes = params.parsers.micromark.tokens
      .filter((token) => token.type === "blockQuote");

    for (const blockquote of blockquotes) {
      const lines = blockquote.endLine - blockquote.startLine + 1;

      onError({
        lineNumber: blockquote.startLine,
        detail: `块引用跨越${lines}行。`,
        context: params.lines[blockquote.startLine - 1]
      });
    }
  }
};

无解析器

适用于简单的基于行的规则:

module.exports = {
  names: ["no-todo-comments"],
  description: "禁止在markdown中使用TODO注释",
  tags: ["custom"],
  parser: "none",
  function: (params, onError) => {
    params.lines.forEach((line, index) => {
      if (line.includes("TODO:") || line.includes("FIXME:")) {
        onError({
          lineNumber: index + 1,
          detail: "应解决TODO/FIXME注释",
          context: line.trim()
        });
      }
    });
  }
};

函数参数

params 对象

params 对象包含关于markdown内容的所有信息:

function rule(params, onError) {
  // params.name - 输入文件/字符串名称
  // params.lines - 行数组(string[])
  // params.frontMatterLines - 前导符行
  // params.config - 来自 .markdownlint.json 的规则配置
  // params.version - markdownlint库版本
  // params.parsers - 解析器输出
}

访问行

function: (params, onError) => {
  params.lines.forEach((line, index) => {
    const lineNumber = index + 1;  // 行基于1

    if (someCondition(line)) {
      onError({
        lineNumber,
        detail: "问题描述",
        context: line.trim()
      });
    }
  });
}

使用配置

// 在 .markdownlint.json 中
{
  "custom-rule": {
    "max_length": 50,
    "pattern": "^[A-Z]"
  }
}

// 在规则中
function: (params, onError) => {
  const config = params.config || {};
  const maxLength = config.max_length || 40;
  const pattern = config.pattern ? new RegExp(config.pattern) : null;

  // 使用配置值
}

处理前导符

function: (params, onError) => {
  const frontMatterLines = params.frontMatterLines;

  if (frontMatterLines.length > 0) {
    // 处理YAML前导符
    const frontMatter = frontMatterLines.join('
');
    // 验证前导符
  }
}

错误报告与 onError

基本错误报告

onError({
  lineNumber: 5,                          // 必需:基于1的行号
  detail: "行超过最大长度",               // 可选:附加信息
  context: "这是有问题的..."            // 可选:相关文本
});

带范围的错误

高亮行的特定部分:

onError({
  lineNumber: 10,
  detail: "无效的标题格式",
  context: "### 标题",
  range: [1, 3]  // 列1,长度3(高亮"###")
});

带修复信息的错误

启用自动修复:

onError({
  lineNumber: 15,
  detail: "额外空白",
  context: "  文本  ",
  fixInfo: {
    editColumn: 1,
    deleteCount: 2,
    insertText: ""
  }
});

自动修复与 fixInfo

删除字符

// 从列10开始删除5个字符
fixInfo: {
  lineNumber: 5,
  editColumn: 10,
  deleteCount: 5
}

插入文本

// 在列1插入文本
fixInfo: {
  lineNumber: 3,
  editColumn: 1,
  insertText: "# "
}

替换文本

// 用新文本替换3个字符
fixInfo: {
  lineNumber: 7,
  editColumn: 5,
  deleteCount: 3,
  insertText: "new"
}

删除整行

// 删除整行
fixInfo: {
  lineNumber: 10,
  deleteCount: -1
}

插入新行

// 插入空行
fixInfo: {
  lineNumber: 8,
  insertText: "
"
}

多行修复

为同一违规报告多个修复:

function: (params, onError) => {
  // 修复需要在多行进行更改
  onError({
    lineNumber: 5,
    detail: "不一致的列表标记",
    fixInfo: {
      lineNumber: 5,
      editColumn: 1,
      deleteCount: 1,
      insertText: "-"
    }
  });

  onError({
    lineNumber: 6,
    detail: "不一致的列表标记",
    fixInfo: {
      lineNumber: 6,
      editColumn: 1,
      deleteCount: 1,
      insertText: "-"
    }
  });
}

完整规则示例

强制标题大写

module.exports = {
  names: ["heading-capitalization", "HC001"],
  description: "标题必须以大写字母开头",
  tags: ["headings", "custom"],
  parser: "markdownit",
  function: (params, onError) => {
    const headings = params.parsers.markdownit.tokens
      .filter(token => token.type === "heading_open");

    for (const heading of headings) {
      const headingLine = params.lines[heading.lineNumber - 1];
      const match = headingLine.match(/^#+\s+(.+)$/);

      if (match) {
        const text = match[1];
        const firstChar = text.charAt(0);

        if (firstChar !== firstChar.toUpperCase()) {
          const hashCount = headingLine.indexOf(' ');

          onError({
            lineNumber: heading.lineNumber,
            detail: "标题必须以大写字母开头",
            context: headingLine,
            range: [hashCount + 2, 1],
            fixInfo: {
              editColumn: hashCount + 2,
              deleteCount: 1,
              insertText: firstChar.toUpperCase()
            }
          });
        }
      }
    }
  }
};

要求标题前有空行

module.exports = {
  names: ["blank-line-before-heading", "BLH001"],
  description: "要求标题前有空行(首行除外)",
  tags: ["headings", "custom", "whitespace"],
  parser: "markdownit",
  function: (params, onError) => {
    const headings = params.parsers.markdownit.tokens
      .filter(token => token.type === "heading_open");

    for (const heading of headings) {
      const lineNumber = heading.lineNumber;

      // 如果是首行或在前导符后跳过
      if (lineNumber <= params.frontMatterLines.length + 1) {
        continue;
      }

      const previousLine = params.lines[lineNumber - 2];

      if (previousLine.trim() !== "") {
        onError({
          lineNumber: lineNumber - 1,
          detail: "期望标题前有空行",
          context: previousLine,
          fixInfo: {
            lineNumber: lineNumber - 1,
            editColumn: previousLine.length + 1,
            insertText: "
"
          }
        });
      }
    }
  }
};

验证代码块语言

module.exports = {
  names: ["code-block-language", "CBL001"],
  description: "代码块必须指定语言",
  tags: ["code", "custom"],
  parser: "markdownit",
  function: (params, onError) => {
    const config = params.config || {};
    const allowedLanguages = config.allowed_languages || [];

    const fences = params.parsers.markdownit.tokens
      .filter(token => token.type === "fence");

    for (const fence of fences) {
      const language = fence.info.trim();

      if (!language) {
        onError({
          lineNumber: fence.lineNumber,
          detail: "代码块必须指定语言",
          context: fence.line
        });
      } else if (allowedLanguages.length > 0 && !allowedLanguages.includes(language)) {
        onError({
          lineNumber: fence.lineNumber,
          detail: `语言'${language}'不在允许列表中:${allowedLanguages.join(', ')}`,
          context: fence.line
        });
      }
    }
  }
};

检测损坏的相对链接

const fs = require('fs');
const path = require('path');

module.exports = {
  names: ["no-broken-links", "NBL001"],
  description: "检测损坏的相对链接",
  tags: ["links", "custom"],
  parser: "markdownit",
  asynchronous: true,
  function: async (params, onError) => {
    const links = params.parsers.markdownit.tokens
      .filter(token => token.type === "link_open");

    for (const link of links) {
      const hrefToken = link.attrs.find(attr => attr[0] === "href");

      if (hrefToken) {
        const href = hrefToken[1];

        // 仅检查相对链接
        if (!href.startsWith('http://') && !href.startsWith('https://')) {
          const filePath = path.join(path.dirname(params.name), href);

          try {
            await fs.promises.access(filePath);
          } catch (err) {
            onError({
              lineNumber: link.lineNumber,
              detail: `损坏的链接:${href}`,
              context: link.line
            });
          }
        }
      }
    }
  }
};

强制一致的列表标记

module.exports = {
  names: ["consistent-list-markers", "CLM001"],
  description: "列表必须在同一级别使用一致的标记",
  tags: ["lists", "custom"],
  parser: "micromark",
  function: (params, onError) => {
    const lists = params.parsers.micromark.tokens
      .filter(token => token.type === "listUnordered");

    for (const list of lists) {
      const items = params.parsers.micromark.tokens.filter(
        token => token.type === "listItemMarker" &&
                 token.startLine >= list.startLine &&
                 token.endLine <= list.endLine
      );

      if (items.length > 0) {
        const firstMarker = params.lines[items[0].startLine - 1]
          .charAt(items[0].startColumn - 1);

        for (const item of items.slice(1)) {
          const marker = params.lines[item.startLine - 1]
            .charAt(item.startColumn - 1);

          if (marker !== firstMarker) {
            onError({
              lineNumber: item.startLine,
              detail: `不一致的列表标记:期望'${firstMarker}',找到'${marker}'`,
              context: params.lines[item.startLine - 1],
              range: [item.startColumn, 1],
              fixInfo: {
                editColumn: item.startColumn,
                deleteCount: 1,
                insertText: firstMarker
              }
            });
          }
        }
      }
    }
  }
};

异步规则

基本异步规则

module.exports = {
  names: ["async-rule-example"],
  description: "示例异步规则",
  tags: ["async", "custom"],
  parser: "none",
  asynchronous: true,
  function: async (params, onError) => {
    // 可以使用await
    const result = await someAsyncOperation();

    if (!result.valid) {
      onError({
        lineNumber: 1,
        detail: "异步验证失败"
      });
    }

    // 必须返回Promise(由async函数隐式返回)
  }
};

网络验证

const https = require('https');

module.exports = {
  names: ["validate-external-links"],
  description: "验证外部HTTP链接返回200",
  tags: ["links", "async"],
  parser: "markdownit",
  asynchronous: true,
  function: async (params, onError) => {
    const links = params.parsers.markdownit.tokens
      .filter(token => token.type === "link_open");

    const checkLink = (url) => {
      return new Promise((resolve) => {
        https.get(url, (res) => {
          resolve(res.statusCode === 200);
        }).on('error', () => {
          resolve(false);
        });
      });
    };

    for (const link of links) {
      const hrefToken = link.attrs.find(attr => attr[0] === "href");

      if (hrefToken) {
        const href = hrefToken[1];

        if (href.startsWith('http://') || href.startsWith('https://')) {
          const valid = await checkLink(href);

          if (!valid) {
            onError({
              lineNumber: link.lineNumber,
              detail: `外部链接可能损坏:${href}`,
              context: link.line
            });
          }
        }
      }
    }
  }
};

使用自定义规则

在配置文件中

// .markdownlint.js
const customRules = require('./custom-rules');

module.exports = {
  default: true,
  customRules: [
    customRules.headingCapitalization,
    customRules.blankLineBeforeHeading,
    customRules.codeBlockLanguage
  ],
  "heading-capitalization": true,
  "blank-line-before-heading": true,
  "code-block-language": {
    "allowed_languages": ["javascript", "typescript", "bash", "json"]
  }
};

在Node.js脚本中

const markdownlint = require('markdownlint');
const customRules = require('./custom-rules');

const options = {
  files: ['README.md'],
  customRules: [
    customRules.headingCapitalization,
    customRules.blankLineBeforeHeading
  ],
  config: {
    default: true,
    "heading-capitalization": true,
    "blank-line-before-heading": true
  }
};

markdownlint(options, (err, result) => {
  if (!err) {
    console.log(result.toString());
  }
});

使用 markdownlint-cli

# 使用CLI自定义规则
markdownlint -c .markdownlint.js -r ./custom-rules/*.js *.md

TypeScript 支持

类型安全规则定义

import { Rule } from 'markdownlint';

const rule: Rule = {
  names: ['typescript-rule', 'TS001'],
  description: '示例TypeScript自定义规则',
  tags: ['custom'],
  parser: 'markdownit',
  function: (params, onError) => {
    // 类型安全实现
    params.parsers.markdownit.tokens.forEach(token => {
      if (token.type === 'heading_open') {
        onError({
          lineNumber: token.lineNumber,
          detail: '示例错误'
        });
      }
    });
  }
};

export default rule;

何时使用此技能

  • 强制执行项目特定的文档标准
  • 验证自定义markdown模式
  • 检查领域特定要求
  • 扩展markdownlint超越内置规则
  • 创建可重用的规则包
  • 自动化文档质量检查
  • 实施团队编码标准
  • 构建自定义linting工具链

最佳实践

  1. 清晰的规则名称 - 使用描述性名称指示目的
  2. 全面的描述 - 记录规则检查内容
  3. 适当的标签 - 分类规则以便轻松过滤
  4. 选择正确的解析器 - 大多数情况使用markdownit,精确定位使用micromark
  5. 提供信息URL - 链接到详细规则文档
  6. 支持配置 - 允许通过params.config自定义规则
  7. 有帮助的错误消息 - 提供清晰的细节和上下文
  8. 尽可能使用范围 - 高亮确切问题位置
  9. 实现fixInfo - 尽可能启用自动修复
  10. 处理边缘情况 - 考虑前导符、空文件等
  11. 性能考虑 - 避免在规则中执行昂贵操作
  12. 全面测试 - 使用各种markdown文件测试
  13. 版本文档 - 记录所需的markdownlint版本
  14. 正确导出 - 一致使用module.exports或ES6导出
  15. 需要时使用异步 - 仅对I/O操作使用异步

常见陷阱

  1. 错误行号 - 忘记行基于1,不是基于0
  2. 缺少解析器 - 未指定解析器属性
  3. 不正确的令牌类型 - 使用错误的令牌类型名称
  4. 无错误上下文 - 未在错误中提供有帮助的上下文
  5. 同步I/O - 使用同步函数而不是异步
  6. 忽略前导符 - 未正确处理前导符
  7. 硬编码值 - 未使用配置参数
  8. 性能差 - 在大文件上使用低效算法
  9. 缺少可修复性 - 可能时未实现fixInfo
  10. 不完全测试 - 未测试边缘情况和错误条件
  11. 解析器不匹配 - 访问错误的解析器输出
  12. 列偏移1 - 列基于1,如行号
  13. 内存泄漏 - 在异步规则中未清理
  14. 阻塞操作 - 长时间运行的同步操作
  15. 类型混淆 - 在解析器之间混淆令牌属性

资源