Contributions are welcome from anyone — bug reports, documentation fixes, new templates, and features are all useful. This guide covers what you need to know to get a change merged.
By participating, you agree to follow the Code of Conduct.
| I want to... | Go here |
|---|---|
| Report a bug | Open an issue |
| Ask a question | Discussions → Q&A |
| Suggest a feature | Discussions → Ideas |
| Report a security issue | Security policy — please do not open a public issue |
| Fix something yourself | Read on |
git clone https://github.com/leonardosalasd/doc-engine-cli.git
cd doc-engine-cli
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"Check that the tool runs before changing anything:
python -m doc_engine --version
python -m pytest tests/ -vUse python -m doc_engine rather than the bare doc-engine command while
working on the project. If you already have a release installed, your shell can
keep resolving doc-engine to that copy even inside the virtualenv, and you
would end up testing the published version instead of your changes without
anything saying so. Running it through python -m always uses the environment
you are standing in. (hash -r usually settles the shell, and
which doc-engine tells you which one you are about to run.)
Run the suite before opening a pull request:
python -m pytest tests/ -vTest on more than one Python version. This matters more than it sounds:
v1.1.0 shipped completely broken on Python 3.10 through 3.13 because it was
only ever run on 3.14, where deferred annotation evaluation hid an import-time
crash. CI now covers 3.10–3.14 on Linux, macOS, and Windows, but catching it
locally is faster. If you have uv:
uv run --python 3.10 --no-project --with mistune --with click --with rich \
--with typst --with mermaidx --with pillow --with tomli --with pytest \
python -m pytest tests/ -qSwap 3.10 for any version in the supported range.
New behavior needs a test. Bug fixes need a test that fails before the fix and passes after it — that is what stops the same bug from coming back.
Formatting and linting are handled by ruff, and CI checks both. Before opening a pull request:
ruff format doc_engine/ tests/
ruff check doc_engine/ tests/Ruff settles the mechanical questions. The rest is convention — match the surrounding code:
- Type hints on function signatures.
- Module-level constants in
SCREAMING_SNAKE_CASE, prefixed with_when private. - Helper functions prefixed with
_. - Tests grouped in classes named
TestSomething, one behavior per method. - Docstrings where the why is not obvious from the code. Skip them where it is.
- No decorative comment banners (
# ====,# ----). If a comment is needed, write a sentence explaining the reasoning, not a divider.
Keep changes focused. A pull request that fixes one thing is easier to review, and far easier to revert if it turns out to be wrong.
The history uses Conventional Commits:
feat: add support for A4 page size
fix: prevent crash on legacy Windows consoles
docs: document the --watch flag
ci: test on Python 3.14
chore: bump version to 2.0.0
Write the subject in the imperative mood, under ~72 characters. If the change needs explanation, put it in the body: what was broken, why the fix works, and anything a future reader would need in order to not undo it by accident.
- Branch from
mainusing a descriptive prefix:feat/,fix/, ordocs/. - Make your change, with tests.
- Run the suite and confirm it passes.
- Open the pull request against
main.
Every pull request is reviewed by the repository owner (CODEOWNERS), and CI must be green before merging. CI runs the test suite across every supported Python version and operating system, a smoke job that installs the built package and drives the CLI from a clean directory, a lint and format check, and a build of the Docker image.
In the description, explain what changed and why. If it fixes an open issue,
reference it with Fixes #123.
Templates live in doc_engine/templates/ as .typ
files. Each one exposes a single setup_doc entry point, and the compiler
passes it the document metadata:
#let setup_doc(
title: "",
subtitle: "",
author: "Anonymous",
date: datetime.today().display(),
bibliography_file: none,
accent: none,
branding: true,
version: "",
paper: "a4",
..options,
body,
) = { ... }The quickest start is to copy an existing template and edit it. A new template should:
- Honor
accentwhen it is notnone, and fall back to its own default color. - Honor
branding: falseby hiding thedoc-engineattribution. - Pass
paperthrough toset page, rather than hardcoding a size. - Render
subtitleonly when it is not empty. - Render the bibliography when
bibliography_fileis notnone. - Keep the
..optionssink, so a later release can add an option without breaking templates that have not been updated. - End every font stack in one Typst ships —
New Computer Modern,Libertinus Serif, orDejaVu Sans Mono— so the document looks the same inside a bare container as it does on a laptop full of fonts.
Templates are discovered automatically, so dropping the file in is enough for
--template <name> to find it. Add it to the templates table in the README and
to the test in tests/test_cli.py that asserts the shipped set.
| Module | Responsibility |
|---|---|
cli.py |
Argument parsing, Git detection, watch loop, output naming |
help.py |
The Rich help screens |
settings.py |
Deciding between a flag, front matter, and the project file |
config.py |
Reading .doc-engine.toml |
frontmatter.py |
Leading --- metadata block |
manifest.py |
Building one document out of several files |
linter.py |
Source checks reported with line and column |
converter.py |
Markdown AST → Typst markup |
latex.py |
LaTeX math → Typst math |
diagrams.py |
Mermaid and SVG blocks |
images.py |
Cutting pictures taller than a page |
remote.py |
Downloading linked images |
compiler.py |
Template injection and PDF compilation |
Changes usually belong in exactly one of these. If a change needs to touch all of them, it is worth opening a discussion first to agree on the approach.
A good report includes:
- What you ran and what happened, with the full error output.
- Your OS, Python version, and
doc-engine --version. - A minimal Markdown file that reproduces it, if the problem is in a document.
The report that led to v1.1.1 was excellent precisely because it pasted the
entire traceback — that made the diagnosis immediate.
By contributing, you agree that your contributions are licensed under the MIT License that covers this project.