markdownlint-custom-rules
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMarkdownlint 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 object contains all information about the markdown content:
paramsjavascript
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
}paramsjavascript
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
undefinedbash
undefinedUsing custom rules with CLI
使用CLI运行自定义规则
markdownlint -c .markdownlint.js -r ./custom-rules/*.js *.md
undefinedmarkdownlint -c .markdownlint.js -r ./custom-rules/*.js *.md
undefinedTypeScript 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
最佳实践
- Clear Rule Names - Use descriptive names that indicate purpose
- Comprehensive Descriptions - Document what the rule checks
- Appropriate Tags - Categorize rules for easy filtering
- Choose Right Parser - Use markdownit for most cases, micromark for precision
- Provide Information URLs - Link to detailed rule documentation
- Support Configuration - Allow rule customization via params.config
- Helpful Error Messages - Provide clear detail and context
- Use Range When Possible - Highlight exact problem location
- Implement fixInfo - Enable automatic fixing when possible
- Handle Edge Cases - Account for front matter, empty files, etc.
- Performance Consideration - Avoid expensive operations in rules
- Test Thoroughly - Test with various markdown files
- Version Documentation - Document which markdownlint version required
- Export Properly - Use module.exports or ES6 exports consistently
- Async When Needed - Only use asynchronous for I/O operations
- 清晰的规则名称 - 使用能表明用途的描述性名称
- 全面的描述 - 文档化规则的检查内容
- 合适的标签 - 对规则进行分类以便筛选
- 选择正确的解析器 - 大多数情况使用markdownit,需要精确定位时使用micromark
- 提供信息URL - 链接到详细的规则文档
- 支持配置 - 允许通过params.config自定义规则
- 有用的错误消息 - 提供清晰的细节和上下文
- 尽可能使用范围 - 高亮问题的精确位置
- 实现fixInfo - 尽可能启用自动修复
- 处理边缘情况 - 考虑前置元数据、空文件等场景
- 性能考量 - 避免在规则中执行昂贵的操作
- 彻底测试 - 使用各种Markdown文件进行测试
- 版本文档 - 记录所需的markdownlint版本
- 正确导出 - 一致使用module.exports或ES6导出
- 必要时使用异步 - 仅在涉及I/O操作时使用异步
Common Pitfalls
常见陷阱
- Wrong Line Numbers - Forgetting lines are 1-based, not 0-based
- Missing Parser - Not specifying parser property
- Incorrect Token Types - Using wrong token type names
- No Error Context - Not providing helpful context in errors
- Synchronous I/O - Using sync functions instead of async
- Ignoring Front Matter - Not handling front matter correctly
- Hardcoded Values - Not using configuration parameters
- Poor Performance - Using inefficient algorithms on large files
- Missing Fixability - Not implementing fixInfo when possible
- Incomplete Testing - Not testing edge cases and error conditions
- Parser Mismatch - Accessing wrong parser output
- Column Off-by-One - Columns are 1-based like line numbers
- Memory Leaks - Not cleaning up in async rules
- Blocking Operations - Long-running synchronous operations
- Type Confusion - Mixing up token properties between parsers
- 行号错误 - 忘记行号从1开始而非0
- 缺少解析器 - 未指定parser属性
- 令牌类型错误 - 使用错误的令牌类型名称
- 错误无上下文 - 未在错误中提供有用的上下文
- 同步I/O - 使用同步函数而非异步函数
- 忽略前置元数据 - 未正确处理前置元数据
- 硬编码值 - 未使用配置参数
- 性能不佳 - 在大文件上使用低效算法
- 缺少修复功能 - 未在可能时实现fixInfo
- 测试不完整 - 未测试边缘情况和错误条件
- 解析器不匹配 - 访问错误的解析器输出
- 列数偏差 - 列数与行号一样从1开始
- 内存泄漏 - 未在异步规则中清理资源
- 阻塞操作 - 长时间运行的同步操作
- 令牌属性混淆 - 在不同解析器间混淆令牌属性