The Rust workspace is rooted at the repository root (see Cargo.toml). All cargo commands can run from the repository root or from src/. The Python venv lives at src/.venv/ and is git-ignored.
# one-time setup
cd src
python3 -m venv .venv && source .venv/bin/activate
pip install maturin pytest
# install Rust dev tools (once — downloads cargo-nextest, cargo-llvm-cov, and
# the llvm-tools rustup component; subsequent runs are no-ops)
make install-nextest-deps
cargo install cargo-insta
# Rust gates (from repository root or src/)
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
# or with cargo-nextest (faster, better output)
cargo nextest run --workspace
# via Makefile — also logs to reports/<ISO-timestamp>/
make nextest
make nextest-coverage # requires nightly + llvm-tools (install via make install-nextest-deps)
# pin nightly to skip per-run channel-sync check:
# make nextest-coverage NIGHTLY_TOOLCHAIN=nightly-2026-07-02
# Build extensions into the venv (re-run after Rust changes)
cd src
(cd docutilsrs && maturin develop --release)
(cd sphinxdocrs && maturin develop --release)
(cd pygmentsrs && maturin develop --release)
# Python gate
pytest tests/ -q
Snapshot tests use insta. Review pending snapshots with cargo insta review
(install via cargo install cargo-insta).
Some crates in the workspace support optional features that require system dependencies:
To build jinja2rs with syscall sandboxing (Linux only):
# Install system dependency (choose one)
# Ubuntu/Debian
sudo apt install libseccomp-dev
# Fedora/RHEL
sudo dnf install libseccomp-devel
Then build with the seccomp feature (from repository root or src/jinja2rs):
# From repository root:
cargo test -p jinja2rs --features sandbox,seccomp,resource-limits
# Or from src/jinja2rs:
cd src/jinja2rs
cargo test --features sandbox,seccomp,resource-limits
See docs/LIBSECCOMP_SETUP.md for detailed platform-specific installation instructions and troubleshooting.
docutilsrs and sphinxdocrs support optional features to reduce dependencies for minimal builds:
syntax-highlighting (default)
pygmentsrs for syntax highlighting supportdocutils code-block directive--no-default-featuresExample builds:
# Full build (default) — includes syntax highlighting
cargo build -p docutilsrs
cargo build -p sphinxdocrs
# Minimal build without pygmentsrs (no syntax highlighting)
cargo build -p docutilsrs --no-default-features
cargo build -p sphinxdocrs --no-default-features
# With only specific features
cargo build -p docutilsrs --no-default-features --features syntax-highlighting
When building as Python extensions via maturin, the extension-module feature ensures all dependencies are built correctly for embedding:
# Full extension (includes syntax highlighting)
cd src/docutilsrs && maturin develop --release
cd src/sphinxdocrs && maturin develop --release
# Minimal extension (no syntax highlighting)
cd src/docutilsrs && maturin develop --release -- --no-default-features --features extension-module
cd src/sphinxdocrs && maturin develop --release -- --no-default-features --features extension-module
Feature details:
extension-module — build as Python extension (PyO3 no-embed mode) + propagate to dependenciessyntax-highlighting — include pygmentsrs support (default)When extension-module is enabled, it automatically propagates to pygmentsrs/extension-module so the dependency doesn't try to embed libpython independently.
docutilsrssphinxdocrsdocutilsrs and sphinxdocrs from PythonThese choices unblock everything downstream. Capture each in a short ADR under docs/adr/ when made.
src/docutils/ and src/sphinx/. Parity is defined against that pin; bumps are explicit events.docutilsrs and sphinxdocrs. These do not shadow installed docutils / sphinx. The compiled PyO3 extension modules are exposed directly under those names (no separate _-prefixed inner module).Cargo.toml), with docutilsrs, sphinxdocrs, pygmentsrs, and jinja2rs as members under src/, so shared code (PyO3 conversion layer, plugin-resolver crate, test helpers) lives as path deps and tooling runs once across the workspace.NodeId indices, enum-dispatched node kinds) with FFI converters to/from docutils.nodes at the boundary. Wrapper-over-Python-nodes was rejected: per-access PyO3/GIL cost is paid on every traversal, and traversals dominate transforms and writers. Converter parity is guarded by round-trip snapshot tests (Python → Rust → Python, asserted via pseudo-XML).docutilsrs.equivalents) that maps a Python dotted name to a Rust crate + symbol. This is more robust than a freeform pyproject.toml key and works for already-installed third-party packages.src/docutilsrs/Cargo.toml and src/sphinxdocrs/Cargo.toml with cdylib + rlib crate typessrc/lib.rs for each, exporting a version() PyO3 functioncargo fmt, cargo clippy -- -D warnings, cargo test, maturin develop, pytestinsta wired for snapshot tests; one trivial snapshot landed to prove the loopGoal: one full input → doctree → output path working end-to-end on a tiny subset, not broad coverage.
docutils.nodes for a fixed set of inputs converted via the FFI boundarysrc/docutils/docutils/test/test_parsers/test_rst/ rather than the whole treeparse_rst(source: str) -> Doctree to Python; validate parity by comparing pseudo-XML output against vendored docutils on the same inputsStatus: done. Per-feature status tracked in docs/compat.md.
Parser:
nested_*/multipara_* parity cases)<problematic> + trailing system-messages section; line tracking for top-level paragraphs only — nested paragraphs report no line)grid_table_colspan/grid_table_rowspan/grid_table_rowspan_colspan/grid_table_multipara_cell parity cases)Transforms:
docutilsrs::transforms module mirroring docutils.transforms.* with a composable Transform/Pipeline APIWriters:
docutils.publish_string(..., writer="pseudoxml") (tests/test_parity_pseudoxml.py, 113 cases)docutilsrs.parse_to_html5) — minimal semantic fragment; accepted-deviation, structurally gateddocutilsrs.parse_to_latex) — minimal, accepted-deviation, structurally gateddocutilsrs.parse_to_manpage) — minimal, accepted-deviation, structurally gateddocutilsrs.parse_to_odt):
.odt ZIP container (mimetype + META-INF/manifest.xml + content.xml + styles.xml); accepted-deviation, structurally gated by tests/test_writer_odt.pycompat=True (with optional settings_overrides=...): delegates via PyO3 to vendored docutils.writers.odf_odt; byte-parity-gated against all 13 upstream .odt fixtures (tests/test_writer_odt_parity.py) using the same content.xml-after-ET.tostring normalization upstream's own test_odt.py usesPlugin bridges:
code-block (Pygments) via the Python directive plugin bridge — see src/docutilsrs/python/docutilsrs_pygments.pypygmentsrs crate (Rust→Rust call for supported languages; PyO3 bridge to docutils.utils.code_analyzer.Lexer as fallback): wired in src/docutilsrs/src/code_block.rs. Smoke-gated by tests/test_pygments_native.py; byte-parity gate covers the always-passthrough text language + the unparseable/no-lang shape (tests/test_parity_pseudoxml.py). Full byte-parity for the python lexer is the remaining pygmentsrs Phase 1 followup.Open handoffs (next-up work, in progress via the dedicated pygmentsrs workspace crate — see Phase 2.5 below):
code/code-block/sourcecode (replace the opt-in plugin-bridge stub with byte-parity emission of <literal_block> + token-classed <inline> children). Brief, target output, recommended implementation path, fixtures to add, and gate commands: docs/handoff/pygments.md.Spun up as a separate workspace crate at src/pygmentsrs/ so the
code-block handoff can land as a native Rust→Rust call rather than a
per-block PyO3 hop. Scope: top-N lexers used in Sphinx/RST docs
(text, python, rust, c, cpp, js/ts, bash, json,
yaml, toml, go, rst, html, css, sql, diff, make,
dockerfile). Parity strategy: byte-parity against vendored
pygments HtmlFormatter and against
docutils.utils.code_analyzer.Lexer's token stream (which is what
test_parity_pseudoxml.py compares).
Status:
pygmentsrs.version() /
features() exposed via PyO3, insta snapshot loop proven
(5 passing cargo test cases), TextLexer passthrough +
HtmlFormatter skeleton landed.pygments.token), RegexLexer engine ported
(state stack, bygroups, default, #pop/#push/named-state
transitions, adjacent-same-type merging, error/whitespace
fallback). PythonLexer covers 33 byte-parity fixtures in
tests/test_parity_pseudoxml.py (code_block_python_*):
def/class/decorators, imports (relative + parenthesised + as),
True/False/None, walrus, line-continuations, escape
sequences, raw / triple / prefixed / f-strings (including nested
literals inside {…}), 69 builtins, pseudo-builtins, stdlib
exceptions, magic variables, comments, numbers, operators, and
in/is/and/or/not as Operator.Word inside f-string
expressions. Accepted deviations tracked in
src/pygmentsrs/docs/compat.md (docstring sub-type, match/case
soft keywords, complex-number suffix).src/pygmentsrs/src/bridge.rs)
landed: pygmentsrs.lex(alias, code, backend="auto"|"rust"|"python"),
has_native_lexer(alias), native_aliases(), and a highlight()
shortcut. backend="auto" tries the native Rust lexer first and
transparently falls back to upstream pygments via PyO3 for any
alias without a Rust implementation, so the workspace gets full
upstream coverage today and incremental Rust speedups as lexers
land. Native lexers landed: text, python, json
(src/pygmentsrs/src/lexers/json.rs, hand-written state machine,
10 byte-parity fixtures in tests/test_pygments_json_lexer.py),
diff (src/pygmentsrs/src/lexers/diff.rs, RegexLexer-engine
port, 6 byte-parity fixtures in tests/test_pygments_diff_lexer.py).
HtmlFormatter now ships the full STANDARD_TYPES short-name
table (src/pygmentsrs/src/token.rs), so its default-options
output is byte-compatible with pygments.formatters.html.HtmlFormatter
for every native lexer above.pygmentsrs = { path = "../pygmentsrs" }): the parser's
code/code-block/sourcecode arm calls pygmentsrs::tokenize
first and only falls back to the existing
docutils.utils.code_analyzer.Lexer Python bridge when the
native path declines.
src/docutilsrs/src/code_block.rs hosts the dispatcher;
Block::LiteralBlock carries an optional
tokens: Vec<(Option<String>, String)> field that the pseudo-XML
emit path renders as <inline classes="…"> token spans. Gates:
tests/test_pygments_native.py (smoke) and
tests/test_parity_pseudoxml.py now contains 33+ byte-parity
code-block cases (text alias, sourcecode alias, and the full
code_block_python_* family).Remaining followups: continue widening the lexer registry (next
priorities are rust, c/cpp, yaml, rst, toml, make).
bash/sh/ksh/zsh/shell lexers are unblocked: the regex
engine was upgraded from regex to fancy-regex (ADR 0012) to support
backreferences required by heredoc patterns (upstream's heredoc rule uses
\2); see src/pygmentsrs/src/lexer/engine.rs.
This is the integration safety net, not a stretch goal.
docutilsrs::transforms module mirroring docutils.transforms.*, each pass independently testable — done (src/docutilsrs/src/transforms.rs, Transform trait + Pipeline)docutilsrs package routes calls to Rust when implemented, falls back to vendored Python otherwise, on a per-component basis (parser, transform, writer) — done (src/docutilsrs/python/docutilsrs_hybrid.py: publish_string(prefer=...) + dispatch_plan(writer, prefer, has_python_transforms) reporting {parser, transforms, writer})Transform or Writer against a Rust-owned doctree (via converter) — done (docutilsrs.register_transform(name, callable): callable receives a read-only PyDoctree view and returns [(node_id, new_text), ...] edits applied to the arena after the default pipeline; see src/docutilsrs/src/plugins.rs::apply_transforms)tests/test_phase3_hybrid_e2e.py, parametrized over 4 docs, uppercases all text via the bridge and compares to pure-Python with the equivalent docutils.transforms.Transform)src/sphinx/tests/ and tag each test by subsystem (config, environment, builders, extensions, domains) — done (docs/sphinxdocrs-port-plan.md)errors, events, project (incl. discover) landed with mirrored parity tests; P2: extension.Extension + verify_needs_extensions landed (tests/test_sphinxdocrs_extension.py); P2: util.matching (compile_matchers/Matcher/get_matching_files) landed (tests/test_sphinxdocrs_util_matching.py); P2: util.console (port of sphinx.util.console + sphinx._cli.util.colour + sphinx._cli.util.errors: colourise, disable_colour/enable_colour, strip_escape_sequences, terminal_safe, 22 named colour escape codes) landed (tests/test_sphinxdocrs_util_console.py, 40 byte-parity tests); Project.discover gated by tests/test_sphinxdocrs_project_discover.py)src/sphinxdocrs/src/events.rs: priority ordering, allowed_exceptions, app.pdb short-circuit, ExtensionError wrapping with __cause__)sphinxdocrs.{EventManager,Project,SphinxError,…} + sphinxdocrs/python/sphinxdocrs_hybrid.py with event_manager / project / dispatch_plan / features / supports)src/docutilsrs/python/docutilsrs_plugins.py: entry-point group docutilsrs.equivalents + in-memory register() for tests; resolve(target, prefer=...) -> Resolution(impl, source, reason))Equivalent.upstream_requires PEP 440 spec; mismatch emits UserWarning and falls through to _load_python)dispatch() returning Rust classes such as sphinxdocrs.EventManager; Rust→Python via the existing docutilsrs.register_transform bridge from Phase 3)tests/test_phase5_resolver.py, 9 tests including an e2e route of sphinx.events:EventManager through the resolver into sphinxdocrs.EventManager)cargo fmt --check, cargo clippy -- -D warnings, cargo test, pytest all green on every PRdocs/changes/src/docutils/ or src/sphinx/ outside of an explicit, isolated PRStatus: done. All deliverables landed; exit criteria met.
goal
deliverables
docs/adr/0001-upstream-pin.md, 0002-names.md, 0003-bindings-and-layout.md, 0004-doctree-representation.md, 0005-plugin-discovery.md)Cargo.toml + src/lib.rs for docutilsrs and sphinxdocrs, each exporting a PyO3 version() — done (both crates plus the in-workspace pygmentsrs member also expose version() / features())maturin develop produces importable docutilsrs and sphinxdocrs modules — done (dev-loop block at the top of this README)insta snapshot test per crate proving the test harness works — done (docutilsrs/tests/snapshot.rs, sphinxdocrs/tests/snapshot.rs, pygmentsrs/tests/snapshot.rs).github/workflows/ci.yml)docs/compat.md skeleton with the matrix columns defined — done (plus per-crate src/pygmentsrs/docs/compat.md for the Pygments port)exit criteria
version() — met (tests/test_smoke.py for docutilsrs; tests/test_sphinxdocrs_* cover the sphinxdocrs import surface)explicit non-goals for M1 (kept out of scope for the bootstrap; subsequently delivered by later phases)
The following design choices were locked during Phases 2–5 and need ADRs
written under docs/adr/:
| # | Decision | Where decided | Phase |
|---|---|---|---|
| 0006 | Transform / Pipeline API — composable Transform trait + Pipeline struct in a standalone docutilsrs::transforms module, mirroring docutils.transforms.*; inline-in-parser transforms rejected | src/docutilsrs/src/transforms.rs | 2 / 3 |
| 0007 | Hybrid-mode dispatch — Python-side docutilsrs_hybrid.py routes parse / transform / write decisions via publish_string(prefer=...) + dispatch_plan(); per-component granularity, not all-or-nothing | src/docutilsrs/python/docutilsrs_hybrid.py | 3 |
| 0008 | pygmentsrs scope and backend="auto" strategy — spun as a separate workspace crate; scope is the top-N lexers used in Sphinx/RST docs; backend="auto" tries native Rust first and falls back to Python for any alias without a native implementation; byte-parity target is vendored HtmlFormatter | src/pygmentsrs/ | 2.5 |
| 0009 | Code-block dispatcher — docutilsrs::code_block calls pygmentsrs::tokenize first; falls back to the docutils.utils.code_analyzer.Lexer PyO3 bridge only when the native path declines | src/docutilsrs/src/code_block.rs | 2.5 / 3 |
| 0010 | ODT writer dual-path — default native Rust path produces a valid .odt ZIP container (accepted-deviation); compat=True delegates via PyO3 to docutils.writers.odf_odt and is byte-parity-gated against all 13 upstream fixtures | src/docutilsrs/src/writers/odt.rs | 2 |
| 0011 | Version guard for Rust equivalents — Equivalent.upstream_requires field holds a PEP 440 specifier; a version mismatch emits UserWarning and falls through to _load_python rather than raising | src/docutilsrs/python/docutilsrs_plugins.py | 5 |
| 0012 | Regex engine upgrade to fancy-regex — upgraded from regex to fancy-regex to support backreferences required by heredoc patterns in bash, sh, ksh, zsh, and shell lexers; landed | src/pygmentsrs/src/lexer/engine.rs, src/pygmentsrs/docs/compat.md | 2.5 |
Rust
97.4%
Python
2.1%
The Rust workspace is rooted at the repository root (see Cargo.toml). All cargo commands can run from the repository root or from src/. The Python venv lives at src/.venv/ and is git-ignored.
# one-time setup
cd src
python3 -m venv .venv && source .venv/bin/activate
pip install maturin pytest
# install Rust dev tools (once — downloads cargo-nextest, cargo-llvm-cov, and
# the llvm-tools rustup component; subsequent runs are no-ops)
make install-nextest-deps
cargo install cargo-insta
# Rust gates (from repository root or src/)
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
# or with cargo-nextest (faster, better output)
cargo nextest run --workspace
# via Makefile — also logs to reports/<ISO-timestamp>/
make nextest
make nextest-coverage # requires nightly + llvm-tools (install via make install-nextest-deps)
# pin nightly to skip per-run channel-sync check:
# make nextest-coverage NIGHTLY_TOOLCHAIN=nightly-2026-07-02
# Build extensions into the venv (re-run after Rust changes)
cd src
(cd docutilsrs && maturin develop --release)
(cd sphinxdocrs && maturin develop --release)
(cd pygmentsrs && maturin develop --release)
# Python gate
pytest tests/ -q
Snapshot tests use insta. Review pending snapshots with cargo insta review
(install via cargo install cargo-insta).
Some crates in the workspace support optional features that require system dependencies:
To build jinja2rs with syscall sandboxing (Linux only):
# Install system dependency (choose one)
# Ubuntu/Debian
sudo apt install libseccomp-dev
# Fedora/RHEL
sudo dnf install libseccomp-devel
Then build with the seccomp feature (from repository root or src/jinja2rs):
# From repository root:
cargo test -p jinja2rs --features sandbox,seccomp,resource-limits
# Or from src/jinja2rs:
cd src/jinja2rs
cargo test --features sandbox,seccomp,resource-limits
See docs/LIBSECCOMP_SETUP.md for detailed platform-specific installation instructions and troubleshooting.
docutilsrs and sphinxdocrs support optional features to reduce dependencies for minimal builds:
syntax-highlighting (default)
pygmentsrs for syntax highlighting supportdocutils code-block directive--no-default-featuresExample builds:
# Full build (default) — includes syntax highlighting
cargo build -p docutilsrs
cargo build -p sphinxdocrs
# Minimal build without pygmentsrs (no syntax highlighting)
cargo build -p docutilsrs --no-default-features
cargo build -p sphinxdocrs --no-default-features
# With only specific features
cargo build -p docutilsrs --no-default-features --features syntax-highlighting
When building as Python extensions via maturin, the extension-module feature ensures all dependencies are built correctly for embedding:
# Full extension (includes syntax highlighting)
cd src/docutilsrs && maturin develop --release
cd src/sphinxdocrs && maturin develop --release
# Minimal extension (no syntax highlighting)
cd src/docutilsrs && maturin develop --release -- --no-default-features --features extension-module
cd src/sphinxdocrs && maturin develop --release -- --no-default-features --features extension-module
Feature details:
extension-module — build as Python extension (PyO3 no-embed mode) + propagate to dependenciessyntax-highlighting — include pygmentsrs support (default)When extension-module is enabled, it automatically propagates to pygmentsrs/extension-module so the dependency doesn't try to embed libpython independently.
docutilsrssphinxdocrsdocutilsrs and sphinxdocrs from PythonThese choices unblock everything downstream. Capture each in a short ADR under docs/adr/ when made.
src/docutils/ and src/sphinx/. Parity is defined against that pin; bumps are explicit events.docutilsrs and sphinxdocrs. These do not shadow installed docutils / sphinx. The compiled PyO3 extension modules are exposed directly under those names (no separate _-prefixed inner module).Cargo.toml), with docutilsrs, sphinxdocrs, pygmentsrs, and jinja2rs as members under src/, so shared code (PyO3 conversion layer, plugin-resolver crate, test helpers) lives as path deps and tooling runs once across the workspace.NodeId indices, enum-dispatched node kinds) with FFI converters to/from docutils.nodes at the boundary. Wrapper-over-Python-nodes was rejected: per-access PyO3/GIL cost is paid on every traversal, and traversals dominate transforms and writers. Converter parity is guarded by round-trip snapshot tests (Python → Rust → Python, asserted via pseudo-XML).docutilsrs.equivalents) that maps a Python dotted name to a Rust crate + symbol. This is more robust than a freeform pyproject.toml key and works for already-installed third-party packages.src/docutilsrs/Cargo.toml and src/sphinxdocrs/Cargo.toml with cdylib + rlib crate typessrc/lib.rs for each, exporting a version() PyO3 functioncargo fmt, cargo clippy -- -D warnings, cargo test, maturin develop, pytestinsta wired for snapshot tests; one trivial snapshot landed to prove the loopGoal: one full input → doctree → output path working end-to-end on a tiny subset, not broad coverage.
docutils.nodes for a fixed set of inputs converted via the FFI boundarysrc/docutils/docutils/test/test_parsers/test_rst/ rather than the whole treeparse_rst(source: str) -> Doctree to Python; validate parity by comparing pseudo-XML output against vendored docutils on the same inputsStatus: done. Per-feature status tracked in docs/compat.md.
Parser:
nested_*/multipara_* parity cases)<problematic> + trailing system-messages section; line tracking for top-level paragraphs only — nested paragraphs report no line)grid_table_colspan/grid_table_rowspan/grid_table_rowspan_colspan/grid_table_multipara_cell parity cases)Transforms:
docutilsrs::transforms module mirroring docutils.transforms.* with a composable Transform/Pipeline APIWriters:
docutils.publish_string(..., writer="pseudoxml") (tests/test_parity_pseudoxml.py, 113 cases)docutilsrs.parse_to_html5) — minimal semantic fragment; accepted-deviation, structurally gateddocutilsrs.parse_to_latex) — minimal, accepted-deviation, structurally gateddocutilsrs.parse_to_manpage) — minimal, accepted-deviation, structurally gateddocutilsrs.parse_to_odt):
.odt ZIP container (mimetype + META-INF/manifest.xml + content.xml + styles.xml); accepted-deviation, structurally gated by tests/test_writer_odt.pycompat=True (with optional settings_overrides=...): delegates via PyO3 to vendored docutils.writers.odf_odt; byte-parity-gated against all 13 upstream .odt fixtures (tests/test_writer_odt_parity.py) using the same content.xml-after-ET.tostring normalization upstream's own test_odt.py usesPlugin bridges:
code-block (Pygments) via the Python directive plugin bridge — see src/docutilsrs/python/docutilsrs_pygments.pypygmentsrs crate (Rust→Rust call for supported languages; PyO3 bridge to docutils.utils.code_analyzer.Lexer as fallback): wired in src/docutilsrs/src/code_block.rs. Smoke-gated by tests/test_pygments_native.py; byte-parity gate covers the always-passthrough text language + the unparseable/no-lang shape (tests/test_parity_pseudoxml.py). Full byte-parity for the python lexer is the remaining pygmentsrs Phase 1 followup.Open handoffs (next-up work, in progress via the dedicated pygmentsrs workspace crate — see Phase 2.5 below):
code/code-block/sourcecode (replace the opt-in plugin-bridge stub with byte-parity emission of <literal_block> + token-classed <inline> children). Brief, target output, recommended implementation path, fixtures to add, and gate commands: docs/handoff/pygments.md.Spun up as a separate workspace crate at src/pygmentsrs/ so the
code-block handoff can land as a native Rust→Rust call rather than a
per-block PyO3 hop. Scope: top-N lexers used in Sphinx/RST docs
(text, python, rust, c, cpp, js/ts, bash, json,
yaml, toml, go, rst, html, css, sql, diff, make,
dockerfile). Parity strategy: byte-parity against vendored
pygments HtmlFormatter and against
docutils.utils.code_analyzer.Lexer's token stream (which is what
test_parity_pseudoxml.py compares).
Status:
pygmentsrs.version() /
features() exposed via PyO3, insta snapshot loop proven
(5 passing cargo test cases), TextLexer passthrough +
HtmlFormatter skeleton landed.pygments.token), RegexLexer engine ported
(state stack, bygroups, default, #pop/#push/named-state
transitions, adjacent-same-type merging, error/whitespace
fallback). PythonLexer covers 33 byte-parity fixtures in
tests/test_parity_pseudoxml.py (code_block_python_*):
def/class/decorators, imports (relative + parenthesised + as),
True/False/None, walrus, line-continuations, escape
sequences, raw / triple / prefixed / f-strings (including nested
literals inside {…}), 69 builtins, pseudo-builtins, stdlib
exceptions, magic variables, comments, numbers, operators, and
in/is/and/or/not as Operator.Word inside f-string
expressions. Accepted deviations tracked in
src/pygmentsrs/docs/compat.md (docstring sub-type, match/case
soft keywords, complex-number suffix).src/pygmentsrs/src/bridge.rs)
landed: pygmentsrs.lex(alias, code, backend="auto"|"rust"|"python"),
has_native_lexer(alias), native_aliases(), and a highlight()
shortcut. backend="auto" tries the native Rust lexer first and
transparently falls back to upstream pygments via PyO3 for any
alias without a Rust implementation, so the workspace gets full
upstream coverage today and incremental Rust speedups as lexers
land. Native lexers landed: text, python, json
(src/pygmentsrs/src/lexers/json.rs, hand-written state machine,
10 byte-parity fixtures in tests/test_pygments_json_lexer.py),
diff (src/pygmentsrs/src/lexers/diff.rs, RegexLexer-engine
port, 6 byte-parity fixtures in tests/test_pygments_diff_lexer.py).
HtmlFormatter now ships the full STANDARD_TYPES short-name
table (src/pygmentsrs/src/token.rs), so its default-options
output is byte-compatible with pygments.formatters.html.HtmlFormatter
for every native lexer above.pygmentsrs = { path = "../pygmentsrs" }): the parser's
code/code-block/sourcecode arm calls pygmentsrs::tokenize
first and only falls back to the existing
docutils.utils.code_analyzer.Lexer Python bridge when the
native path declines.
src/docutilsrs/src/code_block.rs hosts the dispatcher;
Block::LiteralBlock carries an optional
tokens: Vec<(Option<String>, String)> field that the pseudo-XML
emit path renders as <inline classes="…"> token spans. Gates:
tests/test_pygments_native.py (smoke) and
tests/test_parity_pseudoxml.py now contains 33+ byte-parity
code-block cases (text alias, sourcecode alias, and the full
code_block_python_* family).Remaining followups: continue widening the lexer registry (next
priorities are rust, c/cpp, yaml, rst, toml, make).
bash/sh/ksh/zsh/shell lexers are unblocked: the regex
engine was upgraded from regex to fancy-regex (ADR 0012) to support
backreferences required by heredoc patterns (upstream's heredoc rule uses
\2); see src/pygmentsrs/src/lexer/engine.rs.
This is the integration safety net, not a stretch goal.
docutilsrs::transforms module mirroring docutils.transforms.*, each pass independently testable — done (src/docutilsrs/src/transforms.rs, Transform trait + Pipeline)docutilsrs package routes calls to Rust when implemented, falls back to vendored Python otherwise, on a per-component basis (parser, transform, writer) — done (src/docutilsrs/python/docutilsrs_hybrid.py: publish_string(prefer=...) + dispatch_plan(writer, prefer, has_python_transforms) reporting {parser, transforms, writer})Transform or Writer against a Rust-owned doctree (via converter) — done (docutilsrs.register_transform(name, callable): callable receives a read-only PyDoctree view and returns [(node_id, new_text), ...] edits applied to the arena after the default pipeline; see src/docutilsrs/src/plugins.rs::apply_transforms)tests/test_phase3_hybrid_e2e.py, parametrized over 4 docs, uppercases all text via the bridge and compares to pure-Python with the equivalent docutils.transforms.Transform)src/sphinx/tests/ and tag each test by subsystem (config, environment, builders, extensions, domains) — done (docs/sphinxdocrs-port-plan.md)errors, events, project (incl. discover) landed with mirrored parity tests; P2: extension.Extension + verify_needs_extensions landed (tests/test_sphinxdocrs_extension.py); P2: util.matching (compile_matchers/Matcher/get_matching_files) landed (tests/test_sphinxdocrs_util_matching.py); P2: util.console (port of sphinx.util.console + sphinx._cli.util.colour + sphinx._cli.util.errors: colourise, disable_colour/enable_colour, strip_escape_sequences, terminal_safe, 22 named colour escape codes) landed (tests/test_sphinxdocrs_util_console.py, 40 byte-parity tests); Project.discover gated by tests/test_sphinxdocrs_project_discover.py)src/sphinxdocrs/src/events.rs: priority ordering, allowed_exceptions, app.pdb short-circuit, ExtensionError wrapping with __cause__)sphinxdocrs.{EventManager,Project,SphinxError,…} + sphinxdocrs/python/sphinxdocrs_hybrid.py with event_manager / project / dispatch_plan / features / supports)src/docutilsrs/python/docutilsrs_plugins.py: entry-point group docutilsrs.equivalents + in-memory register() for tests; resolve(target, prefer=...) -> Resolution(impl, source, reason))Equivalent.upstream_requires PEP 440 spec; mismatch emits UserWarning and falls through to _load_python)dispatch() returning Rust classes such as sphinxdocrs.EventManager; Rust→Python via the existing docutilsrs.register_transform bridge from Phase 3)tests/test_phase5_resolver.py, 9 tests including an e2e route of sphinx.events:EventManager through the resolver into sphinxdocrs.EventManager)cargo fmt --check, cargo clippy -- -D warnings, cargo test, pytest all green on every PRdocs/changes/src/docutils/ or src/sphinx/ outside of an explicit, isolated PRStatus: done. All deliverables landed; exit criteria met.
goal
deliverables
docs/adr/0001-upstream-pin.md, 0002-names.md, 0003-bindings-and-layout.md, 0004-doctree-representation.md, 0005-plugin-discovery.md)Cargo.toml + src/lib.rs for docutilsrs and sphinxdocrs, each exporting a PyO3 version() — done (both crates plus the in-workspace pygmentsrs member also expose version() / features())maturin develop produces importable docutilsrs and sphinxdocrs modules — done (dev-loop block at the top of this README)insta snapshot test per crate proving the test harness works — done (docutilsrs/tests/snapshot.rs, sphinxdocrs/tests/snapshot.rs, pygmentsrs/tests/snapshot.rs).github/workflows/ci.yml)docs/compat.md skeleton with the matrix columns defined — done (plus per-crate src/pygmentsrs/docs/compat.md for the Pygments port)exit criteria
version() — met (tests/test_smoke.py for docutilsrs; tests/test_sphinxdocrs_* cover the sphinxdocrs import surface)explicit non-goals for M1 (kept out of scope for the bootstrap; subsequently delivered by later phases)
The following design choices were locked during Phases 2–5 and need ADRs
written under docs/adr/:
| # | Decision | Where decided | Phase |
|---|---|---|---|
| 0006 | Transform / Pipeline API — composable Transform trait + Pipeline struct in a standalone docutilsrs::transforms module, mirroring docutils.transforms.*; inline-in-parser transforms rejected | src/docutilsrs/src/transforms.rs | 2 / 3 |
| 0007 | Hybrid-mode dispatch — Python-side docutilsrs_hybrid.py routes parse / transform / write decisions via publish_string(prefer=...) + dispatch_plan(); per-component granularity, not all-or-nothing | src/docutilsrs/python/docutilsrs_hybrid.py | 3 |
| 0008 | pygmentsrs scope and backend="auto" strategy — spun as a separate workspace crate; scope is the top-N lexers used in Sphinx/RST docs; backend="auto" tries native Rust first and falls back to Python for any alias without a native implementation; byte-parity target is vendored HtmlFormatter | src/pygmentsrs/ | 2.5 |
| 0009 | Code-block dispatcher — docutilsrs::code_block calls pygmentsrs::tokenize first; falls back to the docutils.utils.code_analyzer.Lexer PyO3 bridge only when the native path declines | src/docutilsrs/src/code_block.rs | 2.5 / 3 |
| 0010 | ODT writer dual-path — default native Rust path produces a valid .odt ZIP container (accepted-deviation); compat=True delegates via PyO3 to docutils.writers.odf_odt and is byte-parity-gated against all 13 upstream fixtures | src/docutilsrs/src/writers/odt.rs | 2 |
| 0011 | Version guard for Rust equivalents — Equivalent.upstream_requires field holds a PEP 440 specifier; a version mismatch emits UserWarning and falls through to _load_python rather than raising | src/docutilsrs/python/docutilsrs_plugins.py | 5 |
| 0012 | Regex engine upgrade to fancy-regex — upgraded from regex to fancy-regex to support backreferences required by heredoc patterns in bash, sh, ksh, zsh, and shell lexers; landed | src/pygmentsrs/src/lexer/engine.rs, src/pygmentsrs/docs/compat.md | 2.5 |
Rust
97.4%
Python
2.1%