OMEP-0001: Project Architecture¶
Context and Problem Statement¶
OxydeMark aims to be an extensible Markdown processing engine. We need to decide on the overall architecture: which language(s) to use, how to structure the codebase, and how the processing pipeline should work.
The key tension is between performance (favoring a systems language) and extensibility / developer experience (favoring a dynamic language with a rich ecosystem).
Decision Drivers¶
- Performance -- Markdown parsing and rendering must be fast, even for large documents.
- Extensibility -- Users must be able to write plugins without learning Rust.
- Pipeline architecture -- Processing should flow through well-defined stages with hooks at each boundary.
- Separation of concerns -- Parsing, transformation, and rendering should be independent modules.
- AST as the central abstraction -- All transformations operate on a shared AST, making the system composable and inspectable.
Considered Options¶
- Option A: Pure Rust -- Entire stack in Rust, plugins via trait objects or WASM.
- Option B: Pure Python -- Use an existing Python Markdown library
(e.g.
markdown-it-py) and extend it. - Option C: Rust core with Python bindings via PyO3 -- High-performance Rust engine exposed to Python through a native extension module.
Decision Outcome¶
Chosen option: "Rust core with Python bindings via PyO3" (Option C), because it delivers Rust-level performance for the hot path (parsing and rendering) while giving plugin authors the full Python ecosystem for preprocessing and postprocessing.
Consequences¶
- Good, because the Rust core can be optimized independently of the Python layer.
- Good, because Python users get a familiar API (
OxydeEngine, plugin protocol) without needing to know Rust. - Good, because the architecture naturally separates concerns: Rust owns parsing/rendering, Python owns orchestration/plugins.
- Bad, because contributors need some familiarity with both Rust and Python.
- Bad, because the build process is more complex (maturin, cross-compilation for wheels).
Confirmation¶
cargo testvalidates the Rust core.- A Python smoke test (
python -c "import oxydemark; ...") confirms the binding works after each wheel build. - CI runs both Rust and Python checks.
Pros and Cons of the Options¶
Option A: Pure Rust¶
- Good, because single-language stack simplifies the build.
- Good, because maximum possible performance.
- Bad, because Rust plugin authoring has a steep learning curve for most users.
- Bad, because the Rust ecosystem for dynamic plugin loading (WASM, dylib) is still maturing.
Option B: Pure Python¶
- Good, because lowest barrier to entry for contributors.
- Good, because rich ecosystem of existing Markdown libraries.
- Bad, because Python is orders of magnitude slower for parsing large documents.
- Bad, because difficult to achieve the performance goals.
Option C: Rust Core with Python Bindings (Chosen)¶
- Good, because parsing/rendering performance is near-native.
- Good, because Python plugin system is easy to use and well-understood.
- Good, because PyO3 + maturin is a mature, well-supported approach.
- Neutral, because two-language builds add CI complexity, but maturin abstracts most of it.
- Bad, because debugging across the FFI boundary can be challenging.
More Information¶
Pipeline architecture:
Markdown Input
-> Preprocessing Plugins (Python, text-level)
-> Rust Parser / rushdown (AST generation)
-> AST exposed to Python (AstNode tree)
-> AST Transformation Plugins (Python, AST-level)
-> Rust Renderer (HTML generation)
-> Postprocessing Plugins (Python, HTML-level)
-> Final Output
The pipeline supports three plugin hook points: text-level preprocessing before parsing, AST-level transformation between parsing and rendering, and HTML-level postprocessing after rendering.
Project structure:
src/ # Rust core
├── lib.rs # Crate root, public re-exports
├── api.rs # Public API, rushdown integration
├── ast.rs # AstNode and arena-to-tree conversion
└── python.rs # PyO3 module (`python` feature)
python/oxydemark/ # Python package
├── __init__.py # Re-exports from _core
└── api.py # OxydeEngine, plugin protocols
Cargo.toml # Rust crate config (cdylib + rlib)
pyproject.toml # maturin build backend
Key dependencies:
| Dependency | Role |
|---|---|
| PyO3 0.28 | Rust ↔ Python FFI bindings |
| maturin | Build backend for mixed Rust/Python packages |
| rushdown 0.18 | Markdown parser and HTML renderer (CommonMark + GFM) |
| rushdown-meta =0.9.9 | YAML frontmatter extension |
| rushdown-emoji =0.9.8 | Emoji shortcode extension |
See also: PyO3 User Guide, maturin docs, OMEP-0006 (parser choice), OMEP-0007 (extended syntax).
Layout since OMEP-0008
The original sketch put the whole Rust core in src/lib.rs. Feature-gating
the bindings (OMEP-0008) split it: lib.rs is now only the crate root and
its public re-exports, and the PyO3 layer lives in src/python.rs behind
the optional python feature. The tree above reflects the current layout.