Skip to main content

doxtr-d2

A Sphinx extension that renders d2 diagrams via the external d2 binary, with first-class doxtr-pdf-theme-core integration for flawless light/dark LaTeX→PDF output.

The extension works standalone — the theme-core dependency is soft (optional). When the core is importable, dark-palette integration lights up automatically so diagrams use the same colors the theme computed for the page.

Requirements

  • Python ≥ 3.8
  • Sphinx ≥ 5.0
  • The external d2 binary on PATH (or point d2_binary at it). Verified against d2 v0.9.0.
  • For PDF output: LuaLaTeX (the theme core forces lualatex; graphicx/luatex embed PDF natively — no epstopdf needed).

Install

pip install doxtr-d2

Optional extras:

pip install "doxtr-d2[theme]"   # doxtr-pdf-theme-core dark-palette integration
pip install "doxtr-d2[xlink]"   # sphinxcontrib-xlink :xlink: link support
pip install "doxtr-d2[dev]"     # test/dev dependencies

Enable it in conf.py:

extensions = [
    # ...
    "doxtr_d2",
]

Usage

Inline d2 script:

.. d2::

   a -> b: hello
   b -> c

Or a .d2 file argument (resolved relative to the current document, and registered as a rebuild dependency). The file argument and inline content are mutually exclusive — supplying both (or neither) warns and skips the diagram.

.. d2:: diagrams/architecture.d2
   :caption: System architecture
   :width: 80%

Directive options

Option Type Notes
align left/center/right figure/image alignment
alt text alt text (also the man/text placeholder)
caption text wraps the node in a figure with a caption (inline roles/xrefs ok)
class class list extra CSS/LaTeX classes
width length or % figure width (translated to a LaTeX length for PDF)
name text cross-reference target
theme nonneg int base light theme id
dark_theme nonneg int dark fallback theme id (overrides d2_dark_theme)
layout text dagre/elk (+ any configured extra engine)
pad positive int px padding
sketch bool hand-drawn look
scale float render scale (only applied when > 0)
center bool center in the viewbox
format svg/png/pdf/gif per-directive override of the resolved format
target text board selector (--target), e.g. layers.x.*
animate_interval positive int multi-board animated SVG/GIF interval (ms)

Boolean options follow the common convention: a bare flag or 1/true/yes/onTrue; 0/false/no/offFalse.

Configuration

Every directive option has a d2_<option> config default; the directive value wins over the config default.

Config value Default Notes
d2_binary "d2" binary path (argv[0]; invoked with shell=False)
d2_theme 0 base light theme id
d2_dark_theme 200 dark fallback theme id
d2_layout "dagre" default layout engine
d2_extra_layouts [] whitelist of extra engine names (e.g. ["tala"]); a resolved engine outside {dagre, elk} ∪ d2_extra_layouts warns and falls back to the default (raises under d2_strict)
d2_pad 100 px padding
d2_sketch False hand-drawn look
d2_scale -1 --scale appended only when > 0
d2_center False center in the viewbox
d2_class "" default extra class
d2_redirect_links_to_blank_page True SVG <a> target="_blank" for non-external links
d2_animate_interval 0 multi-board animated SVG/GIF interval (ms)
d2_timeout 120 --timeout=<n> and the subprocess timeout
d2_fonts {} {role: ttf-path} (regular/italic/bold/semibold/mono/…)
d2_multiboard_svg "animate" SVG multi-board policy: animate/root/error
d2_builder_formats {} per-builder format overrides (merged over the defaults)
d2_default_format "svg" fallback for builders not in the map
d2_use_theme_core_palette True build custom theme-overrides from the core palette
d2_match_page_background True match the d2 canvas (N7) to the resolved page color
d2_dark_theme_overrides {} user dict deep-merged over the generated block
d2_dark_html False opt-in to apply dark theme-overrides in HTML builds
d2_allowed_link_schemes None None = all schemes except denylist {javascript, data, vbscript}
d2_latex_max_width \linewidth default width cap for the LaTeX visitor
d2_strict False when True, a d2 failure raises instead of warn+skip

Per-builder output formats

Each builder gets a sensible default; override any of them via d2_builder_formats.

Builder Default format
html / dirhtml / singlehtml svg
epub svg
latex pdf
confluence png (experimental)
(any other) d2_default_format (svg)
d2_builder_formats = {"latex": "png", "epub": "png"}

HTML-family builders can only embed svg/png. A per-directive :format: that the active builder cannot embed (e.g. pdf/gif on an HTML builder) is validated once the builder is known: the extension logs a build warning and falls back to a safe embeddable format (SVG for HTML, PDF for LaTeX) rather than emitting a broken diagram. Under d2_strict = True the mismatch raises a D2Error instead.

Dark mode & theme-core integration

d2's --dark-theme only affects a browser in dark mode; a static PDF needs an explicit theme. So for dark PDF/SVG this extension computes a custom d2 theme-overrides block from the theme-core's resolved dark palette and injects it on top of the base --theme=<d2_dark_theme> (default 200). When the core is absent or dark mode is inactive it falls back to that base dark theme id.

Integration is soft: the core import is guarded on ImportError only, so a version-skew (core present but an expected API missing) surfaces as a warning rather than silently degrading to "core absent". At startup the extension logs whether palette integration is active.

You can also ship a hand-authored arch_dark.d2 next to arch.d2; in a dark LaTeX build the core swaps the directive argument to the _dark file. When a swap occurs the auto-generated theme-overrides are skipped so the hand-authored file speaks for itself.

theme-core version floor. The four integration APIs (get_dark_mode_context, is_dark_mode_active, register_dark_swap_directive, mark_image_dark_ready) are exported since 1.1.9, so the optional theme extra pins doxtr-pdf-theme-core>=1.1.9. (The original plan tentatively named >=1.1.10; 1.1.9 is the earliest release verified to export all four.)

d2 link: fields may carry Sphinx cross-references and arbitrary URLs. Three kinds are supported:

  • Internal xrefslink: :ref:\target`(and:doc:` / other domain roles). Resolved to a builder-correct relative URI in HTML. On the PDF path internal xrefs are dropped (the label is kept, but a relative-file URI is meaningless inside a single PDF).
  • sphinxcontrib-xlinklink: :xlink:\link-id`resolves to an external URL via thexlink` config map. Degrades gracefully (warn + drop) when xlink is not installed.
  • Raw / custom-scheme URLs — any link: <uri> passes through scheme-agnostically: https:, mailto:, obsidian://, x-devonthink-item://…, etc. External/custom-scheme links become clickable /URI annotations in the PDF too.

Security: javascript: / data: / vbscript: URIs are an XSS vector in SVG output and are stripped with a warning by default. Configure d2_allowed_link_schemes to change the policy (e.g. ["https", "mailto"] to restrict to an allowlist, or include "data" to opt back in).

Own-line constraint. A link: carrying a role must be on its own line:

node: {
  link: :ref:`my-target`
}

x: { link: :ref:\t` }` on a single line is not rewritten.

By default SVG links get target="_blank" (configurable via d2_redirect_links_to_blank_page).

Multi-board compositions

d2 compositions (layers / scenarios / steps) declare multiple boards. The output must always be a single file, never a directory, so the extension picks a single-file representation per format:

  • SVG — a multi-board SVG written to foo.svg becomes a directory in d2. To avoid that, d2_multiboard_svg selects the policy:
    • "animate" (default) — render an animated SVG (--animate-interval).
    • "root" — render the root board only.
    • "error" — warn and fall back to the root board. A :target: always selects a specific board and takes precedence.
  • PDF — a multi-board diagram renders to a multipage PDF (one board per page). \includegraphics embeds page 1 only, so set :target: to choose a specific board; without it the extension warns and shows the first board.
  • GIF — an animated multi-board GIF is the natural single-file form.

LaTeX / PDF notes

  • PDF output requires LuaLaTeX.
  • A raw d2 PDF (~203 mm) overflows the text block, so the LaTeX visitor always emits width=\linewidth,height=0.9\textheight,keepaspectratio (width from d2_latex_max_width; a :width: value, including %, is translated to a LaTeX length).
  • d2 PDFs embed real clickable /URI link annotations, so external/custom-scheme links survive into the PDF. Internal :ref: cross-references are an HTML-only feature (a relative-file URI is meaningless inside a single PDF).

Layout engines

d2 ships two free layout engines, dagre (default) and elk. A licensed engine such as tala must be whitelisted by name in d2_extra_layouts so a typo'd engine is caught rather than silently handed to d2:

d2_extra_layouts = ["tala"]  # then :layout: tala is accepted

A resolved layout engine outside {dagre, elk} ∪ d2_extra_layouts is rejected: the extension logs a build warning and falls back to d2's default engine (or raises a D2Error under d2_strict).

Replacing the renderer

Everything that shells out to d2 lives behind a single indirection, doxtr_d2.render.render_backend (which defaults to doxtr_d2.render.render_d2). The builder visitors never call render_d2 directly — they call render.render_backend(...). A child theme can therefore replace all rendering by reassigning that one value, instead of overriding every per-builder visitor:

import doxtr_d2.render as render

def my_render_backend(stdin_bytes, fmt, resolved_options, image_dir, **kwargs):
    # e.g. call a d2 HTTP service or a different diagram tool.
    # Contract: write the artifact under image_dir and return its filename
    # RELATIVE to image_dir; raise doxtr_d2.errors.D2Error on failure.
    ...
    return "d2-<hash>.%s" % fmt

render.render_backend = my_render_backend

Keep the signature and return/raise contract identical to render_d2 ((stdin_bytes, fmt, resolved_options, image_dir, *, binary, fonts, timeout, cwd, post_process) -> out_name) and the visitors, caching key, and atomic-write guarantees all continue to work unchanged.

License

MIT — see LICENSE.

Release files for doxtr-d2 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for doxtr-d2 0.1.0
File Size Uploaded
doxtr_d2-0.1.0.tar.gz 45.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for doxtr-d2 0.1.0
File Interpreter ABI Platform
doxtr_d2-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 81.4 kB

Release files / doxtr_d2-0.1.0.tar.gz

Download URL doxtr_d2-0.1.0.tar.gz
Size 45.8 kB
Tags Source
SHA-256 checksum
How to use checksums
1098ea316b0064dcd4e4e6e8c619a3c07c3119b20e7234f14d34ec4d446f4c42
BLAKE2b-256 checksum
How to use checksums
e98ddba19fa1814ccb7feb40ea731397cadc80d60578412f8399d9cd77f1ffb9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release files / doxtr_d2-0.1.0-py3-none-any.whl

Download URL doxtr_d2-0.1.0-py3-none-any.whl
Size 35.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
de58c1ebcb288648a0c343cf126552d3f44ffd97161528a4aaa1c54adade0229
BLAKE2b-256 checksum
How to use checksums
9f74326983f6aab4d17385021f91ca53a634beedce1e2204d0bdbcd5ebeb4889
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 15, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page