# Comparison with markdownlint

This document provides a detailed comparison between rumdl and markdownlint, covering rule compatibility, intentional design differences, and features unique to each tool.

## Quick Summary

rumdl offers **high markdownlint compatibility with intentional differences** while also adding performance improvements and newer features. All 53 markdownlint rules are implemented, but rumdl prefers predictable CommonMark-oriented behavior over bug-for-bug compatibility in a few documented areas.

**Key Differences:**

- **Performance**: rumdl is significantly faster (30-100x in many cases) thanks to Rust and intelligent caching
- **Rule Coverage**: All 53 markdownlint rules are implemented, with a small number of intentional behavioral differences documented below
- **Unique Features**: 18 additional rules (MD057, MD061-MD077), built-in LSP server, VS Code extension, 6 Markdown flavors
- **Configuration**: Automatic markdownlint config discovery and conversion

## Rule Coverage

### Implemented Rules

rumdl implements **71 rules total**: all 53 markdownlint rules plus 18 unique rules.

**Markdownlint-compatible rules (53):** All markdownlint rules are implemented with full compatibility. See the [Rules Reference](rules.md) for the complete list.

**Note:** Rule numbers MD001-MD060 have gaps (MD002, MD006, MD008, MD015-MD017 were never implemented in markdownlint). rumdl maintains these gaps for compatibility.

### Rules Unique to rumdl

rumdl implements 18 additional rules not found in markdownlint:

| Rule   | Name                           | Description                                                |
| ------ | ------------------------------ | ---------------------------------------------------------- |
| MD057  | Relative links                 | Validates that relative file links point to existing files |
| MD061  | Forbidden terms                | Flags usage of configurable forbidden terms                |
| MD062  | Link destination whitespace    | No whitespace in link destinations                         |
| MD063  | Heading capitalization         | Enforces consistent heading capitalization style           |
| MD064  | No multiple consecutive spaces | Flags multiple consecutive spaces in content               |
| MD065  | Blanks around horizontal rules | Horizontal rules should have surrounding blank lines       |
| MD066  | Footnote validation            | Validates footnote references have definitions             |
| MD067  | Footnote definition order      | Footnotes should appear in order of reference              |
| MD068  | Empty footnote definitions     | Footnote definitions should not be empty                   |
| MD069  | No duplicate list markers      | Flags duplicate markers like `- - text` from copy-paste    |
| MD070  | Nested code fence              | Detects nested fence collisions (opt-in)                   |
| MD071  | Blank line after frontmatter   | Frontmatter should be followed by a blank line             |
| MD072  | Frontmatter key sort           | Frontmatter keys should be sorted (opt-in)                 |
| MD073  | TOC validation                 | Table of Contents should match headings (opt-in)           |
| MD074  | MkDocs nav validation          | Validates MkDocs nav entries against the docs tree         |
| MD075  | Orphaned table rows            | Detects headerless pipe tables and orphaned table rows     |
| MD076  | List item spacing              | Enforces consistent blank lines between list items         |
| MD077  | List continuation indent       | Enforces indentation for list continuation content         |

**Opt-in rules:** MD060, MD063, MD070, MD072, MD073, and MD074 are disabled by default. Enable them explicitly in your configuration.

## Intentional Design Differences

### 1. CommonMark Specification Compliance

**rumdl prioritizes CommonMark specification compliance** over bug-for-bug compatibility with markdownlint's parsing.

**Example - List Continuation vs Code Blocks:**

<!-- markdownlint-disable MD046 -->

```markdown
1. List item

    This is a continuation paragraph (4 spaces = continuation)

        This is a code block (8 spaces = continuation indent + 4)
```

<!-- markdownlint-enable MD046 -->

- **markdownlint**: May incorrectly treat 4-space indented paragraphs as code blocks
- **rumdl**: Follows CommonMark: 4 spaces = list continuation, 8 spaces = code block within list
- **Rationale**: Reduces false positives and aligns with the official Markdown spec

**References:**

- [CommonMark List Specification](https://spec.commonmark.org/0.31.2/#lists)
- [rumdl Issue #128](https://github.com/rvben/rumdl/issues/128) - False positive fix

### 2. Performance Architecture

**rumdl uses Rust and intelligent caching** for significant performance gains:

- **Cold start**: 30-100x faster than markdownlint on large repositories
- **Incremental**: Only re-lints changed files (Ruff-style caching)
- **Parallel processing**: Multi-threaded file processing and rule execution
- **Zero dependencies**: Single binary, no Node.js runtime required

**Benchmark:** See the [performance comparison](../README.md#performance) in the main README, which shows detailed benchmarks on the Rust Book repository (478 markdown files).

### 3. Auto-fix Mode Differences

Both tools support auto-fixing, but with different philosophies:

**markdownlint:**

- Fixes issues in-place
- Requires `--fix` flag

**rumdl:**

- Two modes: `rumdl fmt` (formatter-style, exits 0) and `rumdl check --fix` (linter-style, exits 0 if all violations fixed, 1 if violations remain)
- `--diff` mode to preview changes
- Parallel file fixing (4.8x faster on multi-file projects)

**Why two modes?**

- `fmt`: Designed for editor integration (doesn't fail on unfixable issues)
- `check --fix`: Designed for CI/CD (fails if violations remain after fixing)

### 4. Configuration Philosophy

**Automatic Discovery:**

rumdl automatically discovers and loads both markdownlint and markdownlint-cli2 config files:

```bash
# rumdl automatically finds and uses these (in precedence order):
.markdownlint-cli2.jsonc
.markdownlint-cli2.yaml
.markdownlint-cli2.yml
.markdownlint.json
.markdownlint.yaml
markdownlint.json
```

For markdownlint-cli2 files, rumdl extracts rule configuration from the `config:` key and ignores cli2-specific keys like `globs` and `ignores`.

**Conversion Tool:**

```bash
# Convert markdownlint config to rumdl format:
rumdl import .markdownlint.json --output .rumdl.toml
```

**Multiple Formats:**

- Native: `.rumdl.toml` (TOML, with JSON schema support)
- Python projects: `pyproject.toml` with `[tool.rumdl]` section
- Markdownlint: Automatic compatibility mode

### 5. Editor Integration

**rumdl includes a built-in Language Server Protocol (LSP) implementation:**

```bash
# Start LSP server
rumdl server

# Install VS Code extension
rumdl vscode
```

**Features:**

- Real-time linting as you type
- Quick fixes for supported rules
- Hover documentation for rules
- Zero configuration required

### 6. Markdown Flavors

**rumdl supports 6 Markdown flavors** to adapt rule behavior for different documentation systems:

| Flavor     | Use Case                     | Key Adjustments                          |
| ---------- | ---------------------------- | ---------------------------------------- |
| `standard` | Default Markdown             | CommonMark + GFM extensions              |
| `gfm`      | GitHub Flavored Markdown     | Security-sensitive HTML, autolinks       |
| `mkdocs`   | MkDocs / Material for MkDocs | Admonitions, tabs, mkdocstrings          |
| `mdx`      | MDX (JSX in Markdown)        | JSX components, ESM imports              |
| `obsidian` | Obsidian knowledge base      | Callouts, Dataview, Templater, wikilinks |
| `quarto`   | Quarto / RMarkdown           | Citations, shortcodes, executable code   |

**Configuration:**

```toml
[global]
flavor = "mkdocs"

[per-file-flavor]
"docs/**/*.md" = "mkdocs"
"**/*.mdx" = "mdx"
```

markdownlint does not have built-in flavor support; users must configure individual rules manually.

## Configuration Compatibility

### Markdownlint Config Auto-Detection

rumdl automatically discovers and loads markdownlint and markdownlint-cli2 configurations:

```yaml
# .markdownlint.yaml (automatically loaded)
MD013: false
MD033:
  allowed_elements: ['br', 'img']
```

```yaml
# .markdownlint-cli2.yaml (also automatically loaded)
config:
  MD013: false
  MD033:
    allowed_elements: ['br', 'img']
```

### Equivalent rumdl Configuration

```toml
# .rumdl.toml
[global]
disable = ["MD013"]

[MD033]
allowed_elements = ["br", "img"]
```

### Configuration Mapping

Most markdownlint options map directly to rumdl:

| markdownlint                 | rumdl                  |
| ---------------------------- | ---------------------- |
| `default: true`              | `[global]` section     |
| Rule by number (`MD013`)     | Same (`[MD013]`)       |
| Rule by name (`line-length`) | Same (`[line-length]`) |
| Disabling: `"MD013": false`  | `disable = ["MD013"]`  |

### Per-File Ignores

Both support per-file rule configuration:

```toml
# rumdl
[per-file-ignores]
"README.md" = ["MD033"]  # Allow HTML in README
"docs/api/**/*.md" = ["MD013"]  # Relax line length in API docs
```

markdownlint uses glob patterns in separate config files or inline comments.

## Inline Configuration Compatibility

rumdl supports both `rumdl` and `markdownlint` inline comment styles:

```markdown
<!-- markdownlint-disable MD013 -->
This line can be as long as needed
<!-- markdownlint-enable MD013 -->

<!-- rumdl-disable MD013 -->
Alternative syntax also supported
<!-- rumdl-enable MD013 -->
```

Both syntaxes work identically in rumdl for seamless migration.

## CLI Differences

### Command Structure

**markdownlint:**

```bash
markdownlint README.md
markdownlint --fix **/*.md
markdownlint --config .markdownlint.json docs/
```

**rumdl:**

```bash
rumdl check README.md
rumdl check --fix .  # or: rumdl fmt .
rumdl check --config .rumdl.toml docs/
```

### Output Formats

Both support:

- Text output (colored, human-readable)
- JSON output (for tool integration)

rumdl additionally supports:

- Source line display with caret underlines (`--output-format full`)
- GitHub Actions annotations (`--output-format github`)
- GitLab, Azure, SARIF, JUnit, and Pylint formats
- Statistics summary (`--statistics`)
- Profiling information (`--profile`)

### Exit Codes

**markdownlint-cli:**

- `0`: No violations
- `1`: Violations found
- `2`: Unable to write output
- `3`: Unable to load custom rules
- `4`: Unexpected error

**rumdl:**

- `0`: Success (or `rumdl fmt` completed successfully)
- `1`: Violations found (or remain after `--fix`)
- `2`: Tool error

## Migration Guide

### From markdownlint to rumdl

1. **Install rumdl:**

    ```bash
   uv tool install rumdl
   # or: cargo install rumdl
   # or: pip install rumdl
   # or: brew install rumdl (See README.md)
    ```

2. **Test with existing config:**

    ```bash
   # rumdl automatically discovers .markdownlint.json
   rumdl check .
    ```

3. **(Optional) Convert config:**

    ```bash
   rumdl import .markdownlint.json
    ```

4. **Update CI/CD:**

    ```yaml
   # Before (markdownlint)
   - run: markdownlint '**/*.md'

   # After (rumdl)
   - run: rumdl check .
    ```

### Known Behavioral Differences

These are intentional deviations where rumdl produces different results than markdownlint. They are design decisions, not bugs.

**MD004 (unordered-list-style):** In `consistent` mode, rumdl uses prevalence-based detection (most common marker wins, ties prefer dash). markdownlint uses the first marker as the standard.

**MD005/MD007 (list-indent / ul-indent):** rumdl uses parent-based dynamic indentation, properly handling ordered lists with variable marker widths (e.g., `1.` vs `10.`). markdownlint may treat
children at different indentation levels as inconsistent.

**MD012 (no-multiple-blanks):** rumdl uses the `filtered_lines()` architecture to skip frontmatter, code blocks, and flavor-specific constructs. This may produce slightly different counts near block
boundaries.

**MD013 (line-length):** rumdl exempts entire lines that are completely unbreakable (URLs with no spaces, long code spans). It also supports `line_length = 0` to mean unlimited. markdownlint exempts
more selectively. rumdl supports markdownlint's `stern`, `heading_line_length`, and `code_block_line_length` options for context-specific limits and a stricter trailing-token policy. One known
divergence: markdownlint exempts heading lines that contain any link, autolink, or image (its `linkOnlyLineNumbers` covers headings because heading text is not classified as paragraph data); rumdl
applies its URL/inline-link suppression to heading lines but does not exempt heading lines that contain bare autolinks. Affected lines can be tagged with `<!-- rumdl-disable-line MD013 -->` if needed.

**MD027 (no-multiple-space-blockquote):** rumdl's `list-items` option defaults to `false`; markdownlint's `list_items` defaults to `true`. List items inside blockquotes inherently need extra
indentation (`>  - item`, continuation lines), so flagging them by default produces noise. Set `list-items = true` to opt into strict markdownlint behavior.

**MD029 (ordered-list-prefix):** rumdl uses CommonMark AST start values. A list starting at `11` expects items 11, 12, 13. rumdl only auto-fixes when `start_value == 1` to preserve explicit numbering
intent.

**MD051 (link-fragments):** rumdl's `ignore-case` option defaults to `true`; markdownlint's `ignore_case` defaults to `false`. Permissive matching better fits multi-platform docs (where some
processors lowercase fragments and others don't), and it preserves rumdl's long-standing behavior. Set `ignore-case = false` to opt into strict markdownlint parity. The companion `ignored-pattern`
option matches markdownlint exactly.

If you encounter other compatibility issues, please [file an issue](https://github.com/rvben/rumdl/issues).

## Feature Comparison Table

| Feature                  | markdownlint       | rumdl                       |
| ------------------------ | ------------------ | --------------------------- |
| **Core Functionality**   |                    |                             |
| Rule count               | 53 implemented     | 71 (53 compatible + 18 new) |
| Auto-fix                 | ✅                 | ✅                          |
| Configuration file       | ✅ JSON/YAML       | ✅ TOML/JSON/JSONC/YAML/cli2 |
| Inline config            | ✅                 | ✅ (compatible)             |
| Custom rules             | ✅ (JavaScript)    | ❌                          |
| Markdown flavors         | ❌                 | ✅ 6 flavors                |
| **Performance**          |                    |                             |
| Single file              | Fast               | Very Fast (10-30x)          |
| Large repos (100+ files) | Slow               | Very Fast (30-100x)         |
| Incremental mode         | ❌                 | ✅ (caching)                |
| Parallel processing      | Partial            | ✅ Full                     |
| **Developer Experience** |                    |                             |
| Built-in LSP             | ❌                 | ✅                          |
| VS Code extension        | ✅ (separate)      | ✅ (built-in)               |
| Watch mode               | Via external tools | ✅ `--watch`                |
| Stdin/stdout             | ✅                 | ✅                          |
| Diff preview             | ❌                 | ✅ `--diff`                 |
| **Installation**         |                    |                             |
| Node.js required         | ✅                 | ❌                          |
| Python pip               | ❌                 | ✅                          |
| Rust cargo               | ❌                 | ✅                          |
| Single binary            | ❌                 | ✅                          |
| Homebrew                 | ✅                 | ✅                          |
| **Output & Integration** |                    |                             |
| Text format              | ✅                 | ✅                          |
| JSON format              | ✅                 | ✅                          |
| GitHub Actions           | ✅                 | ✅ Enhanced                 |
| Statistics               | ❌                 | ✅                          |
| Profiling                | ❌                 | ✅                          |

## CommonMark Compliance

rumdl prioritizes **CommonMark specification compliance** to reduce false positives and align with modern Markdown standards:

| Aspect                    | markdownlint    | rumdl                                  |
| ------------------------- | --------------- | -------------------------------------- |
| List continuation indent  | Custom logic    | CommonMark spec                        |
| Code blocks in lists      | May misdetect   | Spec-compliant (8 spaces)              |
| Heading anchor generation | GitHub-flavored | Multiple styles (GitHub, GitLab, etc.) |
| Reference definitions     | Basic           | Full spec support                      |

### Reporting Compatibility Issues

If you find a compatibility issue with markdownlint:

1. Check [existing issues](https://github.com/rvben/rumdl/issues?q=is%3Aissue+label%3Acompatibility)
2. Verify with both tools: `markdownlint file.md` and `rumdl check file.md`
3. [File an issue](https://github.com/rvben/rumdl/issues/new) with:
    - Markdown sample
    - Expected behavior (markdownlint output)
    - Actual behavior (rumdl output)
    - Versions of both tools

## See Also

- [Tool Comparison Matrix](comparison.md) - Broad comparison of Markdown linters and formatters
- [Comparison with mdformat](mdformat-comparison.md) - For users coming from the mdformat formatter

## References

- [markdownlint documentation](https://github.com/DavidAnson/markdownlint)
- [CommonMark specification](https://spec.commonmark.org/)
- [rumdl GitHub repository](https://github.com/rvben/rumdl)
- [rumdl rules documentation](rules.md)
- [rumdl flavors documentation](flavors.md)
