Convert a Markdown document to PDF in Python using xhtml2pdf and readme.
- Install uv following the directions if necessary.
- Install Python (if not already available):
uv python install- This ensures a compatible Python version (>=3.13) is available
- Sync dependencies:
uv sync --frozen --all-groups - Install the project in editable mode:
uv pip install -e .- This allows tests to import
mainandservicemodules without path manipulation - Changes to code are immediately reflected without reinstallation
- This allows tests to import
- Install Playwright browsers for end-to-end testing:
uv run playwright install chromium- Only needed if running Playwright tests
- Chromium is the fastest/smallest browser for testing
Once the prerequisites are installed, run the main.py script passing the path to the markdown file as an argument:
# sh
uv run python main.py spam.mdOptionally, specify a CSS file with the --css flag:
# sh
uv run python main.py spam.md --css=eggs.cssFor more information on defining things such as page size and margins, see the xhtml2pdf documentation on Defining Page Layouts.
# sh
uv run python service.pyThe service runs in debug mode by default and will be available at http://127.0.0.1:5000
- Prototype in Code: Python script which takes a markdown file as an argument and returns a formatted PDF
- Styling with CSS: Modify the stying of the output. Building towards having a default stylesheet, the ability to specify a separate stylesheet as a script argument, and documentation for creating additional stylesheets.
- [IN PROGRESS] Backend Service: Abstract the functional prototype from the command line interface, adding a web server to allow posting a markdown file and optionally a stylesheet (ref. https://flask.palletsprojects.com/en/stable/patterns/fileuploads/)
- Test services.py (ref. https://testdriven.io/blog/flask-pytest/)
- Delete uploads after processing
- Allow drag-and-drop uploading (ref. https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop)
- Allow for choosing between stylesheets (currently
default.csswhich is a sans-serif typeface andminimal.csswhich just sets up an A4 page size) - Containerize / make production ready
- Design the UI
- Style the template
- Mac OS / iOS app: Abstract backend service to use pywebview (JavaScript calls Python directly without web server) / evaluate Tauri (continue to use web server), package into a windowed .app including the frontend build as app data
- Allow greater control over styling - one idea would be to choose options e.g. "A4" or "Letter" and "Landscape" or "Portrait" and maybe font, base font size, etc. and have the option to remember that for next time
By submitting a pull request, you agree that your contributions will be licensed under the same license as this project (MIT), and you grant the project owner (you) perpetual rights to use your contribution in the commercial app.
Activate the virtual environment to run pytest or ruff directly
# sh
source .venv/bin/activate # On Windows: .venv\Scripts\activateDeactivate the virtual environment
# sh
deactivateUse uv run to run tools without activating the virtual environment, for example:
# sh
uv run pytestRun all tests (see note about needing a server to be running for end-to-end tests if doing this):
# sh
uv run pytestRun specific test types:
# sh
# Unit tests only
uv run pytest -m unit
# Integration tests only
uv run pytest -m integration
# Everything except end-to-end tests
uv run pytest -m "not e2e"
# Specific test file
uv run pytest tests/test_main.py -vRun non end-to-end tests with coverage reports:
# sh
# Generate HTML coverage report
uv run pytest -m "not e2e" --cov=main --cov=service --cov-report=html
# Show coverage in terminal with missing lines
uv run pytest -m "not e2e" --cov=main --cov=service --cov-report=term-missingNote: In VSCode use the ms-vscode.live-server extension to open HTML coverage reports via the Show Preview option when right-clicking the htmlcov/index.html file.
Run Playwright end-to-end tests (requires playwright install chromium):
Important: You need to have the Flask server running first:
Terminal 1: Start the server
# sh
uv run python service.pyTerminal 2: Run the tests
# sh
# Run all Playwright tests
uv run pytest tests/test_e2e.py
# Run all end-to-end tests
uv run pytest -m e2e
# Run in headed mode (see browser)
uv run pytest tests/test_e2e.py --headed
# Run with slow motion for debugging
uv run pytest tests/test_e2e.py --headed --slowmo 1000Always run formatting and linting after making edits:
# sh
ruff format main.py# sh
ruff check --fix main.py# sh
ruff check main.py- Configure PyCharm to use the uv environment
- Configure PyCharm to Format Code with Black when running the format code command and/or on save
Set pytest as the default test runner:
- On macOS: PyCharm > Settings (or Preferences) > Tools > Python Integrated Tools > Testing > Default test runner: pytest.
Create a Run/Debug configuration using pytest:
- Create a new configuration of type “Python tests” > “pytest”.
- Ensure the Working directory is your project root and the Interpreter is your project’s .venv Python.