Claude
Skills
Sign in
Back

markdownlint-custom-rules

Included with Lifetime
$97 forever

Create custom linting rules for markdownlint including rule structure, parser integration, error reporting, and automatic fixing.

Data & Analytics

What this skill does


# Markdownlint Custom Rules

Master creating custom markdownlint rules including rule structure, markdown-it and micromark parser integration, error reporting with fixInfo, and asynchronous rule development.

## 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.

## 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
  }
};
```

### 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)
}
```

### Optional Properties

```javascript
{
  information: URL,           // Link to rule documentation
  asynchronous: Boolean      // If true, function returns Promise
}
```

## Parser Selection

### markdown-it Parser

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
      });
    }
  }
};
```

### micromark Parser

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]
      });
    }
  }
};
```

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

## Function Parameters

### params Object

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
}
```

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

### 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
}
```

### 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
  }
}
```

## Error Reporting with 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
});
```

### 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 "###")
});
```

### Error with Fix Information

Enable automatic fixing:

```javascript
onError({
  lineNumber: 15,
  detail: "Extra whitespace",
  context: "  text  ",
  fixInfo: {
    editColumn: 1,
    deleteCount: 2,
    insertText: ""
  }
});
```

## Automatic Fixing with fixInfo

### Delete Characters

```javascript
// Remove 5 characters starting at column 10
fixInfo: {
  lineNumber: 5,
  editColumn: 10,
  deleteCount: 5
}
```

### Insert Text

```javascript
// Insert text at column 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"
}
```

### Delete Entire Line

```javascript
// Delete the entire line
fixInfo: {
  lineNumber: 10,
  deleteCount: -1
}
```

### Insert New Line

```javascript
// Insert a blank line
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: "-"
    }
  });
}
```

## 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: {
          

Related in Data & Analytics