# obsidian-pdf-exporter > obsidian-pdf-exporter (`ope`) is a command-line tool that exports a folder of an Obsidian vault to one PDF, and makes redline (tracked-changes) PDFs that show what changed in that folder between two git commits, tags or branches (`ope redline`). It handles wiki links, embeds, callouts, Mermaid, math, footnotes, Bases and community plugin syntax (Dataview, Meta Bind, Excalidraw, draw.io, citations), and styles the PDF with CSS templates. Install with `uv tool install obsidian-pdf-exporter && ope setup --yes`. - `ope` and `obsidian-pdf-exporter` are the same command. `ope --help` is authoritative. - The PDF is laid out by WeasyPrint from HTML written by pandoc. Templates are CSS Paged Media. - A Claude Code plugin with an agent skill lives in the repository: `/plugin marketplace add Spenhouet/obsidian-pdf-exporter`. --- Source: https://spenhouet.com/obsidian-pdf-exporter/ # obsidian-pdf-exporter

Command line tool · open source · MIT

# Turn a folder of Obsidian notes into one polished PDF

Wiki links, embeds, callouts, Mermaid, Dataview and more, rendered without Obsidian. Every folder becomes a numbered section, with a table of contents and your header and footer. Runs on your machine or in CI.

=== "Linux / macOS" ``` { .bash .ope-cmd } curl -LsSf uvx.sh/obsidian-pdf-exporter/install.sh | sh && ope setup --yes ``` === "Windows" ``` { .powershell .ope-cmd } powershell -ExecutionPolicy ByPass -c "irm https://uvx.sh/obsidian-pdf-exporter/install.ps1 | iex; ope setup --yes" ``` ``` { .bash .ope-cmd } ope export ./MyVault/Handbook --title "Team handbook" ``` [Get started](getting-started/installation.md){ .md-button .md-button--primary } [:fontawesome-brands-github: GitHub](https://github.com/Spenhouet/obsidian-pdf-exporter){ .md-button }

Handbook/Release process/Release process.md

````markdown Every release follows the same four steps. New team members start with [[Onboarding]]. ```mermaid flowchart LR A[Draft] --> B[Review] --> C[Approve] --> D[Publish] ``` > [!warning] Feature freeze > No new features in the ==last week== before a release. ## Who signs off | Step | Who | When | | ------- | -------- | ------------------- | | Review | Reviewer | Within three days | | Approve | Owner | Before the release | ![[Terms#Release]] ```` The note on page 2 of the exported PDF: the numbered section heading, the Mermaid flowchart, the callout, the table and the embedded glossary entry

The basics

## A folder in, one finished PDF out Point `ope` at a folder of your vault. Every subfolder becomes a numbered section, and the note inside it with the same name (`Onboarding/Onboarding.md`) its text. Links between notes, embeds and attachments are resolved across the whole vault. ```text Handbook/ ├── Handbook.md → first page ├── Onboarding/ → 1 Onboarding ├── Release process/ → 2 Release process └── Terms/ → 3 Terms ``` ``` { .bash .ope-cmd } ope export ./Handbook --title "Team handbook" --version 1.2.0 ``` - Title block, table of contents and numbered headings - Running header and footer with title, version, date and page numbers - The default template, or your own CSS Page 1 of the exported handbook: title, version, date, table of contents and section 1 Page 2 of the exported handbook: section 2 with a diagram, a callout and a table ### Everything in your notes, on paper - ![A table of contents and numbered sections made from folders](img/home/folder-notes.webp){ width="720" height="450" loading="lazy" decoding="async" } __Folders become sections__ Numbered headings and a table of contents. [:octicons-arrow-right-24: Folder Notes](plugins/folder-notes.md) - ![Callouts in the PDF, with titles, colours and a nested callout](img/home/callouts.webp){ width="720" height="450" loading="lazy" decoding="async" } __Callouts__ Every type, custom titles, nested and foldable. [:octicons-arrow-right-24: Callouts](obsidian/callouts.md) - ![A whole note, a heading and a block embedded into the PDF](img/home/embed-notes.webp){ width="720" height="450" loading="lazy" decoding="async" } __Embeds__ Notes, headings, blocks, images, SVG and PDF pages. [:octicons-arrow-right-24: Embeds](obsidian/embeds.md) - ![A Mermaid flowchart and a sequence diagram as vector graphics](img/home/mermaid.webp){ width="720" height="450" loading="lazy" decoding="async" } __Mermaid diagrams__ Rendered to sharp vector graphics, without Node.js or a browser. [:octicons-arrow-right-24: Mermaid](obsidian/mermaid.md) - ![A table with a header row and striped rows](img/home/tables.webp){ width="720" height="450" loading="lazy" decoding="async" } __Tables__ Alignment, wide tables on landscape pages. [:octicons-arrow-right-24: Tables](markdown/tables.md) - ![Wiki links rendered as links inside the PDF](img/home/wiki-links.webp){ width="720" height="450" loading="lazy" decoding="async" } __Wiki links__ Links to notes, headings and blocks work in the PDF. [:octicons-arrow-right-24: Wiki links](obsidian/wiki-links.md)

Obsidian syntax and community plugins

## Your plugins, rendered to static pages The syntax of popular community plugins becomes tables, lists and drawings in the PDF, with no Obsidian running. - ![Dataview queries rendered as tables](img/home/dataview.webp){ width="720" height="450" loading="lazy" decoding="async" } __Dataview__ Queries, inline fields and DataviewJS. [:octicons-arrow-right-24: Dataview](plugins/dataview.md) - ![An Obsidian Base rendered as a table](img/home/bases.webp){ width="720" height="450" loading="lazy" decoding="async" } __Bases__ Table views of your notes. [:octicons-arrow-right-24: Bases](obsidian/bases.md) - ![An Excalidraw drawing in the PDF](img/home/excalidraw.webp){ width="720" height="450" loading="lazy" decoding="async" } __Excalidraw__ Drawings, hand-drawn text included. [:octicons-arrow-right-24: Excalidraw](plugins/excalidraw.md) - ![A draw.io diagram in the PDF](img/home/drawio.webp){ width="720" height="450" loading="lazy" decoding="async" } __draw.io__ Diagrams from `.drawio.svg` and `.drawio.png`. [:octicons-arrow-right-24: draw.io](plugins/drawio.md) - ![Citations and a reference list](img/home/citations.webp){ width="720" height="450" loading="lazy" decoding="async" } __Citations__ Pandoc citations and a bibliography. [:octicons-arrow-right-24: Citations](plugins/citations.md) - ![Inline and display math](img/home/math.webp){ width="720" height="450" loading="lazy" decoding="async" } __Math__ LaTeX math, inline and as blocks. [:octicons-arrow-right-24: Math](markdown/math.md) Also: [highlights and comments](obsidian/highlights-comments.md), [tasks](obsidian/tasks.md), [footnotes](markdown/footnotes.md), [properties](obsidian/properties.md), [Meta Bind](plugins/meta-bind.md), [Block Link Plus](plugins/block-link-plus.md), and [plugins you write yourself](plugins/write-a-plugin.md). { .ope-more }

Redline

## See every change between two versions Two git commits of your notes become one tracked-changes PDF: inserted text underlined in blue, removed text struck through in red, a change bar in the margin and a summary of every changed section. Made for reviews, approvals, audits and release notes. ``` { .bash .ope-cmd } ope redline ./Policy --from-commit v1.0.0 --to-commit v1.1.0 ``` The same policy before and after a change, and the redline PDF that shows inserted, removed and changed text First page of a blackline redline with the change summary Side-by-side redline with the old and new version in two columns and a blame strip with author, date and commit message [How Redline works](redline/index.md){ .md-button .md-button--primary } [Redline in CI](redline/usage.md#in-ci){ .md-button }

Templates and styling

## Make it look like your documents A template is a folder with CSS, a running header and footer and page rules. Start from a recipe and change what you need. A page with the default template Default The same page with a letterhead Letterhead The same page with a confidentiality label Confidentiality label The same page with a dark variant Dark variant [Templates and styling](templating/index.md){ .md-button } [Recipes](templating/recipes.md){ .md-button }

How it works

## Plain Markdown in, PDF out, anywhere
  1. Write in ObsidianYour notes stay plain Markdown files in a folder, in git or not.
  2. Run opeOn your machine or in CI. Obsidian does not need to be installed.
  3. Share the PDFOne numbered document, or a redline for the review.
**Working with an AI agent?** `ope` ships a Claude Code plugin with a skill, and an [`llms.txt`](llms.txt) for any other agent. [Use with AI agents :octicons-arrow-right-24:](agents.md)
## Export your first folder in a minute ``` { .bash .ope-cmd } curl -LsSf uvx.sh/obsidian-pdf-exporter/install.sh | sh && ope setup --yes ``` [Quick start](getting-started/quick-start.md){ .md-button .md-button--primary } [Read the introduction](intro.md){ .md-button } [:fontawesome-brands-github: Star on GitHub](https://github.com/Spenhouet/obsidian-pdf-exporter){ .md-button }
--- Source: https://spenhouet.com/obsidian-pdf-exporter/intro/ # Introduction obsidian-pdf-exporter (`ope`) turns a folder of an Obsidian vault into one PDF. It runs on the command line, so the same export works on a laptop and in a CI pipeline. It does not need Obsidian itself. ## Who it is for - Teams who write handbooks, specifications or procedures in Obsidian and hand out PDFs. - Projects that keep their docs in git and want a PDF per release, built by CI. - Reviews that need a tracked-changes PDF between two versions of the docs. ## How it works An export runs in three layers, the same layers the code is split into: 1. Markdown dialect. Pandoc reads the notes as Markdown with a fixed set of extensions. See [Markdown](markdown/index.md). 2. Obsidian core syntax. A list of small steps rewrites what Obsidian adds to Markdown: wiki links, embeds, callouts, comments and more. See [Obsidian](obsidian/index.md). 3. Community plugins. Plugin support modules render syntax of community plugins such as Dataview, Meta Bind, Excalidraw or citations. See [Plugins](plugins/index.md). Pandoc then writes HTML, and [WeasyPrint](https://weasyprint.org/) lays out the PDF with the CSS of a [template](templating/index.md). ```mermaid flowchart LR A[Vault folder] --> B[Core steps and plugins] B --> C[pandoc: HTML] C --> D[Template CSS] D --> E[WeasyPrint: PDF] ``` ## What it is not - Not an Obsidian plugin. It runs outside Obsidian and reads the files on disk. - Not a pixel copy of the Obsidian reading view. The PDF is styled by its template, and some interactive features (folding, input fields, buttons) have no PDF form. --- Source: https://spenhouet.com/obsidian-pdf-exporter/agents/ # Use with AI agents ope is a command-line tool with plain flags, real exit codes and text output, so coding agents can run it like any other CLI. Two things help them use it well: a skill that explains how, and a short index of these docs. ## Claude Code plugin The repository is a Claude Code plugin marketplace with one plugin. The plugin holds the `obsidian-pdf-exporter` skill: how to install ope when it is missing, export, redline, templates, debugging, and how to check the PDF with `pdftotext` and `pdftoppm`. In Claude Code: ```text /plugin marketplace add Spenhouet/obsidian-pdf-exporter /plugin install obsidian-pdf-exporter@obsidian-pdf-exporter ``` Claude Code 2.1.275 and later can do both in one step: ```text /plugin install obsidian-pdf-exporter --marketplace Spenhouet/obsidian-pdf-exporter ``` From a shell: ```bash claude plugin marketplace add Spenhouet/obsidian-pdf-exporter claude plugin install obsidian-pdf-exporter@obsidian-pdf-exporter claude plugin details obsidian-pdf-exporter # shows the skill ``` Updates arrive with `/plugin marketplace update`. The skill loads when a task is about exporting notes to PDF. It can also be called directly as `/obsidian-pdf-exporter:obsidian-pdf-exporter`. ## Copy the skill by hand The skill is a plain [Agent Skill](https://agentskills.io/) folder, [`skills/obsidian-pdf-exporter/`](https://github.com/Spenhouet/obsidian-pdf-exporter/tree/main/skills/obsidian-pdf-exporter). Copy it into the skills folder of your agent: ```bash git clone --depth 1 https://github.com/Spenhouet/obsidian-pdf-exporter /tmp/ope # Claude Code, for all projects mkdir -p ~/.claude/skills && cp -r /tmp/ope/skills/obsidian-pdf-exporter ~/.claude/skills/ # Claude Code, for one project (commit it to share with the team) mkdir -p .claude/skills && cp -r /tmp/ope/skills/obsidian-pdf-exporter .claude/skills/ ``` Other agents that read Agent Skills use their own folder, such as `.github/skills/` or `.agents/skills/`. Tools like `npx skills add Spenhouet/obsidian-pdf-exporter` fetch the same folder. ## Other agents: llms.txt These docs publish an index for language models, following [llmstxt.org](https://llmstxt.org/): - [`llms.txt`](https://spenhouet.com/obsidian-pdf-exporter/llms.txt): a short description and a link to every page. - [`llms-full.txt`](https://spenhouet.com/obsidian-pdf-exporter/llms-full.txt): the text of all pages in one file. Point an agent without skill support at one of them, for example in its rules file: ```markdown ## PDF export Export Obsidian notes to PDF with `ope` (obsidian-pdf-exporter). Install it with `uv tool install obsidian-pdf-exporter && ope setup --yes`. Docs for agents: https://spenhouet.com/obsidian-pdf-exporter/llms.txt ``` ## Working on ope itself The repository has an [`AGENTS.md`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/AGENTS.md) for agents that change the code: setup, checks, the layers of the pipeline, where new syntax goes, and how to rebuild the docs and screenshots. ## Example prompt ```text Export the folder "Handbook" of my vault to PDF with the title "Team handbook" and version 2.0.0. Use a template with our logo (logo.svg in the repo root) at the top right of every page and "Page X of Y" in the footer. Check the PDF and tell me about any warnings or raw Obsidian syntax left in it. ``` With the skill, the agent installs ope if needed, builds a template folder from a copy of `default.css` with the logo as a running element, runs the export, and checks the result with `pdftotext` and the rendered pages. --- Source: https://spenhouet.com/obsidian-pdf-exporter/contributing/ # Contributing Any contribution is welcome! This document provides guidelines for contributing to the obsidian-pdf-exporter project. ## Table of Contents - [Getting Started](#getting-started) - [Development Workflow](#development-workflow) - [Architecture](#architecture) - [Adding support for an Obsidian plugin](#adding-support-for-an-obsidian-plugin) - [Adding a PDF template](#adding-a-pdf-template) - [Patches for WeasyPrint](#patches-for-weasyprint) - [Testing](#testing) - [Code Quality](#code-quality) - [Documentation](#documentation) - [Release Process](#release-process) - [Pull Request Guidelines](#pull-request-guidelines) ## Getting Started ### Prerequisites - Python 3.10 or higher - Git - `uv` (Python package manager) - `jq` (for JSON processing) ### Install jq ```bash sudo apt-get install jq ``` ### Install `uv` Following the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation): ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` Add shell completion (optional): ```bash echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc ``` ### Project Setup 1. **Fork and Clone the Repository** ```bash git clone https://github.com/Spenhouet/obsidian-pdf-exporter.git cd obsidian-pdf-exporter ``` 2. **Install Dependencies** ```bash uv sync --all-groups ``` This will: - Create a virtual environment - Install all dependencies (including development dependencies via dependency groups) - Install the project in editable mode 3. **Verify Installation** ```bash uv run obsidian-pdf-exporter --help uv run ope --help ``` ## Development Workflow ### Running the Application ```bash # Run with uv (recommended) uv run obsidian-pdf-exporter [commands] uv run ope [commands] # Or activate the virtual environment source .venv/bin/activate obsidian-pdf-exporter [commands] ``` ### Adding Dependencies ```bash # Add runtime dependency uv add package-name # Add development dependency (to dev group) uv add --group dev package-name # Add to custom dependency group uv add --group group-name package-name ``` ### Updating Dependencies ```bash # Update all dependencies uv sync --upgrade # Update specific dependency uv sync --upgrade-package package-name ``` ## Architecture An export reads a folder of notes and turns it into one PDF. The work is split into three layers. Each layer has one place where its parts are listed. 1. Markdown dialect ([`core/dialect.py`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/obsidian_pdf_exporter/core/dialect.py)). The pandoc reader extensions and pandoc arguments. The base is pandoc's `markdown` reader with `raw_html` and `lists_without_preceding_blankline` on and `raw_tex` off. Steps add what they need, for example `mark` for highlights, `hard_line_breaks` for line breaks or `--mathml` for math. 2. Obsidian core syntax ([`core/steps/`](https://github.com/Spenhouet/obsidian-pdf-exporter/tree/main/obsidian_pdf_exporter/core/steps/)). Every piece of syntax that Obsidian itself supports is a small step: frontmatter, comments, Bases, PDF embeds, embeds, stray HTML tags, block IDs, callouts, heading links, Markdown images, wiki links, image captions, headings, warnings about plugin code blocks, highlights, line breaks, tasks, math, footnotes, Mermaid, landscape diagrams, SVG images and table fixes. All steps are listed in `CORE_STEPS` in [`core/steps/__init__.py`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/obsidian_pdf_exporter/core/steps/__init__.py). 3. Community plugins ([`plugins/`](https://github.com/Spenhouet/obsidian-pdf-exporter/tree/main/obsidian_pdf_exporter/plugins/)). Support for syntax from community Obsidian plugins and other tools: Folder Notes, Dataview, Excalidraw, Meta Bind, Block Link Plus, draw.io and pandoc citations. Plugins are found through an entry point, listed by `list-plugins` and can be turned off with `--disable-plugin`. They run inside the `plugins` step of layer 2. ```text obsidian_pdf_exporter/ ├── core/ │ ├── exporter.py # export and redline: tree → markdown → HTML → PDF │ ├── redline.py # redline: the block and word diff of two assembled documents │ ├── redline_html.py # redline: summary, TOC marks, moves, change groups, styles │ ├── redline_side_by_side.py, redline_collapse.py # the side-by-side style, collapse │ ├── blame.py # --blame: match blocks to git blame, the strip next to the page │ ├── htmltree.py # parse and write the HTML as a tree (lxml) │ ├── build_files.py # copy images into the build folder, named by their content │ ├── assets/ # CSS of the redline marks, side by side and the blame strip │ ├── assemble.py # runs the markdown hooks on every page │ ├── pipeline.py # the loops over the steps, one per stage │ ├── dialect.py # layer 1: pandoc reader extensions and arguments │ ├── pandoc.py # runs pandoc with a dialect │ ├── steps/ # layer 2: one module per piece of core syntax │ │ ├── __init__.py # CORE_STEPS, the ordered list of steps │ │ ├── base.py # Step, PageContext, DocumentContext │ │ ├── frontmatter.py, plugins.py, comments.py, bases.py, embeds.py, raw_html.py, │ │ ├── block_ids.py, callouts.py, heading_links.py, wiki_links.py, image_captions.py, │ │ ├── headings.py, code_blocks.py, highlights.py, line_breaks.py, tasks.py, math.py, │ │ ├── footnotes.py, landscape.py, pdf_embeds.py, │ │ └── mermaid.py, svg_images.py, tables.py, base_css.py │ ├── bases/ # Bases: expression parser, evaluator and views │ ├── code.py # outside_code: keeps fenced and inline code as written │ └── embeds.py, callouts.py, wiki_links.py, mermaid.py, ... # the work behind the steps ├── compat/ # patches for WeasyPrint, until a release has the fix ├── plugins/ # layer 3 │ ├── base.py # ObsidianPlugin, PluginContext, DocumentContext │ ├── registry.py # entry-point + runtime discovery │ ├── folder_notes/, dataview/, meta_bind/ │ ├── excalidraw/ # Excalidraw drawings drawn as SVG │ ├── block_link_plus/ # ![[Note#^id-id]] ranges │ ├── drawio/ # draw.io SVG labels │ └── citations/ # [@key] citations with pandoc citeproc └── templates/ # PDF templates (CSS, header, footer) ``` ### Stages A step overrides the hooks it needs: | Hook | Runs on | Order | |----------------------------|------------------------------------------|------------------| | `process_markdown` | each page, and embedded notes if `embedded = True` | `priority` | | `process_document` | the assembled markdown of the whole document | `priority` | | `reader_extensions`, `pandoc_args` | builds the dialect for pandoc | `priority` | | `process_html` | the HTML that pandoc wrote | `html_priority` (defaults to `priority`) | An export runs them in this order: the markdown hooks on every page, the document hooks, the template's `process_markdown`, pandoc, the HTML hooks, the template's `process_html`, and WeasyPrint. With `--debug`, the build folder keeps `assembled.md` (what pandoc reads) and `final.html` (what WeasyPrint reads). The order of the markdown steps matters. For example, `embeds` runs after `html_comments`, so an embed inside an HTML comment is not expanded. It runs before `block_ids`, so the block IDs in embedded text are removed too. `bracket_links` runs after `wiki_links`, so real wiki links are already resolved. The table in the docstring of [`core/steps/__init__.py`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/obsidian_pdf_exporter/core/steps/__init__.py) gives the reason for each position. Embedded notes (`![[Note]]`) run through the steps marked `embedded` (`plugins`, `html_comments`, `bases`, `pdf_embeds`, `embeds`, `footnotes` and `wiki_links`) with the context of the embedded note, so links and images resolve from its folder. The embedded text then becomes part of the page and passes the remaining steps. ### Adding a core syntax step Add a step only for syntax that Obsidian supports without a community plugin. Everything else is a plugin (see below). 1. Put the step in a new module under `core/steps/`. Keep it thin and put larger parsing code in a module of its own under `core/`: ```python from obsidian_pdf_exporter.core.steps.base import PageContext from obsidian_pdf_exporter.core.steps.base import Step class SubscriptStep(Step): name = "subscript" description = "Render ~sub~ as subscript" priority = 115 # after image_captions (110), before title_heading (120) def reader_extensions(self, context): return ("subscript",) def process_markdown(self, content: str, context: PageContext) -> str: return content ``` 2. Add the class to `CORE_STEPS` in `core/steps/__init__.py`, at the position of its priority, and to the table in the module docstring. 3. Leave code alone. Obsidian shows fenced code blocks and inline code as written, so wrap the rewrite in `outside_code` from [`core/code.py`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/obsidian_pdf_exporter/core/code.py). 4. Never drop content silently. When the step sees syntax it can not render, it keeps it in a simple form and reports it with `context.warn(...)`, which prints a line starting with `Warning:`. 5. Add tests. [`test_steps.py`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/tests/unit/test_steps.py) checks the order of all steps, so update the expected lists there. 6. Describe the syntax in the docs (see [Documentation](#documentation)). ## Adding support for an Obsidian plugin Obsidian plugins (Dataview, Meta Bind, Tasks, Templater, Excalidraw …) introduce non-core syntax into markdown notes. To make those notes export cleanly, the project carries one **plugin support module** per Obsidian plugin. They all live under [`obsidian_pdf_exporter/plugins/`](https://github.com/Spenhouet/obsidian-pdf-exporter/tree/main/obsidian_pdf_exporter/plugins/), one subpackage per plugin, so contributors have an obvious place to add a new one. ### File layout ```text obsidian_pdf_exporter/plugins/ ├── base.py # ObsidianPlugin base class + PluginContext, DocumentContext ├── registry.py # entry-point + runtime discovery ├── dataview/ │ ├── __init__.py # re-exports DataviewPlugin │ ├── plugin.py # the plugin class (thin) │ ├── parse.py # DQL grammar, on parsimmon.py │ ├── index.py # pages, fields, tasks of the vault │ ├── engine.py # query execution │ ├── render.py # Markdown output of the views │ └── js/ # DataviewJS: dv API, Luxon, embedded V8 ├── drawio/ │ ├── __init__.py │ ├── plugin.py │ └── svg.py # draw.io label conversion └── meta_bind/ ├── __init__.py └── plugin.py ``` Keep the plugin class itself thin and put any non-trivial parsing, indexing or rendering in sibling modules so it stays easy to test in isolation. The Dataview package is the canonical example. The currently bundled plugins are: | Plugin | What it covers | |----------------|----------------------------------------------------------| | `folder_notes` | Map ``Folder/Folder.md`` onto its containing folder | | `dataview` | Run Dataview queries, inline fields and DataviewJS | | `excalidraw` | Turn embedded Excalidraw drawings into images | | `meta_bind` | Resolve Meta Bind ``VIEW`` widgets, strip INPUT/BUTTON | | `block_link_plus` | Resolve ``![[Note#^id-id]]`` range embeds | | `drawio` | Turn draw.io HTML labels in SVG images into SVG text | | `citations` | Render ``[@key]`` citations with ``pandoc --citeproc`` | ### Walkthrough — adding a new plugin Suppose you want to add support for the **Tasks** plugin so its ``- [ ] do thing 📅 2026-05-10`` syntax renders as a clean checkbox list. #### 1. Create the package ```text obsidian_pdf_exporter/plugins/tasks/ ├── __init__.py └── plugin.py ``` `__init__.py`: ```python """Support for the Obsidian `Tasks` plugin.""" from obsidian_pdf_exporter.plugins.tasks.plugin import TasksPlugin __all__ = ["TasksPlugin"] ``` #### 2. Implement the plugin class Every plugin subclasses [`ObsidianPlugin`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/obsidian_pdf_exporter/plugins/base.py) and overrides ``process_markdown(content, context)``. ``context`` is a ``PluginContext`` carrying the page's vault root, page folder, frontmatter, and the user's ``--option key=value`` settings. `plugin.py`: ```python from __future__ import annotations import re from obsidian_pdf_exporter.plugins.base import ObsidianPlugin from obsidian_pdf_exporter.plugins.base import PluginContext _TASK_LINE = re.compile(r"^(\s*-\s\[)([ x/\-])(\]\s.+)$", re.MULTILINE) class TasksPlugin(ObsidianPlugin): """Strip Tasks emoji-suffixes (📅, ⏫, …) and normalise checkbox state.""" name = "tasks" description = "Render Tasks-plugin checkboxes without emoji metadata" priority = 50 # runs after dataview (10) and meta_bind (20) def process_markdown(self, content: str, context: PluginContext) -> str: # Drop the trailing emoji-tagged metadata that Tasks renders. content = re.sub(r"\s+[⏩-⏺✅❌\U0001F4C5][^\n]*", "", content) return _TASK_LINE.sub(lambda m: m.group(1) + m.group(2).lower() + m.group(3), content) ``` The pipeline guarantees: - Frontmatter is already parsed and stripped before your plugin runs. - Plugins run **before** embeds, callouts, wiki link resolution and image promotion, so anything your plugin produces flows through the rest of the pipeline like authored markdown would. - Plugins also run on the content of embedded notes, with the context of the embedded note. - ``context.vault_root`` may be ``None`` for ad-hoc exports outside a git repository — gracefully no-op in that case if you need vault-wide data (see the Dataview plugin for the pattern). #### 3. Register it via an entry point Add the new class to ``pyproject.toml``: ```toml [project.entry-points."obsidian_pdf_exporter.obsidian_plugins"] dataview = "obsidian_pdf_exporter.plugins.dataview:DataviewPlugin" meta_bind = "obsidian_pdf_exporter.plugins.meta_bind:MetaBindPlugin" tasks = "obsidian_pdf_exporter.plugins.tasks:TasksPlugin" ``` Re-install the project in editable mode so the entry point is picked up: ```bash uv pip install -e . ``` Verify: ```bash uv run obsidian-pdf-exporter list-plugins ``` #### 4. Add unit tests Drop a file in `tests/unit/` that imports your plugin directly and feeds it a few representative markdown snippets. Mirror the style in [`test_plugins.py`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/tests/unit/test_plugins.py). #### 5. Document it Add a page under `docs/plugins/`, an example note under `examples/` and its screenshot (see [Documentation](#documentation)), and list the plugin in the table above. ### Plugin priority `priority` is an integer — lower runs earlier. The built-ins use: | Plugin | Priority | |----------------|----------| | `folder_notes` | 5 | | `dataview` | 10 | | `excalidraw` | 15 | | `meta_bind` | 20 | | `block_link_plus` | 30 | | `drawio` | 40 | | `citations` | 50 | Pick a value that respects your dependencies (e.g. if your plugin substitutes frontmatter fields like Meta Bind, run after Dataview so any tables it generated are visible). ### Hooks A plugin can override any of these methods on [`ObsidianPlugin`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/obsidian_pdf_exporter/plugins/base.py). All have no-op defaults so you only implement what you need. A plugin that only implements `process_markdown` keeps working: | Hook | Stage | Used by | |----------------------|--------------------------------------|--------------------------------| | `find_folder_note` | Tree building | `folder_notes` | | `process_markdown` | Per-page markdown rewrite | `dataview`, `meta_bind`, `excalidraw` | | `extract_block` | Resolving `![[Note#^id]]` embeds | `block_link_plus` | | `process_document` | Assembled markdown of the whole document | `citations` | | `reader_extensions`, `pandoc_args` | Building the pandoc call (after `process_document`) | `citations` | | `process_svg` | Inlining SVG images into the HTML | `drawio` | | `process_html` | HTML of the whole document | none yet | `find_folder_note(folder)` returns the markdown file that represents a folder (or ``None``). Plugins are consulted in priority order; the first non-None result wins. Use this when the Obsidian plugin you are mirroring affects which file is the "page" for a folder. Examples: - The bundled `folder_notes` plugin returns ``Folder/Folder.md``. - A `readme_folder_notes` plugin could return ``Folder/README.md``. - A frontmatter-driven plugin could return whichever file in the folder has ``folder-note: true``. `process_markdown(content, context)` runs after frontmatter has been stripped and before callouts / wiki links / image promotion. The ``context`` dataclass exposes ``vault_root``, ``page_folder``, ``frontmatter`` and the user's ``--option key=value`` settings. `extract_block(body, block_id)` returns the text that `![[Note#^block_id]]` selects in the note body, or ``None``. Plugins are asked in priority order before core looks for a block marked ``^block_id``. `process_document(markdown, context)` gets the assembled markdown of the whole document, after the markdown hook ran on every page. The plugin can then return pandoc reader extensions and arguments from `reader_extensions(context)` and `pandoc_args(context)`, for example `--citeproc` when it found citations. `process_svg(svg, context)` rewrites each SVG image while it is inlined into the HTML. `process_html(html, context)` gets the HTML of the whole document after the core HTML steps and before the template. Both get a ``DocumentContext`` with the build folder, the options, the export metadata and the template. ### Disabling and configuring plugins Users can opt out of a single plugin per export with ``--disable-plugin `` (repeatable). Free-form configuration is forwarded through the existing ``--option key=value`` CLI flag and exposed to every hook via ``context.options``. ### Third-party plugins Plugins are not required to live in this repository. Anyone can publish a separate Python distribution that registers a plugin under the same ``obsidian_pdf_exporter.obsidian_plugins`` entry point group; installing it makes it instantly available to ``obsidian-pdf-exporter`` without code changes here. Only add a plugin to this repository if it covers a broadly used Obsidian plugin and is not better maintained as a separate package. ## Adding a PDF template PDF appearance (layout, header, footer, colour palette) is provided by **templates**, a separate extension point that lives next to the plugin system but is not the same thing. Templates plug into rendering, plugins plug into markdown. End users do **not** need to touch this repository to ship a custom look. [Templates and styling](https://spenhouet.com/obsidian-pdf-exporter/templating/) in the docs covers the no-Python options — drop-in CSS file, template directory with a `template.yaml` manifest, or a named directory under `~/.config/obsidian-pdf-exporter/templates/`. Reach for the steps below only when your template needs to mutate markdown or HTML (i.e. requires Python code). Templates that ship inside this project live under [`obsidian_pdf_exporter/templates/builtin/`](https://github.com/Spenhouet/obsidian-pdf-exporter/tree/main/obsidian_pdf_exporter/templates/builtin/); each one ships a ``Template`` subclass plus its CSS file. To add one: 1. Drop a new package next to ``default`` and ``redline``. 2. Subclass [`Template`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/obsidian_pdf_exporter/templates/base.py) and override ``get_css()`` (return CSS source) and optionally ``decorations(ctx)`` to inject running header/footer HTML and ``@page`` rules. 3. Register the class under the ``obsidian_pdf_exporter.templates`` entry point group in ``pyproject.toml`` and reinstall. Branded or company-specific templates should usually be published as a separate distribution rather than added here. ## Patches for WeasyPrint [`compat/weasyprint_patches.py`](https://github.com/Spenhouet/obsidian-pdf-exporter/blob/main/obsidian_pdf_exporter/compat/weasyprint_patches.py) holds small monkey patches for fixes that are merged or proposed upstream but not in a WeasyPrint release yet. Each patch checks the WeasyPrint version and the signature of the function it wraps, and is skipped (with a debug message) when either does not match. Every export applies them once before rendering; `-O weasyprint.patches=off` turns them off. - Elements with the same `style` attribute share one computed style ([Kozea/WeasyPrint#2981](https://github.com/Kozea/WeasyPrint/pull/2981), issue [#2980](https://github.com/Kozea/WeasyPrint/issues/2980)). Applied to WeasyPrint 70.0. Remove the patch when WeasyPrint >= the first release that includes #2981 is the oldest supported version. When a new WeasyPrint release still lacks the fix, raise `LAST_VERSION`. ## Testing We use `pytest` for testing. Tests are located in the `tests/` directory. ### Running Tests ```bash # Run all tests uv run pytest # Run tests with verbose output uv run pytest -v # Run specific test file uv run pytest tests/test_basic.py # Run specific test uv run pytest tests/test_basic.py::test_package_imports ``` ### Writing Tests 1. **Create test files** in the `tests/` directory with the prefix `test_` 2. **Follow naming conventions**: `test_*.py` files, `test_*` functions 3. **Use descriptive test names** that explain what is being tested 4. **Add docstrings** to explain complex test scenarios Example test structure: ```python def test_feature_description() -> None: """Test that the feature works as expected.""" # Arrange input_data = "test input" # Act result = function_under_test(input_data) # Assert assert result == expected_output ``` ## Code Quality ### Linting with Ruff We use `ruff` for Python linting and code formatting. ```bash # Check code quality uv run ruff check # Auto-fix issues where possible uv run ruff check --fix # Check specific files or directories uv run ruff check obsidian_pdf_exporter/ uv run ruff check tests/ ``` ### Code Style Guidelines - **Line length**: Maximum 100 characters - **Docstring style**: Google docstring convention - **Import formatting**: One import per line (enforced by ruff) - **Type hints**: Use type annotations for new code ### Pre-commit Workflow Before committing: 1. **Run linting**: `uv run ruff check` 2. **Run tests**: `uv run pytest` 3. **Fix any issues** before committing ## Documentation The documentation site is built with [Zensical](https://zensical.org/) from `mkdocs.yml` and `docs/`, and published to GitHub Pages by `.github/workflows/docs.yml` on every push to `main`. ```bash uv sync --group docs uv run zensical serve # live preview on http://localhost:8000 uv run zensical build --strict # what CI runs ``` Generated content: - `docs/reference/cli.md` comes from the Typer app. Run `uv run python scripts/gen_cli_docs.py` after changing a command or flag. CI fails when the page is out of date. - `docs/img/features/*.png` and `docs/img/templates/*.png` are rendered from the example vault in `examples/`. Each folder there (not starting with `_`) is one example and becomes `docs/img/features/.png`. Run `uv run python scripts/render_examples.py [name ...]` after a change that affects the output. It needs `pdftoppm` (poppler). Commit the images. - `docs/img/redline/*.png` come from `examples/_redline/`. Each case folder there holds a `before/` and an `after/` version of a small vault. `uv run python scripts/render_redline_examples.py [case ...]` commits both into a temporary git repository (fixed dates, so the hashes do not change), runs `ope redline` in both styles and writes the images. The case `styles` renders `examples/_redline/_styles` in every combination of style, collapse and `--blame`, plus whole pages. Commit the images. - `docs/img/home/*.webp` are the images of the home page. `uv run python scripts/render_home.py` exports the small handbook in `examples/_showcase/Handbook` (the note in the hero is included from there) and makes WebP thumbnails of the feature and template screenshots. Run it after `render_examples.py`. The images have fixed sizes, so the `width` and `height` in `docs/index.md` stay right. - `docs/llms.txt` and `docs/llms-full.txt` come from the pages. Run `uv run python scripts/gen_llms_txt.py` after adding or renaming a page. CI fails when they are out of date. The home page (`docs/index.md` with `overrides/home.html`, styles under "Home page" in `docs/stylesheets/extra.css`) follows what works on the home pages of developer tools: the value in one line, the install and first command with copy buttons above the fold, visual proof (a note next to its PDF page), real output images instead of text-only cards, and a clear next step at the end. Keep the basic export first and Redline as the featured section after it. Images below the fold get `loading="lazy"`, every image gets `width` and `height`, and the page must work at 390px and in the dark theme. A feature page shows the example source next to its image with the `ope-example` block, see any page under `docs/obsidian/`. The source is included with `--8<-- "examples//.md"`, so it cannot drift from the note that was rendered. ## Release Process > [!NOTE] > Only relevant for maintainers. ### Automated Release We use GitHub Actions for automated releases: 1. **Trigger Release Workflow** - Go to GitHub Actions tab - Run "Release" workflow - Choose version bump type (patch/minor/major) or specify custom version 2. **Automated Steps** - Updates version in `pyproject.toml` - Runs tests and builds - Creates Git tag - Publishes to PyPI - Creates GitHub release with auto-generated notes ## Pull Request Guidelines ### Before Submitting 1. **Create a feature branch** ```bash git checkout -b feature/your-feature-name ``` 2. **Run the full test suite** ```bash uv run ruff check uv run pytest uv build --no-sources # Test build ``` 3. **Update documentation** if needed ### PR Requirements - All tests pass (verified by CI) - Code passes linting (ruff check) - Descriptive PR title and description - Reference related issues if applicable - Update tests for new functionality - Update documentation for user-facing changes ## Development Environment ### Recommended Tools - **IDE**: VS Code with Python extension - **Git client**: Command line or your preferred GUI - **Terminal**: Any modern terminal with shell completion ### VS Code Extensions Recommended extensions for development: - Python (Microsoft) - Ruff (Astral Software) ### Project Structure ```text obsidian-pdf-exporter/ ├── .github/workflows/ # CI/CD workflows ├── obsidian_pdf_exporter/ # Main package, see Architecture │ ├── __init__.py │ ├── main.py # CLI entry point │ ├── core/ # Export pipeline, dialect and core syntax steps │ ├── plugins/ # Obsidian community plugin support │ └── templates/ # PDF templates ├── tests/ # Test suite ├── pyproject.toml # Project configuration ├── uv.lock # Dependency lock file └── CONTRIBUTING.md # This file ``` ## Getting Help - **GitHub Issues**: For bug reports and feature requests - **GitHub Discussions**: For questions and general discussion - **Documentation**: https://spenhouet.com/obsidian-pdf-exporter/ --- Source: https://spenhouet.com/obsidian-pdf-exporter/getting-started/ # Getting started ope runs on Linux, macOS and Windows. Install it once, then export a folder of your vault with one command. - :material-download-outline: __Installation__ Install the CLI and the native libraries WeasyPrint and pandoc need. [:octicons-arrow-right-24: Installation](installation.md) - :material-rocket-launch-outline: __Quick start__ Export a folder, set title and version, pick a template and inspect the result. [:octicons-arrow-right-24: Quick start](quick-start.md) - :material-console: __CLI reference__ Every command, argument, option, environment variable and exit code. [:octicons-arrow-right-24: CLI reference](../reference/cli.md) --- Source: https://spenhouet.com/obsidian-pdf-exporter/getting-started/installation/ # Installation ope is a Python package with two native dependencies: the Pango text stack that WeasyPrint uses, and pandoc. The installer below puts the CLI in place, and `ope setup` installs the native parts. ## Install the CLI === "Linux / macOS" ```bash curl -LsSf uvx.sh/obsidian-pdf-exporter/install.sh | sh && ope setup --yes ``` === "Windows" ```powershell powershell -ExecutionPolicy ByPass -c "irm https://uvx.sh/obsidian-pdf-exporter/install.ps1 | iex; ope setup --yes" ``` === "uv" ```bash uv tool install obsidian-pdf-exporter ope setup --yes # or run it once without installing uvx obsidian-pdf-exporter --help ``` === "pip" ```bash pip install obsidian-pdf-exporter ope setup --yes ``` The installer script installs [uv](https://docs.astral.sh/uv/) if needed and runs `uv tool install obsidian-pdf-exporter`, so the CLI lives in its own environment. A pinned version uses the version in the URL: ```bash curl -LsSf uvx.sh/obsidian-pdf-exporter/0.5.1/install.sh | sh ``` `ope` and `obsidian-pdf-exporter` are the same command. ## Native dependencies - [WeasyPrint](https://weasyprint.org/) needs Pango, GLib, HarfBuzz (with `harfbuzz-subset`) and Fontconfig. Cairo is not needed. The package lists follow [WeasyPrint's first steps](https://doc.courtbouillon.org/weasyprint/stable/first_steps.html). - [Pandoc](https://pandoc.org/) converts Markdown to HTML. If it is missing, `ope setup` downloads it from the official pandoc release with `pypandoc`. ```bash ope setup # install what is missing, asks first ope setup --yes # same, without asking (for CI) ope setup --check # report only, exit 1 if something is missing ``` `setup` only runs the package manager of your platform: | OS | What `setup` runs when libraries are missing | Notes | | -- | -------------------------------------------- | ----- | | Debian / Ubuntu | `sudo apt-get install -y libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libharfbuzz-subset0 ...` | Same list as the WeasyPrint docs. | | Fedora | `sudo dnf install -y pango` | Pulls GLib, HarfBuzz and Fontconfig. | | Arch | `sudo pacman -S --needed --noconfirm pango` | | | openSUSE | `sudo zypper install -y pango` | | | Alpine | `sudo apk add pango fontconfig` | | | macOS | `brew install pango` | Needs [Homebrew](https://brew.sh). Without it, `setup` prints the install link and exits with 1. | | Windows | `winget install --id tschoonj.GTKForWindows --accept-source-agreements --accept-package-agreements` | The GTK3 runtime. An [Inkscape](https://inkscape.org/) install also brings GTK. | When WeasyPrint still cannot find the libraries, set `OBSIDIAN_PDF_GTK_PATH` to the folders that hold them, separated by `os.pathsep`. ## Optional tools | Tool | Needed for | | ---- | ---------- | | git | [Redline](../redline/index.md), the version label from the commit hash, and the vault root for Dataview. | ## In CI A GitHub Actions job that exports a folder and keeps the PDF: ```yaml jobs: pdf: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # full history for redline and the version label - uses: astral-sh/setup-uv@v6 - run: uv tool install obsidian-pdf-exporter - run: ope setup --yes - run: ope export ./Handbook --title "Team handbook" --version "${{ github.ref_name }}" -o handbook.pdf - uses: actions/upload-artifact@v4 with: name: handbook path: handbook.pdf ``` [Mermaid](../obsidian/mermaid.md) diagrams are drawn inside ope, so the runner needs no Node.js or browser. ## Check the install ```bash ope --version ope setup --check ope list-templates ope list-plugins ``` --- Source: https://spenhouet.com/obsidian-pdf-exporter/getting-started/quick-start/ # Quick start ## Export a folder ```bash ope export ./MyVault/Handbook ``` The PDF is written to `./.export/Handbook.pdf`. The folder name is the title. Every subfolder becomes a section, see [Folder Notes](../plugins/folder-notes.md). ## Title, version and output ```bash ope export ./MyVault/Handbook \ --title "Team handbook" --subtitle "How we work" \ --version 3.1.0 --date 2026-05-07 \ --output ./out/handbook.pdf ``` The title and subtitle go into the title block and the running header. The version and date go into the footer of the `default` template. Without `--version`, a folder inside a git repository shows the short commit hash. ## Only some chapters ```bash ope export ./MyVault/Handbook --include "Chapter A" --include "Chapter B" ope export ./MyVault/Handbook --exclude "Drafts" --no-toc ``` `--include` and `--exclude` take names of the top-level subfolders. ## A different look ```bash ope list-templates ope export ./MyVault/Handbook --template ./my-template/ ``` A template is a CSS file or a folder with CSS, header and footer. See [Templates and styling](../templating/index.md). ## Settings for features `-O key=value` passes settings to the steps, plugins and the template: ```bash ope export ./MyVault/Handbook -O mermaid.landscape=off -O tables.landscape=off ope export ./MyVault/Handbook --disable-plugin dataview ``` All keys are listed in the [CLI reference](../reference/cli.md#options-for-o-keyvalue). ## Tracked changes ```bash cd MyVault ope redline ./Handbook --from-commit v3.0.0 --to-commit HEAD ``` Inserted text is green, removed text red and struck through. See [Redline](../redline/index.md) for flags, recipes and CI. ## When something looks wrong ```bash ope export ./MyVault/Handbook --debug ``` `--debug` keeps the build folder next to the PDF. `assembled.md` is what pandoc reads, `final.html` is what WeasyPrint lays out. See [Debugging](../templating/debugging.md). --- Source: https://spenhouet.com/obsidian-pdf-exporter/reference/cli/ # CLI reference # CLI reference `ope` and `obsidian-pdf-exporter` are the same command. Run `ope --help` or `ope --help` for the same information in the terminal. ```text ope [--version] COMMAND [ARGS]... ``` | Command | Summary | | ------- | ------- | | [`version`](#version) | Print the installed version. | | [`setup`](#setup) | Install missing system dependencies (Pango/HarfBuzz/Fontconfig, pandoc). | | [`list-templates`](#list-templates) | List all known PDF templates (built-in, packaged, user-config, runtime). | | [`list-plugins`](#list-plugins) | List all installed Obsidian-plugin support modules (built-in + third-party). | | [`export`](#export) | Export an Obsidian-format folder tree to a single PDF file. | | [`redline`](#redline) | Generate a redline (tracked-changes) PDF between two versions. | ## `version` Print the installed version. ```text ope version ``` Prints the installed version. `ope --version` does the same. ```bash ope version ``` ## `setup` Install missing system dependencies (Pango/HarfBuzz/Fontconfig, pandoc). ```text ope setup [OPTIONS] ``` Installs missing native dependencies (Pango, HarfBuzz, Fontconfig and pandoc) with the package manager of the platform. See [Installation](../getting-started/installation.md). | Option | Description | Default | | ------ | ----------- | ------- | | `--check` | Diagnose only — do not install anything (exit 1 if something is missing) | off | | `-y`, `--yes` | Skip the confirmation prompt | off | ```bash ope setup --check # report only ope setup --yes # install without asking ``` ## `list-templates` List all known PDF templates (built-in, packaged, user-config, runtime). ```text ope list-templates ``` Prints every known template with its source: built-in, packaged entry point, user-config folder or registered at runtime. See [Templates](../templating/index.md). ```bash ope list-templates ``` ## `list-plugins` List all installed Obsidian-plugin support modules (built-in + third-party). ```text ope list-plugins ``` Prints every installed plugin support module, sorted by priority. See [Plugins](../plugins/index.md). ```bash ope list-plugins ``` ## `export` Export an Obsidian-format folder tree to a single PDF file. ```text ope export ROOT [OPTIONS] ``` Builds one PDF from a vault folder. Git is not required. When the folder is in a git repository and `--version` is not set, the short commit hash is shown as the version label. See [Folder Notes](../plugins/folder-notes.md) for how the folder becomes sections. | Argument | Description | | -------- | ----------- | | `ROOT` | Path to the vault folder to export (required) | | Option | Description | Default | | ------ | ----------- | ------- | | `-T NAME\|PATH`, `--template NAME\|PATH` | Template name (see `list-templates`) or path to a custom template (.css file or directory with template.yaml) | `default` | | `-o PATH`, `--output PATH` | Output PDF path (default: ./.export/<title>.pdf) | | | `-t TEXT`, `--title TEXT` | Document title (default: folder name) | | | `--subtitle TEXT` | Document subtitle | | | `-v TEXT`, `--version TEXT` | Document version (e.g. 3.1.0) | | | `-d YYYY-MM-DD`, `--date YYYY-MM-DD` | Document date (YYYY-MM-DD, default: today) | | | `--include NAME` | Only include these subfolder names. Repeatable. | | | `--exclude NAME` | Exclude these subfolder names. Repeatable. | | | `-O KEY=VALUE`, `--option KEY=VALUE` | Pass key=value options to plugins / template (repeatable) | | | `--disable-plugin NAME` | Skip the named Obsidian plugin (repeatable; see `list-plugins`) | | | `--toc/--no-toc` | Include a table of contents at the start of the document | `--toc` | | `--blame` | Add a strip next to every page with the last commit that changed each block (author, date, message; details on hover) | off | | `--debug` | Keep the intermediate build directory for inspection | off | ```bash ope export ./Vault/Handbook \ --title "Team handbook" --subtitle "How we work" \ --version 3.1.0 --date 2026-05-07 \ --include "Chapter A" --include "Chapter B" \ --disable-plugin meta_bind \ -O mermaid.landscape=off -O tables.landscape=off ``` ## `redline` Generate a redline (tracked-changes) PDF between two versions. ```text ope redline ROOT [OPTIONS] ``` Builds a tracked-changes PDF between two git commits of `ROOT` (`--from-commit`, run inside the git repository that holds `ROOT`), or between two folders or notes on disk without git (`--from-path`). See [Redline](../redline/index.md) and [Comparing versions](../redline/compare.md). | Argument | Description | | -------- | ----------- | | `ROOT` | Folder or note to compare (with --from-path: the newer version on disk) (required) | | Option | Description | Default | | ------ | ----------- | ------- | | `--from-commit REF` | Older git commit ref (baseline) | | | `--to-commit REF` | Newer git commit ref (default: HEAD) | | | `--from-path PATH` | Older version as a folder or note on disk (baseline), compared with ROOT on disk without git; not with --from-commit, --to-commit or --blame | | | `-T NAME\|PATH`, `--template NAME\|PATH` | Template name (default: redline) or path to a custom template (.css file or directory with template.yaml) | `redline` | | `-o PATH`, `--output PATH` | Output PDF path (default: ./.export/<title>_redline.pdf) | | | `-t TEXT`, `--title TEXT` | Document title (default: folder or note name) | | | `--subtitle TEXT` | Document subtitle | | | `-O KEY=VALUE`, `--option KEY=VALUE` | Pass key=value options to plugins / template (repeatable), e.g. redline.style=side-by-side or redline.collapse=on | | | `--disable-plugin NAME` | Skip the named Obsidian plugin (repeatable; see `list-plugins`) | | | `--toc/--no-toc` | Include a table of contents at the start of the document | `--toc` | | `--blame` | Add a strip next to every page with the last commit that changed each block (author, date, message; details on hover) | off | | `--debug` | Keep the intermediate build directory for inspection | off | ```bash ope redline ./Vault/Handbook --from-commit v3.0.0 --to-commit HEAD ``` ## Options for `-O key=value` `-O` (`--option`) passes free-form settings to the core steps, the plugins and the template. These keys are read by ope itself: | Key | Values | Default | Described in | | --- | ------ | ------- | ------------ | | `line_breaks` | `hard`, `soft` | from `.obsidian/app.json` | [Line breaks](../obsidian/line-breaks.md) | | `links.outside` | `text`, `italic` | `text` | [Wiki links](../obsidian/wiki-links.md#links-to-pages-outside-the-pdf) | | `tables.landscape` | `auto`, `on`, `off` | `auto` | [Tables](../markdown/tables.md#wide-tables) | | `tables.landscape_min_cols` | number | `6` | [Tables](../markdown/tables.md#wide-tables) | | `mermaid` | `off` | on | [Mermaid](../obsidian/mermaid.md) | | `mermaid.config` | path to JSON | none | [Mermaid](../obsidian/mermaid.md#options) | | `mermaid.max_height` | CSS length | none | [Mermaid](../obsidian/mermaid.md#options) | | `mermaid.landscape` | `on`, `off` | `on` | [Mermaid](../obsidian/mermaid.md#wide-diagrams) | | `mermaid.landscape_ratio` | number | `1.4` | [Mermaid](../obsidian/mermaid.md#wide-diagrams) | | `mermaid.landscape_min_font` | points | `7` | [Mermaid](../obsidian/mermaid.md#wide-diagrams) | | `mermaid.fit` | `on`, `off` | `on` | [Mermaid](../obsidian/mermaid.md#tall-diagrams) | | `footnotes` | `page`, `end` | `page` | [Footnotes](../markdown/footnotes.md) | | `pdf_embeds` | `pages`, `link` | `pages` | [Embeds](../obsidian/embeds.md#pdf) | | `pdf_embeds.max_pages` | number | `10` | [Embeds](../obsidian/embeds.md#pdf) | | `pdf_embeds.dpi` | number | `150` | [Embeds](../obsidian/embeds.md#pdf) | | `citations` | `off` | on | [Citations](../plugins/citations.md#options) | | `citations.bibliography` | paths, separated by `,` | auto | [Citations](../plugins/citations.md#bibliography) | | `citations.csl` | path to a CSL style | Chicago author-date | [Citations](../plugins/citations.md#options) | | `citations.title` | text | `References` | [Citations](../plugins/citations.md#options) | | `excalidraw.auto_export` | `on`, `off` | `on` | [Excalidraw](../plugins/excalidraw.md#auto-export-files-first) | | `weasyprint.patches` | `on`, `off` | `on` | [Tables](../markdown/tables.md#large-tables) | Third-party plugins and templates can read any other key. By convention a plugin uses `.` and a template `