Skip to content

Latest commit

 

History

History
183 lines (136 loc) · 6.98 KB

File metadata and controls

183 lines (136 loc) · 6.98 KB

mdlint Development Guidelines

Project Overview

mdlint is a Python Markdown linter. It checks Markdown files for style violations and outputs results in human-readable or JSON format.

Project Structure

src/mdlint/
├── __init__.py          # Version info
├── cli.py               # CLI interface (rich-click)
├── config.py            # Configuration management (TOML)
├── document.py          # Markdown parsing (markdown-it-py)
├── linter.py            # Core linting orchestration
├── violation.py         # Violation data model
├── rules/               # Rule implementations
│   ├── __init__.py      # Rule base class and registry
│   ├── md001.py
│   ├── md002.py
│   └── ...
└── output/              # Output formatters
    ├── __init__.py
    ├── terminal.py      # Rich terminal output
    └── json.py          # JSON output

tests/
├── unit/                # Unit tests
├── integration/         # Integration tests
├── fixtures/            # Test markdown files
└── conftest.py          # Pytest configuration

docs/
├── index.md             # Documentation home
├── hooks.py             # MkDocs hooks (generates rule docs and index table at build time)
├── assets/              # Static assets
├── stylesheets/         # Custom CSS
└── rules/               # Stub files for rule docs (content generated by hooks.py)

Development Commands

uvx tox -e test          # Run tests
uvx tox -e lint          # Run linter (use lint-fix to auto-fix)
uvx tox -e check-format  # Check formatting (use format to auto-fix)

IMPORTANT: run tests, linting, and formatting after every code change.

When running mdlint directly, use:

uv run mdlint -h               # Print help
uv run mdlint check README.md  # Check local file

To build and test the documentation site:

uv run mkdocs build --strict  # Build the site
rm -rf site/                  # Clean up after testing

IMPORTANT: Always remove the site/ directory after testing documentation builds.

Architecture

Rule System

Rules are defined in src/mdlint/rules/ and registered in the RULE_REGISTRY dict in rules/__init__.py:

class MD001(Rule):
    id = "MD001"
    name = "heading-increment"
    description = "Headings should only increment by one level at a time"
    config_class = RuleConfig  # or a custom dataclass extending RuleConfig

    def check(self, document: Document, config: RuleConfig) -> list[Violation]:
        # Return list of violations
        return

Document Parsing

Uses markdown-it-py for token-based parsing. The Document class provides:

  • path: File path or <stdin>
  • content: Raw markdown content
  • tokens: Parsed markdown-it tokens
  • lines: Content split by lines (1-indexed via get_line())
  • front_matter: Extracted YAML/TOML front matter
  • code_block_lines: (cached) set[int] of 1-indexed line numbers inside fenced/indented code blocks
  • html_block_lines: (cached) set[int] of 1-indexed line numbers inside HTML blocks
  • code_span_positions: (cached) dict[int, set[int]] mapping line numbers to 1-indexed column positions inside inline code spans
  • reference_definitions: (cached) dict[str, str] mapping lowercase reference IDs to destinations
  • REFERENCE_DEF_PATTERN: compiled regex for matching reference definition lines

Configuration

Precedence (highest to lowest):

  1. --config CLI flag (file path or inline TOML)
  2. .mdlint.toml in project
  3. pyproject.toml under [tool.mdlint]
  4. Rule defaults

File Discovery

Uses the ignore library for gitignore-aware file traversal. The discover_files() function in linter.py handles both files and directories, respecting .gitignore patterns by default.

Exit Codes

  • 0: No violations, no errors
  • 1: Violations found (with or without errors)
  • 2: Errors only (file read failures, invalid config)

Code Style

  • Python 3.10+ with type hints
  • Ruff for linting and formatting (see pyproject.toml for configuration)
  • Dataclasses for configuration and data models
  • Abstract base classes for extensibility (Rule)
  • Always use file-level (top-of-file) imports; only use function-level imports if absolutely necessary (e.g., to avoid circular imports)

Key Patterns

  • Registry Pattern: Rules are registered in RULE_REGISTRY in rules/__init__.py
  • Strategy Pattern: Rules implement common check() interface
  • Immutable Data Models: Violation is a frozen dataclass

Base Class Helpers

The Rule base class in rules/base.py provides:

  • _overlaps_ranges(start, end, ranges) — checks if a 0-indexed position range overlaps with any existing ranges

Document-level cached properties (code_block_lines, code_span_positions, etc.) and REFERENCE_DEF_PATTERN (compiled regex for matching reference definition lines) are accessed directly on the Document class — see Document Parsing above.

Adding a New Rule

  1. Create src/mdlint/rules/mdXXX.py
  2. Define config class if needed (extend RuleConfig)
  3. Implement rule class extending Rule
  4. Import and add the rule to RULE_REGISTRY in rules/__init__.py
  5. Add tests in tests/unit/rules/test_mdXXX.py
  6. Add fixtures: at least one valid.md and one invalid.md file that shows good and bad examples
    • Keep valid.md and invalid.md minimal — they should cover the common/basic cases only
    • For edge cases, regressions, or special scenarios, create separate fixture files (e.g., multiline_and_nested.md)
    • Always use fixtures loaded via load_fixture() in tests; avoid inline content strings for anything beyond trivial one-line cases
  7. Add a stub file docs/rules/mdXXX.md and a nav entry in mkdocs.yml for the new rule
  8. Run all checks: uvx tox -e py313,lint,check-format

Rule Documentation

Rule documentation is auto-generated at build time by the MkDocs hook in docs/hooks.py. Each rule page in docs/rules/ is a stub file whose content is replaced entirely by the hook during mkdocs build or mkdocs serve.

The hook extracts these attributes from rule classes in RULE_REGISTRY:

  • id, name, summary - Basic rule info
  • description, rationale - Detailed explanations
  • example_valid, example_invalid - Code examples (conditionally rendered)
  • config_fields - Configuration options from the rule's config dataclass

The hook also auto-generates the rules table on docs/rules/index.md.

No separate generation script needs to be run. Changes to rule class attributes are picked up automatically on the next build. When using mkdocs serve, changes to src/mdlint/rules/ trigger a live reload (configured via watch in mkdocs.yml).

Notes

  • Stub files in docs/rules/ must exist for MkDocs to recognize the pages; their content is ignored
  • Examples use 4 backticks for outer fences to allow triple backticks in content
  • Sections are conditionally rendered (config only if has_config, examples only if defined)