Skip to content

Latest commit

 

History

History
197 lines (136 loc) · 8.13 KB

AGENTS.md

File metadata and controls

197 lines (136 loc) · 8.13 KB

AGENTS.md

Fixing Issues (Test-Driven Development)

  1. Add failing test in tests/format/fixtures/ (format: description, input, expected output separated by .)
  2. Run test: tox -e test -- -vv
  3. Fix code: implement minimal fix
  4. Verify: tox -e test -- --snapshot-update
  5. Full suite: tox

Fixture Idempotency Rule

Whenever a fixture's input differs from its expected output (A → B), also add a second fixture testing that the output is stable (B → B). This catches cases where the transformation is correct but the result is not idempotent, a class of bug that canary testing only finds if the transformed form happens to appear in a tracked downstream repo.

Issue #N: descriptive title of the forward transformation
.
<input that needs fixing>
.
<corrected output>
.

Issue #N: descriptive title (idempotency)
.
<corrected output>
.
<corrected output>
.

Testing

# Run all tests using tox
tox

# Run tests with coverage (Python 3.14 - current version)
tox -e test

# Run tests with coverage (Python 3.10 - minimum version)
tox -e test-min

# Run specific tests with pytest flags
tox -e test -- --exitfirst --failed-first --new-first -vv --snapshot-update

Linting and Formatting

# Run all pre-commit hooks (using prek)
tox -e prek
# Or run directly with prek
prek run --all

# Run ruff for linting and formatting
tox -e ruff
# With unsafe fixes
tox -e ruff -- --unsafe-fixes

Type Checking

# Run mypy type checking
tox -e type

Canary Testing (Real Downstream Repos)

# Run idempotency checks against all tracked downstream repos
tox -e canary

# Test a subset by name
tox -e canary -- uv ruff

Clones real consumer repos via git sparse checkout and runs a two-pass idempotency check: format once, format again, compare. Not part of the default tox run, so invoke it before releasing to catch crashes or unstable output on real MkDocs content. Configure tracked repos in scripts/canary_repos.json and see scripts/canary.py for excludes.

Pre-commit Hook Testing

# Test the plugin as a pre-commit hook
tox -e hook-min

One-Off Testing

# Create a development environment with local code installed
tox devenv .venv

# Test mdformat on inline content
echo '- \[test\]: value' | .venv/bin/mdformat - --extension mkdocs 2>&1

# Test mdformat on a specific file
.venv/bin/mdformat tests/pre-commit-test.md --extension mkdocs

# Run Python code with local package installed
.venv/bin/python3 << 'PYTHON'
import mdformat
output = mdformat.text("- \[test\]: value", extensions={"mkdocs"})
print(output)
PYTHON

Architecture

Plugin System

The package implements mdformat's plugin interface with up to four exports in __init__.py:

  • update_mdit: Registers markdown-it parser extensions
  • add_cli_argument_group: Optionally adds CLI flags
  • RENDERERS: Maps syntax tree node types to render functions
  • POSTPROCESSORS: Post-processes rendered output of a syntax node. Multiple plugins can register a postprocessor for the same node type, and they run in series

Core Components

mdformat_mkdocs/plugin.py

  • Entry point that configures the mdformat plugin, registers all mdit_plugins, defines custom renders, and handles CLI configuration options

mdformat_mkdocs/_normalize_list.py

  • Complex list indentation normalization logic
  • Enforces 4-space indentation (MkDocs standard) instead of mdformat's default 2-space
  • Handles semantic line breaks with 3-space alignment for numbered lists when --align-semantic-breaks-in-lists is enabled
  • Parses list structure, code blocks, HTML blocks, and nested content
  • Uses functional programming patterns with map_lookback for stateful line processing

mdformat_mkdocs/mdit_plugins/

  • Each file implements a markdown-it plugin for specific MkDocs/Python-Markdown syntax
  • Plugins parse syntax into tokens during the parsing phase
  • Corresponding renderers in plugin.py convert tokens back to formatted markdown

Inline rule protocol (StateInline rules)

Every inline rule (md.inline.ruler.*) must follow this contract:

  • Returning True (match): advance state.pos to just past the match, in both silent and non-silent mode.
  • Returning False (no match): leave state.pos unchanged.

skipToken (used by parseLinkLabel during link-label scanning) calls rules with silent=True and only auto-advances state.pos += 1 when ok=False. If a rule returns True without moving state.pos, the parser stalls in an infinite loop at that position. Verify this contract with a unit test in tests/test_inline_rule_protocol.py.

Link-boundary detection using bracket counting (text_before.count("[") - text_before.count("]")) must search the full state.src[:state.pos] with no length cap. A previous version capped this scan (e.g. to 100 chars), which silently failed for image URLs longer than the cap, causing attr-list tokens to be matched inside link labels.

mdformat_mkdocs/_helpers.py

  • Shared utilities: MKDOCS_INDENT_COUNT (4 spaces), separate_indent, get_conf
  • Configuration reading from mdformat options (CLI, TOML, or API)

mdformat_mkdocs/_synced/

  • Code synced from other mdformat plugins in this family (e.g. admonition factories). Check each subdirectory's README before modifying, since changes flow back upstream

Configuration Options

Configuration can be passed via:

  1. CLI argument, e.g. the scaffolded --argument flag
  2. TOML config file (.mdformat.toml):
    [plugin.mkdocs]
    argument = true
  3. API: mdformat.text(content, extensions={"mkdocs"}, options={...})

Three footguns to avoid:

  • POSTPROCESSORS is keyed on node.type, and the node covering the whole document is "root", not "document". A postprocessor filed under the wrong key never runs, and mdformat reports nothing, so the plugin is silently inert everywhere except tests that build the node themselves. The root node's text also arrives before mdformat appends link reference definitions and the trailing newline
  • Boolean flags in add_cli_argument_group must use action="store_const", const=True (default None), not store_true. A store_true default (False) is indistinguishable from an explicit choice to get_conf(), so it silently overrides argument = true from .mdformat.toml whenever the CLI flag isn't passed. mdformat's CLI builder raises a DeprecationWarning for any plugin flag whose default isn't None or argparse.SUPPRESS
  • Read config lazily. Call get_conf() (or read RenderContext.options) inside the rule/renderer function itself, not inside update_mdit. mdformat runs extensions' update_mdit in an unguaranteed order, so a value captured there can be stale by the time every extension has finished configuring options

Testing Strategy

Fixture Testing

  • Fixture files (before/after markdown pairs) live in tests/format/fixtures/ and tests/render/fixtures/, parsed with markdown_it.utils.read_fixture_file
  • tests/test_mdformat.py verifies idempotent formatting against tests/pre-commit-test.md
  • A downstream project may layer a snapshot library (e.g. syrupy) on top of these fixtures; check pyproject.toml before assuming --snapshot-update applies
  • At least one test per feature must drive mdformat.text or mdformat._cli.run end to end. A test that hands a hand-built node or a mocked RenderContext to a renderer or postprocessor passes whether or not mdformat ever calls it, so a suite made only of those stays green over a plugin that does nothing

Test Organization

  • tests/format/: Tests formatting output (input markdown → formatted markdown)
  • tests/render/: Tests HTML rendering (markdown → HTML via markdown-it)
  • tests/test_hypothesis.py: Property-based idempotency testing over generated markdown documents

Development Notes

  • Do not use uv commands (there is no uv.lock file). Always use tox (installed via mise and available on PATH), which manages environments and dependencies

This file is template-owned and copier update keeps it current. Put project-specific guidance in AGENTS.local.md (loaded below when present) or in a nested AGENTS.md scoped to its directory.

@AGENTS.local.md