markdownlint-custom-rules

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Markdownlint Custom Rules

Markdownlint 自定义规则

Master creating custom markdownlint rules including rule structure, markdown-it and micromark parser integration, error reporting with fixInfo, and asynchronous rule development.
掌握如何创建Markdownlint自定义规则,包括规则结构、markdown-it与micromark解析器集成、带fixInfo的错误报告以及异步规则开发。

Overview

概述

Markdownlint allows you to create custom rules tailored to your project's specific documentation requirements. Custom rules can enforce project-specific conventions, validate content patterns, and ensure consistency beyond what built-in rules provide.
Markdownlint允许你创建符合项目特定文档需求的自定义规则。自定义规则可以强制执行项目特有的规范、验证内容格式,并确保文档一致性,这些都是内置规则无法完全覆盖的。

Rule Object Structure

规则对象结构

Basic Rule Definition

基础规则定义

Every custom rule must be a JavaScript object with specific properties:
javascript
module.exports = {
  names: ["rule-name", "RULE001"],
  description: "Description of what this rule checks",
  tags: ["custom", "style"],
  parser: "markdownit",
  function: function(params, onError) {
    // Rule implementation
  }
};
每个自定义规则必须是一个包含特定属性的JavaScript对象:
javascript
module.exports = {
  names: ["rule-name", "RULE001"],
  description: "Description of what this rule checks",
  tags: ["custom", "style"],
  parser: "markdownit",
  function: function(params, onError) {
    // Rule implementation
  }
};

Required Properties

必填属性

javascript
{
  names: Array<String>,        // Rule identifiers (required)
  description: String,         // What the rule checks (required)
  tags: Array<String>,         // Categorization tags (required)
  parser: String,              // "markdownit", "micromark", or "none" (required)
  function: Function          // Rule logic (required)
}
javascript
{
  names: Array<String>,        // 规则标识符(必填)
  description: String,         // 规则检查内容描述(必填)
  tags: Array<String>,         // 分类标签(必填)
  parser: String,              // "markdownit"、"micromark"或"none"(必填)
  function: Function          // 规则逻辑(必填)
}

Optional Properties

可选属性

javascript
{
  information: URL,           // Link to rule documentation
  asynchronous: Boolean      // If true, function returns Promise
}
javascript
{
  information: URL,           // 规则文档链接
  asynchronous: Boolean      // 若为true,函数返回Promise
}

Parser Selection

解析器选择

markdown-it Parser

markdown-it 解析器

Best for token-based parsing with rich metadata:
javascript
module.exports = {
  names: ["any-blockquote-markdown-it"],
  description: "Rule that reports an error for any blockquote",
  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: `Blockquote spans ${lines} line(s).`,
        context: blockquote.line
      });
    }
  }
};
最适合带有丰富元数据的基于令牌的解析:
javascript
module.exports = {
  names: ["any-blockquote-markdown-it"],
  description: "Rule that reports an error for any blockquote",
  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: `Blockquote spans ${lines} line(s).`,
        context: blockquote.line
      });
    }
  }
};

micromark Parser

micromark 解析器

Best for detailed token analysis and precise positioning:
javascript
module.exports = {
  names: ["any-blockquote-micromark"],
  description: "Rule that reports an error for any blockquote",
  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: `Blockquote spans ${lines} line(s).`,
        context: params.lines[blockquote.startLine - 1]
      });
    }
  }
};
最适合详细的令牌分析和精确定位:
javascript
module.exports = {
  names: ["any-blockquote-micromark"],
  description: "Rule that reports an error for any blockquote",
  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: `Blockquote spans ${lines} line(s).`,
        context: params.lines[blockquote.startLine - 1]
      });
    }
  }
};

No Parser

无解析器

For simple line-based rules:
javascript
module.exports = {
  names: ["no-todo-comments"],
  description: "Disallow TODO comments in markdown",
  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 comments should be resolved",
          context: line.trim()
        });
      }
    });
  }
};
适用于简单的基于行的规则:
javascript
module.exports = {
  names: ["no-todo-comments"],
  description: "Disallow TODO comments in markdown",
  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 comments should be resolved",
          context: line.trim()
        });
      }
    });
  }
};

Function Parameters

函数参数

params Object

params 对象

The
params
object contains all information about the markdown content:
javascript
function rule(params, onError) {
  // params.name - Input file/string name
  // params.lines - Array of lines (string[])
  // params.frontMatterLines - Lines of front matter
  // params.config - Rule's configuration from .markdownlint.json
  // params.version - markdownlint library version
  // params.parsers - Parser outputs
}
params
对象包含关于Markdown内容的所有信息:
javascript
function rule(params, onError) {
  // params.name - 输入文件/字符串名称
  // params.lines - 行数组 (string[])
  // params.frontMatterLines - 前置元数据行
  // params.config - 来自.markdownlint.json的规则配置
  // params.version - markdownlint库版本
  // params.parsers - 解析器输出结果
}

Accessing Lines

访问行内容

javascript
function: (params, onError) => {
  params.lines.forEach((line, index) => {
    const lineNumber = index + 1;  // Lines are 1-based

    if (someCondition(line)) {
      onError({
        lineNumber,
        detail: "Issue description",
        context: line.trim()
      });
    }
  });
}
javascript
function: (params, onError) => {
  params.lines.forEach((line, index) => {
    const lineNumber = index + 1;  // 行号从1开始

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

Using Configuration

使用配置

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

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

  // Use configuration values
}
javascript
// 在.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;

  // 使用配置值
}

Working with Front Matter

处理前置元数据

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

  if (frontMatterLines.length > 0) {
    // Process YAML front matter
    const frontMatter = frontMatterLines.join('\n');
    // Validate front matter
  }
}
javascript
function: (params, onError) => {
  const frontMatterLines = params.frontMatterLines;

  if (frontMatterLines.length > 0) {
    // 处理YAML前置元数据
    const frontMatter = frontMatterLines.join('\n');
    // 验证前置元数据
  }
}

Error Reporting with onError

使用onError报告错误

Basic Error Reporting

基础错误报告

javascript
onError({
  lineNumber: 5,                          // Required: 1-based line number
  detail: "Line exceeds maximum length",  // Optional: Additional info
  context: "This is the problematic..."   // Optional: Relevant text
});
javascript
onError({
  lineNumber: 5,                          // 必填:从1开始的行号
  detail: "行长度超过最大值",  // 可选:额外信息
  context: "这是有问题的..."   // 可选:相关文本
});

Error with Range

带范围的错误

Highlight specific portion of the line:
javascript
onError({
  lineNumber: 10,
  detail: "Invalid heading format",
  context: "### Heading",
  range: [1, 3]  // Column 1, length 3 (highlights "###")
});
高亮行中的特定部分:
javascript
onError({
  lineNumber: 10,
  detail: "标题格式无效",
  context: "### Heading",
  range: [1, 3]  // 第1列,长度3(高亮"###")
});

Error with Fix Information

带修复信息的错误

Enable automatic fixing:
javascript
onError({
  lineNumber: 15,
  detail: "Extra whitespace",
  context: "  text  ",
  fixInfo: {
    editColumn: 1,
    deleteCount: 2,
    insertText: ""
  }
});
启用自动修复:
javascript
onError({
  lineNumber: 15,
  detail: "多余空格",
  context: "  text  ",
  fixInfo: {
    editColumn: 1,
    deleteCount: 2,
    insertText: ""
  }
});

Automatic Fixing with fixInfo

使用fixInfo实现自动修复

Delete Characters

删除字符

javascript
// Remove 5 characters starting at column 10
fixInfo: {
  lineNumber: 5,
  editColumn: 10,
  deleteCount: 5
}
javascript
// 从第10列开始删除5个字符
fixInfo: {
  lineNumber: 5,
  editColumn: 10,
  deleteCount: 5
}

Insert Text

插入文本

javascript
// Insert text at column 1
fixInfo: {
  lineNumber: 3,
  editColumn: 1,
  insertText: "# "
}
javascript
// 在第1列插入文本
fixInfo: {
  lineNumber: 3,
  editColumn: 1,
  insertText: "# "
}

Replace Text

替换文本

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

Delete Entire Line

删除整行

javascript
// Delete the entire line
fixInfo: {
  lineNumber: 10,
  deleteCount: -1
}
javascript
// 删除整行
fixInfo: {
  lineNumber: 10,
  deleteCount: -1
}

Insert New Line

插入新行

javascript
// Insert a blank line
fixInfo: {
  lineNumber: 8,
  insertText: "\n"
}
javascript
// 插入空行
fixInfo: {
  lineNumber: 8,
  insertText: "\n"
}

Multi-Line Fix

多行修复

Report multiple fixes for the same violation:
javascript
function: (params, onError) => {
  // Fix requires changes on multiple lines
  onError({
    lineNumber: 5,
    detail: "Inconsistent list markers",
    fixInfo: {
      lineNumber: 5,
      editColumn: 1,
      deleteCount: 1,
      insertText: "-"
    }
  });

  onError({
    lineNumber: 6,
    detail: "Inconsistent list markers",
    fixInfo: {
      lineNumber: 6,
      editColumn: 1,
      deleteCount: 1,
      insertText: "-"
    }
  });
}
为同一违规报告多个修复:
javascript
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: "-"
    }
  });
}

Complete Rule Examples

完整规则示例

Enforce Heading Capitalization

强制标题首字母大写

javascript
module.exports = {
  names: ["heading-capitalization", "HC001"],
  description: "Headings must start with a capital letter",
  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: "Heading must start with capital letter",
            context: headingLine,
            range: [hashCount + 2, 1],
            fixInfo: {
              editColumn: hashCount + 2,
              deleteCount: 1,
              insertText: firstChar.toUpperCase()
            }
          });
        }
      }
    }
  }
};
javascript
module.exports = {
  names: ["heading-capitalization", "HC001"],
  description: "Headings must start with a capital letter",
  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: "Heading must start with capital letter",
            context: headingLine,
            range: [hashCount + 2, 1],
            fixInfo: {
              editColumn: hashCount + 2,
              deleteCount: 1,
              insertText: firstChar.toUpperCase()
            }
          });
        }
      }
    }
  }
};

Require Blank Line Before Headings

要求标题前有空行

javascript
module.exports = {
  names: ["blank-line-before-heading", "BLH001"],
  description: "Require blank line before headings (except first line)",
  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;

      // Skip if first line or after front matter
      if (lineNumber <= params.frontMatterLines.length + 1) {
        continue;
      }

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

      if (previousLine.trim() !== "") {
        onError({
          lineNumber: lineNumber - 1,
          detail: "Expected blank line before heading",
          context: previousLine,
          fixInfo: {
            lineNumber: lineNumber - 1,
            editColumn: previousLine.length + 1,
            insertText: "\n"
          }
        });
      }
    }
  }
};
javascript
module.exports = {
  names: ["blank-line-before-heading", "BLH001"],
  description: "Require blank line before headings (except first line)",
  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: "Expected blank line before heading",
          context: previousLine,
          fixInfo: {
            lineNumber: lineNumber - 1,
            editColumn: previousLine.length + 1,
            insertText: "\n"
          }
        });
      }
    }
  }
};

Validate Code Block Language

验证代码块语言

javascript
module.exports = {
  names: ["code-block-language", "CBL001"],
  description: "Code blocks must specify a language",
  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: "Code block must specify a language",
          context: fence.line
        });
      } else if (allowedLanguages.length > 0 && !allowedLanguages.includes(language)) {
        onError({
          lineNumber: fence.lineNumber,
          detail: `Language '${language}' not in allowed list: ${allowedLanguages.join(', ')}`,
          context: fence.line
        });
      }
    }
  }
};
javascript
module.exports = {
  names: ["code-block-language", "CBL001"],
  description: "Code blocks must specify a language",
  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: "Code block must specify a language",
          context: fence.line
        });
      } else if (allowedLanguages.length > 0 && !allowedLanguages.includes(language)) {
        onError({
          lineNumber: fence.lineNumber,
          detail: `Language '${language}' not in allowed list: ${allowedLanguages.join(', ')}`,
          context: fence.line
        });
      }
    }
  }
};

Detect Broken Relative Links

检测无效相对链接

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

module.exports = {
  names: ["no-broken-links", "NBL001"],
  description: "Detect broken relative links",
  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];

        // Only check relative links
        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: `Broken link: ${href}`,
              context: link.line
            });
          }
        }
      }
    }
  }
};
javascript
const fs = require('fs');
const path = require('path');

module.exports = {
  names: ["no-broken-links", "NBL001"],
  description: "Detect broken relative links",
  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: `Broken link: ${href}`,
              context: link.line
            });
          }
        }
      }
    }
  }
};

Enforce Consistent List Markers

强制统一列表标记

javascript
module.exports = {
  names: ["consistent-list-markers", "CLM001"],
  description: "Lists must use consistent markers within the same level",
  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: `Inconsistent list marker: expected '${firstMarker}', found '${marker}'`,
              context: params.lines[item.startLine - 1],
              range: [item.startColumn, 1],
              fixInfo: {
                editColumn: item.startColumn,
                deleteCount: 1,
                insertText: firstMarker
              }
            });
          }
        }
      }
    }
  }
};
javascript
module.exports = {
  names: ["consistent-list-markers", "CLM001"],
  description: "Lists must use consistent markers within the same level",
  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: `Inconsistent list marker: expected '${firstMarker}', found '${marker}'`,
              context: params.lines[item.startLine - 1],
              range: [item.startColumn, 1],
              fixInfo: {
                editColumn: item.startColumn,
                deleteCount: 1,
                insertText: firstMarker
              }
            });
          }
        }
      }
    }
  }
};

Asynchronous Rules

异步规则

Basic Async Rule

基础异步规则

javascript
module.exports = {
  names: ["async-rule-example"],
  description: "Example asynchronous rule",
  tags: ["async", "custom"],
  parser: "none",
  asynchronous: true,
  function: async (params, onError) => {
    // Can use await
    const result = await someAsyncOperation();

    if (!result.valid) {
      onError({
        lineNumber: 1,
        detail: "Async validation failed"
      });
    }

    // Must return Promise (implicitly returned by async function)
  }
};
javascript
module.exports = {
  names: ["async-rule-example"],
  description: "Example asynchronous rule",
  tags: ["async", "custom"],
  parser: "none",
  asynchronous: true,
  function: async (params, onError) => {
    // 可以使用await
    const result = await someAsyncOperation();

    if (!result.valid) {
      onError({
        lineNumber: 1,
        detail: "Async validation failed"
      });
    }

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

Network Validation

网络验证

javascript
const https = require('https');

module.exports = {
  names: ["validate-external-links"],
  description: "Validate external HTTP links return 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: `External link may be broken: ${href}`,
              context: link.line
            });
          }
        }
      }
    }
  }
};
javascript
const https = require('https');

module.exports = {
  names: ["validate-external-links"],
  description: "Validate external HTTP links return 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: `External link may be broken: ${href}`,
              context: link.line
            });
          }
        }
      }
    }
  }
};

Using Custom Rules

使用自定义规则

In Configuration File

在配置文件中

javascript
// .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"]
  }
};
javascript
// .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"]
  }
};

In Node.js Script

在Node.js脚本中

javascript
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());
  }
});
javascript
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());
  }
});

With markdownlint-cli

与markdownlint-cli一起使用

bash
undefined
bash
undefined

Using custom rules with CLI

使用CLI运行自定义规则

markdownlint -c .markdownlint.js -r ./custom-rules/*.js *.md
undefined
markdownlint -c .markdownlint.js -r ./custom-rules/*.js *.md
undefined

TypeScript Support

TypeScript支持

Type-Safe Rule Definition

类型安全的规则定义

typescript
import { Rule } from 'markdownlint';

const rule: Rule = {
  names: ['typescript-rule', 'TS001'],
  description: 'Example TypeScript custom rule',
  tags: ['custom'],
  parser: 'markdownit',
  function: (params, onError) => {
    // Type-safe implementation
    params.parsers.markdownit.tokens.forEach(token => {
      if (token.type === 'heading_open') {
        onError({
          lineNumber: token.lineNumber,
          detail: 'Example error'
        });
      }
    });
  }
};

export default rule;
typescript
import { Rule } from 'markdownlint';

const rule: Rule = {
  names: ['typescript-rule', 'TS001'],
  description: 'Example TypeScript custom rule',
  tags: ['custom'],
  parser: 'markdownit',
  function: (params, onError) => {
    // 类型安全的实现
    params.parsers.markdownit.tokens.forEach(token => {
      if (token.type === 'heading_open') {
        onError({
          lineNumber: token.lineNumber,
          detail: 'Example error'
        });
      }
    });
  }
};

export default rule;

When to Use This Skill

何时使用此技能

  • Enforcing project-specific documentation standards
  • Validating custom markdown patterns
  • Checking domain-specific requirements
  • Extending markdownlint beyond built-in rules
  • Creating reusable rule packages
  • Automating documentation quality checks
  • Implementing team coding standards
  • Building custom linting toolchains
  • 强制执行项目特定的文档标准
  • 验证自定义Markdown格式
  • 检查领域特定的需求
  • 扩展markdownlint的内置规则能力
  • 创建可复用的规则包
  • 自动化文档质量检查
  • 实现团队编码规范
  • 构建自定义linting工具链

Best Practices

最佳实践

  1. Clear Rule Names - Use descriptive names that indicate purpose
  2. Comprehensive Descriptions - Document what the rule checks
  3. Appropriate Tags - Categorize rules for easy filtering
  4. Choose Right Parser - Use markdownit for most cases, micromark for precision
  5. Provide Information URLs - Link to detailed rule documentation
  6. Support Configuration - Allow rule customization via params.config
  7. Helpful Error Messages - Provide clear detail and context
  8. Use Range When Possible - Highlight exact problem location
  9. Implement fixInfo - Enable automatic fixing when possible
  10. Handle Edge Cases - Account for front matter, empty files, etc.
  11. Performance Consideration - Avoid expensive operations in rules
  12. Test Thoroughly - Test with various markdown files
  13. Version Documentation - Document which markdownlint version required
  14. Export Properly - Use module.exports or ES6 exports consistently
  15. Async When Needed - Only use asynchronous for I/O operations
  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操作时使用异步

Common Pitfalls

常见陷阱

  1. Wrong Line Numbers - Forgetting lines are 1-based, not 0-based
  2. Missing Parser - Not specifying parser property
  3. Incorrect Token Types - Using wrong token type names
  4. No Error Context - Not providing helpful context in errors
  5. Synchronous I/O - Using sync functions instead of async
  6. Ignoring Front Matter - Not handling front matter correctly
  7. Hardcoded Values - Not using configuration parameters
  8. Poor Performance - Using inefficient algorithms on large files
  9. Missing Fixability - Not implementing fixInfo when possible
  10. Incomplete Testing - Not testing edge cases and error conditions
  11. Parser Mismatch - Accessing wrong parser output
  12. Column Off-by-One - Columns are 1-based like line numbers
  13. Memory Leaks - Not cleaning up in async rules
  14. Blocking Operations - Long-running synchronous operations
  15. Type Confusion - Mixing up token properties between parsers
  1. 行号错误 - 忘记行号从1开始而非0
  2. 缺少解析器 - 未指定parser属性
  3. 令牌类型错误 - 使用错误的令牌类型名称
  4. 错误无上下文 - 未在错误中提供有用的上下文
  5. 同步I/O - 使用同步函数而非异步函数
  6. 忽略前置元数据 - 未正确处理前置元数据
  7. 硬编码值 - 未使用配置参数
  8. 性能不佳 - 在大文件上使用低效算法
  9. 缺少修复功能 - 未在可能时实现fixInfo
  10. 测试不完整 - 未测试边缘情况和错误条件
  11. 解析器不匹配 - 访问错误的解析器输出
  12. 列数偏差 - 列数与行号一样从1开始
  13. 内存泄漏 - 未在异步规则中清理资源
  14. 阻塞操作 - 长时间运行的同步操作
  15. 令牌属性混淆 - 在不同解析器间混淆令牌属性

Resources

资源