doxtr-d2 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- doxtr_d2-0.1.0/LICENSE +21 -0
- doxtr_d2-0.1.0/PKG-INFO +301 -0
- doxtr_d2-0.1.0/README.md +276 -0
- doxtr_d2-0.1.0/doxtr_d2/__init__.py +138 -0
- doxtr_d2-0.1.0/doxtr_d2/darkmode.py +422 -0
- doxtr_d2-0.1.0/doxtr_d2/directive.py +201 -0
- doxtr_d2-0.1.0/doxtr_d2/errors.py +17 -0
- doxtr_d2-0.1.0/doxtr_d2/formats.py +246 -0
- doxtr_d2-0.1.0/doxtr_d2/links.py +304 -0
- doxtr_d2-0.1.0/doxtr_d2/nodes.py +366 -0
- doxtr_d2-0.1.0/doxtr_d2/render.py +333 -0
- doxtr_d2-0.1.0/doxtr_d2.egg-info/PKG-INFO +301 -0
- doxtr_d2-0.1.0/doxtr_d2.egg-info/SOURCES.txt +22 -0
- doxtr_d2-0.1.0/doxtr_d2.egg-info/dependency_links.txt +1 -0
- doxtr_d2-0.1.0/doxtr_d2.egg-info/requires.txt +13 -0
- doxtr_d2-0.1.0/doxtr_d2.egg-info/top_level.txt +1 -0
- doxtr_d2-0.1.0/pyproject.toml +62 -0
- doxtr_d2-0.1.0/setup.cfg +4 -0
- doxtr_d2-0.1.0/tests/test_darkmode.py +263 -0
- doxtr_d2-0.1.0/tests/test_directive.py +155 -0
- doxtr_d2-0.1.0/tests/test_formats.py +218 -0
- doxtr_d2-0.1.0/tests/test_links.py +202 -0
- doxtr_d2-0.1.0/tests/test_nodes.py +207 -0
- doxtr_d2-0.1.0/tests/test_render.py +325 -0
doxtr_d2-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Doxtr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
doxtr_d2-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: doxtr-d2
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sphinx extension that renders d2 diagrams, with first-class doxtr-pdf-theme-core PDF integration.
|
|
5
|
+
Author: Jens Frey
|
|
6
|
+
Project-URL: Homepage, https://github.com/doxtr/doxtr-d2
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Framework :: Sphinx :: Extension
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: sphinx>=5.0
|
|
15
|
+
Provides-Extra: theme
|
|
16
|
+
Requires-Dist: doxtr-pdf-theme-core>=1.1.9; extra == "theme"
|
|
17
|
+
Provides-Extra: xlink
|
|
18
|
+
Requires-Dist: sphinxcontrib-xlink; extra == "xlink"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
22
|
+
Requires-Dist: doxtr-pdf-theme-core>=1.1.9; extra == "dev"
|
|
23
|
+
Requires-Dist: sphinxcontrib-xlink; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# doxtr-d2
|
|
27
|
+
|
|
28
|
+
A Sphinx extension that renders [d2](https://d2lang.com/) diagrams via the
|
|
29
|
+
external `d2` binary, with first-class
|
|
30
|
+
[`doxtr-pdf-theme-core`](https://github.com/doxtr/doxtr-pdf-theme-core)
|
|
31
|
+
integration for flawless light/dark LaTeX→PDF output.
|
|
32
|
+
|
|
33
|
+
The extension works **standalone** — the theme-core dependency is soft
|
|
34
|
+
(optional). When the core is importable, dark-palette integration lights up
|
|
35
|
+
automatically so diagrams use the same colors the theme computed for the page.
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python ≥ 3.8
|
|
40
|
+
- Sphinx ≥ 5.0
|
|
41
|
+
- The external [`d2`](https://d2lang.com/tour/install) binary on `PATH`
|
|
42
|
+
(or point `d2_binary` at it). Verified against **d2 v0.9.0**.
|
|
43
|
+
- For PDF output: **LuaLaTeX** (the theme core forces `lualatex`; graphicx/luatex
|
|
44
|
+
embed PDF natively — no `epstopdf` needed).
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install doxtr-d2
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Optional extras:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install "doxtr-d2[theme]" # doxtr-pdf-theme-core dark-palette integration
|
|
56
|
+
pip install "doxtr-d2[xlink]" # sphinxcontrib-xlink :xlink: link support
|
|
57
|
+
pip install "doxtr-d2[dev]" # test/dev dependencies
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Enable it in `conf.py`:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
extensions = [
|
|
64
|
+
# ...
|
|
65
|
+
"doxtr_d2",
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
Inline d2 script:
|
|
72
|
+
|
|
73
|
+
```rst
|
|
74
|
+
.. d2::
|
|
75
|
+
|
|
76
|
+
a -> b: hello
|
|
77
|
+
b -> c
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or a `.d2` file argument (resolved relative to the current document, and
|
|
81
|
+
registered as a rebuild dependency). The file argument and inline content are
|
|
82
|
+
**mutually exclusive** — supplying both (or neither) warns and skips the
|
|
83
|
+
diagram.
|
|
84
|
+
|
|
85
|
+
```rst
|
|
86
|
+
.. d2:: diagrams/architecture.d2
|
|
87
|
+
:caption: System architecture
|
|
88
|
+
:width: 80%
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Directive options
|
|
92
|
+
|
|
93
|
+
| Option | Type | Notes |
|
|
94
|
+
|--------|------|-------|
|
|
95
|
+
| `align` | `left`/`center`/`right` | figure/image alignment |
|
|
96
|
+
| `alt` | text | alt text (also the man/text placeholder) |
|
|
97
|
+
| `caption` | text | wraps the node in a `figure` with a caption (inline roles/xrefs ok) |
|
|
98
|
+
| `class` | class list | extra CSS/LaTeX classes |
|
|
99
|
+
| `width` | length or `%` | figure width (translated to a LaTeX length for PDF) |
|
|
100
|
+
| `name` | text | cross-reference target |
|
|
101
|
+
| `theme` | nonneg int | base light theme id |
|
|
102
|
+
| `dark_theme` | nonneg int | dark fallback theme id (overrides `d2_dark_theme`) |
|
|
103
|
+
| `layout` | text | `dagre`/`elk` (+ any configured extra engine) |
|
|
104
|
+
| `pad` | positive int | px padding |
|
|
105
|
+
| `sketch` | bool | hand-drawn look |
|
|
106
|
+
| `scale` | float | render scale (only applied when `> 0`) |
|
|
107
|
+
| `center` | bool | center in the viewbox |
|
|
108
|
+
| `format` | `svg`/`png`/`pdf`/`gif` | per-directive override of the resolved format |
|
|
109
|
+
| `target` | text | board selector (`--target`), e.g. `layers.x.*` |
|
|
110
|
+
| `animate_interval` | positive int | multi-board animated SVG/GIF interval (ms) |
|
|
111
|
+
|
|
112
|
+
Boolean options follow the common convention: a bare flag or
|
|
113
|
+
`1`/`true`/`yes`/`on` ⇒ `True`; `0`/`false`/`no`/`off` ⇒ `False`.
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
Every directive option has a `d2_<option>` config default; the directive value
|
|
118
|
+
wins over the config default.
|
|
119
|
+
|
|
120
|
+
| Config value | Default | Notes |
|
|
121
|
+
|--------------|---------|-------|
|
|
122
|
+
| `d2_binary` | `"d2"` | binary path (argv[0]; invoked with `shell=False`) |
|
|
123
|
+
| `d2_theme` | `0` | base light theme id |
|
|
124
|
+
| `d2_dark_theme` | `200` | dark fallback theme id |
|
|
125
|
+
| `d2_layout` | `"dagre"` | default layout engine |
|
|
126
|
+
| `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`) |
|
|
127
|
+
| `d2_pad` | `100` | px padding |
|
|
128
|
+
| `d2_sketch` | `False` | hand-drawn look |
|
|
129
|
+
| `d2_scale` | `-1` | `--scale` appended only when `> 0` |
|
|
130
|
+
| `d2_center` | `False` | center in the viewbox |
|
|
131
|
+
| `d2_class` | `""` | default extra class |
|
|
132
|
+
| `d2_redirect_links_to_blank_page` | `True` | SVG `<a>` `target="_blank"` for non-external links |
|
|
133
|
+
| `d2_animate_interval` | `0` | multi-board animated SVG/GIF interval (ms) |
|
|
134
|
+
| `d2_timeout` | `120` | `--timeout=<n>` and the subprocess timeout |
|
|
135
|
+
| `d2_fonts` | `{}` | `{role: ttf-path}` (`regular`/`italic`/`bold`/`semibold`/`mono`/…) |
|
|
136
|
+
| `d2_multiboard_svg` | `"animate"` | SVG multi-board policy: `animate`/`root`/`error` |
|
|
137
|
+
| `d2_builder_formats` | `{}` | per-builder format overrides (merged over the defaults) |
|
|
138
|
+
| `d2_default_format` | `"svg"` | fallback for builders not in the map |
|
|
139
|
+
| `d2_use_theme_core_palette` | `True` | build custom theme-overrides from the core palette |
|
|
140
|
+
| `d2_match_page_background` | `True` | match the d2 canvas (`N7`) to the resolved page color |
|
|
141
|
+
| `d2_dark_theme_overrides` | `{}` | user dict deep-merged over the generated block |
|
|
142
|
+
| `d2_dark_html` | `False` | opt-in to apply dark theme-overrides in HTML builds |
|
|
143
|
+
| `d2_allowed_link_schemes` | `None` | `None` = all schemes except denylist `{javascript, data, vbscript}` |
|
|
144
|
+
| `d2_latex_max_width` | `\linewidth` | default width cap for the LaTeX visitor |
|
|
145
|
+
| `d2_strict` | `False` | when `True`, a d2 failure raises instead of warn+skip |
|
|
146
|
+
|
|
147
|
+
## Per-builder output formats
|
|
148
|
+
|
|
149
|
+
Each builder gets a sensible default; override any of them via
|
|
150
|
+
`d2_builder_formats`.
|
|
151
|
+
|
|
152
|
+
| Builder | Default format |
|
|
153
|
+
|---------|----------------|
|
|
154
|
+
| `html` / `dirhtml` / `singlehtml` | `svg` |
|
|
155
|
+
| `epub` | `svg` |
|
|
156
|
+
| `latex` | `pdf` |
|
|
157
|
+
| `confluence` | `png` (experimental) |
|
|
158
|
+
| (any other) | `d2_default_format` (`svg`) |
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
d2_builder_formats = {"latex": "png", "epub": "png"}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
HTML-family builders can only embed `svg`/`png`. A per-directive `:format:`
|
|
165
|
+
that the active builder cannot embed (e.g. `pdf`/`gif` on an HTML builder) is
|
|
166
|
+
**validated once the builder is known**: the extension logs a build warning and
|
|
167
|
+
falls back to a safe embeddable format (SVG for HTML, PDF for LaTeX) rather than
|
|
168
|
+
emitting a broken diagram. Under `d2_strict = True` the mismatch raises a
|
|
169
|
+
`D2Error` instead.
|
|
170
|
+
|
|
171
|
+
## Dark mode & theme-core integration
|
|
172
|
+
|
|
173
|
+
d2's `--dark-theme` only affects a browser in dark mode; a static PDF needs an
|
|
174
|
+
explicit theme. So for dark PDF/SVG this extension computes a custom d2
|
|
175
|
+
`theme-overrides` block from the theme-core's resolved dark palette and injects
|
|
176
|
+
it on top of the base `--theme=<d2_dark_theme>` (default `200`). When the core
|
|
177
|
+
is absent or dark mode is inactive it falls back to that base dark theme id.
|
|
178
|
+
|
|
179
|
+
Integration is **soft**: the core import is guarded on `ImportError` only, so a
|
|
180
|
+
version-skew (core present but an expected API missing) surfaces as a warning
|
|
181
|
+
rather than silently degrading to "core absent". At startup the extension logs
|
|
182
|
+
whether palette integration is active.
|
|
183
|
+
|
|
184
|
+
You can also ship a hand-authored `arch_dark.d2` next to `arch.d2`; in a dark
|
|
185
|
+
LaTeX build the core swaps the directive argument to the `_dark` file. When a
|
|
186
|
+
swap occurs the auto-generated `theme-overrides` are skipped so the hand-authored
|
|
187
|
+
file speaks for itself.
|
|
188
|
+
|
|
189
|
+
> **theme-core version floor.** The four integration APIs (`get_dark_mode_context`,
|
|
190
|
+
> `is_dark_mode_active`, `register_dark_swap_directive`, `mark_image_dark_ready`)
|
|
191
|
+
> are exported since **1.1.9**, so the optional `theme` extra pins
|
|
192
|
+
> `doxtr-pdf-theme-core>=1.1.9`. (The original plan tentatively named `>=1.1.10`;
|
|
193
|
+
> 1.1.9 is the earliest release verified to export all four.)
|
|
194
|
+
|
|
195
|
+
## Links in diagrams
|
|
196
|
+
|
|
197
|
+
d2 `link:` fields may carry Sphinx cross-references and arbitrary URLs. Three
|
|
198
|
+
kinds are supported:
|
|
199
|
+
|
|
200
|
+
- **Internal xrefs** — `link: :ref:\`target\`` (and `:doc:` / other domain
|
|
201
|
+
roles). Resolved to a builder-correct relative URI in HTML. On the PDF path
|
|
202
|
+
internal xrefs are **dropped** (the label is kept, but a relative-file URI is
|
|
203
|
+
meaningless inside a single PDF).
|
|
204
|
+
- **`sphinxcontrib-xlink`** — `link: :xlink:\`link-id\`` resolves to an external
|
|
205
|
+
URL via the `xlink` config map. Degrades gracefully (warn + drop) when xlink
|
|
206
|
+
is not installed.
|
|
207
|
+
- **Raw / custom-scheme URLs** — any `link: <uri>` passes through
|
|
208
|
+
scheme-agnostically: `https:`, `mailto:`, `obsidian://`,
|
|
209
|
+
`x-devonthink-item://…`, etc. External/custom-scheme links become clickable
|
|
210
|
+
`/URI` annotations in the PDF too.
|
|
211
|
+
|
|
212
|
+
**Security:** `javascript:` / `data:` / `vbscript:` URIs are an XSS vector in SVG
|
|
213
|
+
output and are **stripped with a warning by default**. Configure
|
|
214
|
+
`d2_allowed_link_schemes` to change the policy (e.g. `["https", "mailto"]` to
|
|
215
|
+
restrict to an allowlist, or include `"data"` to opt back in).
|
|
216
|
+
|
|
217
|
+
> **Own-line constraint.** A `link:` carrying a role must be on its **own line**:
|
|
218
|
+
>
|
|
219
|
+
> ```d2
|
|
220
|
+
> node: {
|
|
221
|
+
> link: :ref:`my-target`
|
|
222
|
+
> }
|
|
223
|
+
> ```
|
|
224
|
+
>
|
|
225
|
+
> `x: { link: :ref:\`t\` }` on a single line is **not** rewritten.
|
|
226
|
+
|
|
227
|
+
By default SVG links get `target="_blank"` (configurable via
|
|
228
|
+
`d2_redirect_links_to_blank_page`).
|
|
229
|
+
|
|
230
|
+
## Multi-board compositions
|
|
231
|
+
|
|
232
|
+
d2 compositions (`layers` / `scenarios` / `steps`) declare multiple boards. The
|
|
233
|
+
output must always be a **single file**, never a directory, so the extension
|
|
234
|
+
picks a single-file representation per format:
|
|
235
|
+
|
|
236
|
+
- **SVG** — a multi-board SVG written to `foo.svg` becomes a *directory* in d2.
|
|
237
|
+
To avoid that, `d2_multiboard_svg` selects the policy:
|
|
238
|
+
- `"animate"` (default) — render an animated SVG (`--animate-interval`).
|
|
239
|
+
- `"root"` — render the root board only.
|
|
240
|
+
- `"error"` — warn and fall back to the root board.
|
|
241
|
+
A `:target:` always selects a specific board and takes precedence.
|
|
242
|
+
- **PDF** — a multi-board diagram renders to a multipage PDF (one board per
|
|
243
|
+
page). `\includegraphics` embeds **page 1 only**, so set `:target:` to choose
|
|
244
|
+
a specific board; without it the extension warns and shows the first board.
|
|
245
|
+
- **GIF** — an animated multi-board GIF is the natural single-file form.
|
|
246
|
+
|
|
247
|
+
## LaTeX / PDF notes
|
|
248
|
+
|
|
249
|
+
- PDF output requires **LuaLaTeX**.
|
|
250
|
+
- A raw d2 PDF (~203 mm) overflows the text block, so the LaTeX visitor always
|
|
251
|
+
emits `width=\linewidth,height=0.9\textheight,keepaspectratio` (width from
|
|
252
|
+
`d2_latex_max_width`; a `:width:` value, including `%`, is translated to a
|
|
253
|
+
LaTeX length).
|
|
254
|
+
- d2 PDFs embed real clickable `/URI` link annotations, so external/custom-scheme
|
|
255
|
+
links survive into the PDF. Internal `:ref:` cross-references are an HTML-only
|
|
256
|
+
feature (a relative-file URI is meaningless inside a single PDF).
|
|
257
|
+
|
|
258
|
+
## Layout engines
|
|
259
|
+
|
|
260
|
+
d2 ships two free layout engines, `dagre` (default) and `elk`. A licensed engine
|
|
261
|
+
such as `tala` must be **whitelisted** by name in `d2_extra_layouts` so a typo'd
|
|
262
|
+
engine is caught rather than silently handed to d2:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
d2_extra_layouts = ["tala"] # then :layout: tala is accepted
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
A resolved layout engine outside `{dagre, elk} ∪ d2_extra_layouts` is rejected:
|
|
269
|
+
the extension logs a build warning and falls back to d2's default engine (or
|
|
270
|
+
raises a `D2Error` under `d2_strict`).
|
|
271
|
+
|
|
272
|
+
## Replacing the renderer
|
|
273
|
+
|
|
274
|
+
Everything that shells out to `d2` lives behind a single indirection,
|
|
275
|
+
`doxtr_d2.render.render_backend` (which defaults to
|
|
276
|
+
`doxtr_d2.render.render_d2`). The builder visitors never call `render_d2`
|
|
277
|
+
directly — they call `render.render_backend(...)`. A child theme can therefore
|
|
278
|
+
replace **all** rendering by reassigning that one value, instead of overriding
|
|
279
|
+
every per-builder visitor:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
import doxtr_d2.render as render
|
|
283
|
+
|
|
284
|
+
def my_render_backend(stdin_bytes, fmt, resolved_options, image_dir, **kwargs):
|
|
285
|
+
# e.g. call a d2 HTTP service or a different diagram tool.
|
|
286
|
+
# Contract: write the artifact under image_dir and return its filename
|
|
287
|
+
# RELATIVE to image_dir; raise doxtr_d2.errors.D2Error on failure.
|
|
288
|
+
...
|
|
289
|
+
return "d2-<hash>.%s" % fmt
|
|
290
|
+
|
|
291
|
+
render.render_backend = my_render_backend
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Keep the signature and return/raise contract identical to `render_d2`
|
|
295
|
+
(`(stdin_bytes, fmt, resolved_options, image_dir, *, binary, fonts, timeout,
|
|
296
|
+
cwd, post_process) -> out_name`) and the visitors, caching key, and atomic-write
|
|
297
|
+
guarantees all continue to work unchanged.
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
MIT — see [LICENSE](LICENSE).
|
doxtr_d2-0.1.0/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# doxtr-d2
|
|
2
|
+
|
|
3
|
+
A Sphinx extension that renders [d2](https://d2lang.com/) diagrams via the
|
|
4
|
+
external `d2` binary, with first-class
|
|
5
|
+
[`doxtr-pdf-theme-core`](https://github.com/doxtr/doxtr-pdf-theme-core)
|
|
6
|
+
integration for flawless light/dark LaTeX→PDF output.
|
|
7
|
+
|
|
8
|
+
The extension works **standalone** — the theme-core dependency is soft
|
|
9
|
+
(optional). When the core is importable, dark-palette integration lights up
|
|
10
|
+
automatically so diagrams use the same colors the theme computed for the page.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Python ≥ 3.8
|
|
15
|
+
- Sphinx ≥ 5.0
|
|
16
|
+
- The external [`d2`](https://d2lang.com/tour/install) binary on `PATH`
|
|
17
|
+
(or point `d2_binary` at it). Verified against **d2 v0.9.0**.
|
|
18
|
+
- For PDF output: **LuaLaTeX** (the theme core forces `lualatex`; graphicx/luatex
|
|
19
|
+
embed PDF natively — no `epstopdf` needed).
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install doxtr-d2
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Optional extras:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install "doxtr-d2[theme]" # doxtr-pdf-theme-core dark-palette integration
|
|
31
|
+
pip install "doxtr-d2[xlink]" # sphinxcontrib-xlink :xlink: link support
|
|
32
|
+
pip install "doxtr-d2[dev]" # test/dev dependencies
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Enable it in `conf.py`:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
extensions = [
|
|
39
|
+
# ...
|
|
40
|
+
"doxtr_d2",
|
|
41
|
+
]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
Inline d2 script:
|
|
47
|
+
|
|
48
|
+
```rst
|
|
49
|
+
.. d2::
|
|
50
|
+
|
|
51
|
+
a -> b: hello
|
|
52
|
+
b -> c
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or a `.d2` file argument (resolved relative to the current document, and
|
|
56
|
+
registered as a rebuild dependency). The file argument and inline content are
|
|
57
|
+
**mutually exclusive** — supplying both (or neither) warns and skips the
|
|
58
|
+
diagram.
|
|
59
|
+
|
|
60
|
+
```rst
|
|
61
|
+
.. d2:: diagrams/architecture.d2
|
|
62
|
+
:caption: System architecture
|
|
63
|
+
:width: 80%
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Directive options
|
|
67
|
+
|
|
68
|
+
| Option | Type | Notes |
|
|
69
|
+
|--------|------|-------|
|
|
70
|
+
| `align` | `left`/`center`/`right` | figure/image alignment |
|
|
71
|
+
| `alt` | text | alt text (also the man/text placeholder) |
|
|
72
|
+
| `caption` | text | wraps the node in a `figure` with a caption (inline roles/xrefs ok) |
|
|
73
|
+
| `class` | class list | extra CSS/LaTeX classes |
|
|
74
|
+
| `width` | length or `%` | figure width (translated to a LaTeX length for PDF) |
|
|
75
|
+
| `name` | text | cross-reference target |
|
|
76
|
+
| `theme` | nonneg int | base light theme id |
|
|
77
|
+
| `dark_theme` | nonneg int | dark fallback theme id (overrides `d2_dark_theme`) |
|
|
78
|
+
| `layout` | text | `dagre`/`elk` (+ any configured extra engine) |
|
|
79
|
+
| `pad` | positive int | px padding |
|
|
80
|
+
| `sketch` | bool | hand-drawn look |
|
|
81
|
+
| `scale` | float | render scale (only applied when `> 0`) |
|
|
82
|
+
| `center` | bool | center in the viewbox |
|
|
83
|
+
| `format` | `svg`/`png`/`pdf`/`gif` | per-directive override of the resolved format |
|
|
84
|
+
| `target` | text | board selector (`--target`), e.g. `layers.x.*` |
|
|
85
|
+
| `animate_interval` | positive int | multi-board animated SVG/GIF interval (ms) |
|
|
86
|
+
|
|
87
|
+
Boolean options follow the common convention: a bare flag or
|
|
88
|
+
`1`/`true`/`yes`/`on` ⇒ `True`; `0`/`false`/`no`/`off` ⇒ `False`.
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
Every directive option has a `d2_<option>` config default; the directive value
|
|
93
|
+
wins over the config default.
|
|
94
|
+
|
|
95
|
+
| Config value | Default | Notes |
|
|
96
|
+
|--------------|---------|-------|
|
|
97
|
+
| `d2_binary` | `"d2"` | binary path (argv[0]; invoked with `shell=False`) |
|
|
98
|
+
| `d2_theme` | `0` | base light theme id |
|
|
99
|
+
| `d2_dark_theme` | `200` | dark fallback theme id |
|
|
100
|
+
| `d2_layout` | `"dagre"` | default layout engine |
|
|
101
|
+
| `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`) |
|
|
102
|
+
| `d2_pad` | `100` | px padding |
|
|
103
|
+
| `d2_sketch` | `False` | hand-drawn look |
|
|
104
|
+
| `d2_scale` | `-1` | `--scale` appended only when `> 0` |
|
|
105
|
+
| `d2_center` | `False` | center in the viewbox |
|
|
106
|
+
| `d2_class` | `""` | default extra class |
|
|
107
|
+
| `d2_redirect_links_to_blank_page` | `True` | SVG `<a>` `target="_blank"` for non-external links |
|
|
108
|
+
| `d2_animate_interval` | `0` | multi-board animated SVG/GIF interval (ms) |
|
|
109
|
+
| `d2_timeout` | `120` | `--timeout=<n>` and the subprocess timeout |
|
|
110
|
+
| `d2_fonts` | `{}` | `{role: ttf-path}` (`regular`/`italic`/`bold`/`semibold`/`mono`/…) |
|
|
111
|
+
| `d2_multiboard_svg` | `"animate"` | SVG multi-board policy: `animate`/`root`/`error` |
|
|
112
|
+
| `d2_builder_formats` | `{}` | per-builder format overrides (merged over the defaults) |
|
|
113
|
+
| `d2_default_format` | `"svg"` | fallback for builders not in the map |
|
|
114
|
+
| `d2_use_theme_core_palette` | `True` | build custom theme-overrides from the core palette |
|
|
115
|
+
| `d2_match_page_background` | `True` | match the d2 canvas (`N7`) to the resolved page color |
|
|
116
|
+
| `d2_dark_theme_overrides` | `{}` | user dict deep-merged over the generated block |
|
|
117
|
+
| `d2_dark_html` | `False` | opt-in to apply dark theme-overrides in HTML builds |
|
|
118
|
+
| `d2_allowed_link_schemes` | `None` | `None` = all schemes except denylist `{javascript, data, vbscript}` |
|
|
119
|
+
| `d2_latex_max_width` | `\linewidth` | default width cap for the LaTeX visitor |
|
|
120
|
+
| `d2_strict` | `False` | when `True`, a d2 failure raises instead of warn+skip |
|
|
121
|
+
|
|
122
|
+
## Per-builder output formats
|
|
123
|
+
|
|
124
|
+
Each builder gets a sensible default; override any of them via
|
|
125
|
+
`d2_builder_formats`.
|
|
126
|
+
|
|
127
|
+
| Builder | Default format |
|
|
128
|
+
|---------|----------------|
|
|
129
|
+
| `html` / `dirhtml` / `singlehtml` | `svg` |
|
|
130
|
+
| `epub` | `svg` |
|
|
131
|
+
| `latex` | `pdf` |
|
|
132
|
+
| `confluence` | `png` (experimental) |
|
|
133
|
+
| (any other) | `d2_default_format` (`svg`) |
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
d2_builder_formats = {"latex": "png", "epub": "png"}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
HTML-family builders can only embed `svg`/`png`. A per-directive `:format:`
|
|
140
|
+
that the active builder cannot embed (e.g. `pdf`/`gif` on an HTML builder) is
|
|
141
|
+
**validated once the builder is known**: the extension logs a build warning and
|
|
142
|
+
falls back to a safe embeddable format (SVG for HTML, PDF for LaTeX) rather than
|
|
143
|
+
emitting a broken diagram. Under `d2_strict = True` the mismatch raises a
|
|
144
|
+
`D2Error` instead.
|
|
145
|
+
|
|
146
|
+
## Dark mode & theme-core integration
|
|
147
|
+
|
|
148
|
+
d2's `--dark-theme` only affects a browser in dark mode; a static PDF needs an
|
|
149
|
+
explicit theme. So for dark PDF/SVG this extension computes a custom d2
|
|
150
|
+
`theme-overrides` block from the theme-core's resolved dark palette and injects
|
|
151
|
+
it on top of the base `--theme=<d2_dark_theme>` (default `200`). When the core
|
|
152
|
+
is absent or dark mode is inactive it falls back to that base dark theme id.
|
|
153
|
+
|
|
154
|
+
Integration is **soft**: the core import is guarded on `ImportError` only, so a
|
|
155
|
+
version-skew (core present but an expected API missing) surfaces as a warning
|
|
156
|
+
rather than silently degrading to "core absent". At startup the extension logs
|
|
157
|
+
whether palette integration is active.
|
|
158
|
+
|
|
159
|
+
You can also ship a hand-authored `arch_dark.d2` next to `arch.d2`; in a dark
|
|
160
|
+
LaTeX build the core swaps the directive argument to the `_dark` file. When a
|
|
161
|
+
swap occurs the auto-generated `theme-overrides` are skipped so the hand-authored
|
|
162
|
+
file speaks for itself.
|
|
163
|
+
|
|
164
|
+
> **theme-core version floor.** The four integration APIs (`get_dark_mode_context`,
|
|
165
|
+
> `is_dark_mode_active`, `register_dark_swap_directive`, `mark_image_dark_ready`)
|
|
166
|
+
> are exported since **1.1.9**, so the optional `theme` extra pins
|
|
167
|
+
> `doxtr-pdf-theme-core>=1.1.9`. (The original plan tentatively named `>=1.1.10`;
|
|
168
|
+
> 1.1.9 is the earliest release verified to export all four.)
|
|
169
|
+
|
|
170
|
+
## Links in diagrams
|
|
171
|
+
|
|
172
|
+
d2 `link:` fields may carry Sphinx cross-references and arbitrary URLs. Three
|
|
173
|
+
kinds are supported:
|
|
174
|
+
|
|
175
|
+
- **Internal xrefs** — `link: :ref:\`target\`` (and `:doc:` / other domain
|
|
176
|
+
roles). Resolved to a builder-correct relative URI in HTML. On the PDF path
|
|
177
|
+
internal xrefs are **dropped** (the label is kept, but a relative-file URI is
|
|
178
|
+
meaningless inside a single PDF).
|
|
179
|
+
- **`sphinxcontrib-xlink`** — `link: :xlink:\`link-id\`` resolves to an external
|
|
180
|
+
URL via the `xlink` config map. Degrades gracefully (warn + drop) when xlink
|
|
181
|
+
is not installed.
|
|
182
|
+
- **Raw / custom-scheme URLs** — any `link: <uri>` passes through
|
|
183
|
+
scheme-agnostically: `https:`, `mailto:`, `obsidian://`,
|
|
184
|
+
`x-devonthink-item://…`, etc. External/custom-scheme links become clickable
|
|
185
|
+
`/URI` annotations in the PDF too.
|
|
186
|
+
|
|
187
|
+
**Security:** `javascript:` / `data:` / `vbscript:` URIs are an XSS vector in SVG
|
|
188
|
+
output and are **stripped with a warning by default**. Configure
|
|
189
|
+
`d2_allowed_link_schemes` to change the policy (e.g. `["https", "mailto"]` to
|
|
190
|
+
restrict to an allowlist, or include `"data"` to opt back in).
|
|
191
|
+
|
|
192
|
+
> **Own-line constraint.** A `link:` carrying a role must be on its **own line**:
|
|
193
|
+
>
|
|
194
|
+
> ```d2
|
|
195
|
+
> node: {
|
|
196
|
+
> link: :ref:`my-target`
|
|
197
|
+
> }
|
|
198
|
+
> ```
|
|
199
|
+
>
|
|
200
|
+
> `x: { link: :ref:\`t\` }` on a single line is **not** rewritten.
|
|
201
|
+
|
|
202
|
+
By default SVG links get `target="_blank"` (configurable via
|
|
203
|
+
`d2_redirect_links_to_blank_page`).
|
|
204
|
+
|
|
205
|
+
## Multi-board compositions
|
|
206
|
+
|
|
207
|
+
d2 compositions (`layers` / `scenarios` / `steps`) declare multiple boards. The
|
|
208
|
+
output must always be a **single file**, never a directory, so the extension
|
|
209
|
+
picks a single-file representation per format:
|
|
210
|
+
|
|
211
|
+
- **SVG** — a multi-board SVG written to `foo.svg` becomes a *directory* in d2.
|
|
212
|
+
To avoid that, `d2_multiboard_svg` selects the policy:
|
|
213
|
+
- `"animate"` (default) — render an animated SVG (`--animate-interval`).
|
|
214
|
+
- `"root"` — render the root board only.
|
|
215
|
+
- `"error"` — warn and fall back to the root board.
|
|
216
|
+
A `:target:` always selects a specific board and takes precedence.
|
|
217
|
+
- **PDF** — a multi-board diagram renders to a multipage PDF (one board per
|
|
218
|
+
page). `\includegraphics` embeds **page 1 only**, so set `:target:` to choose
|
|
219
|
+
a specific board; without it the extension warns and shows the first board.
|
|
220
|
+
- **GIF** — an animated multi-board GIF is the natural single-file form.
|
|
221
|
+
|
|
222
|
+
## LaTeX / PDF notes
|
|
223
|
+
|
|
224
|
+
- PDF output requires **LuaLaTeX**.
|
|
225
|
+
- A raw d2 PDF (~203 mm) overflows the text block, so the LaTeX visitor always
|
|
226
|
+
emits `width=\linewidth,height=0.9\textheight,keepaspectratio` (width from
|
|
227
|
+
`d2_latex_max_width`; a `:width:` value, including `%`, is translated to a
|
|
228
|
+
LaTeX length).
|
|
229
|
+
- d2 PDFs embed real clickable `/URI` link annotations, so external/custom-scheme
|
|
230
|
+
links survive into the PDF. Internal `:ref:` cross-references are an HTML-only
|
|
231
|
+
feature (a relative-file URI is meaningless inside a single PDF).
|
|
232
|
+
|
|
233
|
+
## Layout engines
|
|
234
|
+
|
|
235
|
+
d2 ships two free layout engines, `dagre` (default) and `elk`. A licensed engine
|
|
236
|
+
such as `tala` must be **whitelisted** by name in `d2_extra_layouts` so a typo'd
|
|
237
|
+
engine is caught rather than silently handed to d2:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
d2_extra_layouts = ["tala"] # then :layout: tala is accepted
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
A resolved layout engine outside `{dagre, elk} ∪ d2_extra_layouts` is rejected:
|
|
244
|
+
the extension logs a build warning and falls back to d2's default engine (or
|
|
245
|
+
raises a `D2Error` under `d2_strict`).
|
|
246
|
+
|
|
247
|
+
## Replacing the renderer
|
|
248
|
+
|
|
249
|
+
Everything that shells out to `d2` lives behind a single indirection,
|
|
250
|
+
`doxtr_d2.render.render_backend` (which defaults to
|
|
251
|
+
`doxtr_d2.render.render_d2`). The builder visitors never call `render_d2`
|
|
252
|
+
directly — they call `render.render_backend(...)`. A child theme can therefore
|
|
253
|
+
replace **all** rendering by reassigning that one value, instead of overriding
|
|
254
|
+
every per-builder visitor:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
import doxtr_d2.render as render
|
|
258
|
+
|
|
259
|
+
def my_render_backend(stdin_bytes, fmt, resolved_options, image_dir, **kwargs):
|
|
260
|
+
# e.g. call a d2 HTTP service or a different diagram tool.
|
|
261
|
+
# Contract: write the artifact under image_dir and return its filename
|
|
262
|
+
# RELATIVE to image_dir; raise doxtr_d2.errors.D2Error on failure.
|
|
263
|
+
...
|
|
264
|
+
return "d2-<hash>.%s" % fmt
|
|
265
|
+
|
|
266
|
+
render.render_backend = my_render_backend
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Keep the signature and return/raise contract identical to `render_d2`
|
|
270
|
+
(`(stdin_bytes, fmt, resolved_options, image_dir, *, binary, fonts, timeout,
|
|
271
|
+
cwd, post_process) -> out_name`) and the visitors, caching key, and atomic-write
|
|
272
|
+
guarantees all continue to work unchanged.
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT — see [LICENSE](LICENSE).
|