doxtr-roadmap 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.
Files changed (29) hide show
  1. doxtr_roadmap-0.1.0/LICENSE +21 -0
  2. doxtr_roadmap-0.1.0/PKG-INFO +653 -0
  3. doxtr_roadmap-0.1.0/README.md +631 -0
  4. doxtr_roadmap-0.1.0/doxtr_roadmap/__init__.py +269 -0
  5. doxtr_roadmap-0.1.0/doxtr_roadmap/config_defaults.py +144 -0
  6. doxtr_roadmap-0.1.0/doxtr_roadmap/csv_parser.py +301 -0
  7. doxtr_roadmap-0.1.0/doxtr_roadmap/directive.py +989 -0
  8. doxtr_roadmap-0.1.0/doxtr_roadmap/generator.py +868 -0
  9. doxtr_roadmap-0.1.0/doxtr_roadmap/link_resolver.py +239 -0
  10. doxtr_roadmap-0.1.0/doxtr_roadmap/plantuml_version.py +95 -0
  11. doxtr_roadmap-0.1.0/doxtr_roadmap/safe_query.py +345 -0
  12. doxtr_roadmap-0.1.0/doxtr_roadmap/tags.py +424 -0
  13. doxtr_roadmap-0.1.0/doxtr_roadmap/theme_adapter.py +197 -0
  14. doxtr_roadmap-0.1.0/doxtr_roadmap.egg-info/PKG-INFO +653 -0
  15. doxtr_roadmap-0.1.0/doxtr_roadmap.egg-info/SOURCES.txt +27 -0
  16. doxtr_roadmap-0.1.0/doxtr_roadmap.egg-info/dependency_links.txt +1 -0
  17. doxtr_roadmap-0.1.0/doxtr_roadmap.egg-info/requires.txt +8 -0
  18. doxtr_roadmap-0.1.0/doxtr_roadmap.egg-info/top_level.txt +1 -0
  19. doxtr_roadmap-0.1.0/pyproject.toml +57 -0
  20. doxtr_roadmap-0.1.0/setup.cfg +4 -0
  21. doxtr_roadmap-0.1.0/tests/test_collision.py +369 -0
  22. doxtr_roadmap-0.1.0/tests/test_csv_parser.py +365 -0
  23. doxtr_roadmap-0.1.0/tests/test_directive.py +2116 -0
  24. doxtr_roadmap-0.1.0/tests/test_generator.py +823 -0
  25. doxtr_roadmap-0.1.0/tests/test_link_resolver.py +243 -0
  26. doxtr_roadmap-0.1.0/tests/test_plantuml_version.py +356 -0
  27. doxtr_roadmap-0.1.0/tests/test_safe_query.py +349 -0
  28. doxtr_roadmap-0.1.0/tests/test_tags.py +296 -0
  29. doxtr_roadmap-0.1.0/tests/test_theme_adapter.py +297 -0
@@ -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.
@@ -0,0 +1,653 @@
1
+ Metadata-Version: 2.4
2
+ Name: doxtr-roadmap
3
+ Version: 0.1.0
4
+ Summary: Sphinx extension that generates PlantUML Gantt roadmaps from CSV data.
5
+ Author: Jens Frey
6
+ Project-URL: Homepage, https://github.com/doxtr/doxtr-roadmap
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
+ Requires-Dist: sphinxcontrib-plantuml>=0.9
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7.0; extra == "dev"
18
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
19
+ Requires-Dist: sphinxcontrib-xlink; extra == "dev"
20
+ Requires-Dist: doxtr-pdf-theme-core; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # doxtr-roadmap
24
+
25
+ A Sphinx extension that generates PlantUML Gantt roadmaps from CSV data.
26
+
27
+ ## Overview
28
+
29
+ `doxtr-roadmap` provides a `.. roadmap::` directive that reads roadmap items
30
+ from a CSV file (or inline CSV body), applies optional zoom/filter/tag
31
+ constraints, and emits a PlantUML Gantt chart node rendered via
32
+ `sphinxcontrib.plantuml`.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install doxtr-roadmap
38
+ ```
39
+
40
+ Requires `sphinxcontrib-plantuml>=0.9` (listed as a dependency).
41
+
42
+ ## Quick Start
43
+
44
+ Add both extensions to your `conf.py`:
45
+
46
+ ```python
47
+ extensions = [
48
+ "sphinxcontrib.plantuml",
49
+ "doxtr_roadmap",
50
+ ]
51
+ ```
52
+
53
+ Then use the directive in any RST document:
54
+
55
+ ```rst
56
+ .. roadmap::
57
+ :file: path/to/roadmap.csv
58
+ :title: My Project Roadmap
59
+ ```
60
+
61
+ Or with inline CSV content:
62
+
63
+ ```rst
64
+ .. roadmap::
65
+ :title: Sprint Overview
66
+
67
+ section,name,start,end,row_group,link,tags
68
+ Q1,Feature A,2026-01-01,2026-03-31,,,eng
69
+ Q1,Feature B,2026-02-01,2026-04-30,,,code
70
+ ```
71
+
72
+ ## Multiple files and globs
73
+
74
+ The `:file:` option accepts one or more CSV paths and/or glob patterns,
75
+ space- or comma-separated. All matched files are treated as **one combined
76
+ roadmap**: sections with the same name merge across files and subtasks /
77
+ `row_group` references work even when the parent task is defined in a
78
+ different file.
79
+
80
+ ```rst
81
+ .. roadmap::
82
+ :file: sprints/sprint-01.csv sprints/sprint-02.csv sprints/sprint-03.csv
83
+ :title: All Sprints
84
+
85
+ .. roadmap::
86
+ :file: sprints/sprint-*.csv # equivalent glob
87
+ :title: All Sprints
88
+ ```
89
+
90
+ Both examples produce the **same** combined roadmap. Key rules:
91
+
92
+ - Paths are resolved relative to the document directory first, then `srcdir`.
93
+ - Glob matches within a single pattern are sorted lexicographically
94
+ (`sprint-01`, `sprint-02`, …) so the combination order is predictable.
95
+ - A file matched by more than one spec is read only once (de-duplicated,
96
+ first-seen order preserved).
97
+ - If a spec matches no files the directive reports an error.
98
+ - `env.note_dependency` is called for each resolved file so editing any of
99
+ them triggers an incremental rebuild. *Adding a brand-new file* matching
100
+ an existing glob is not detected automatically — run `make clean html`
101
+ after adding files to a glob pattern.
102
+
103
+
104
+
105
+ The CSV must have a header row. Only `name`, `start`, and `end` are
106
+ required; every other column is optional and may be omitted entirely:
107
+
108
+ | Column | Required | Description |
109
+ |-------------|----------|-------------|
110
+ | `section` | no | Section/group name, or a parent task name for subtasks. Blank or omitted → the task renders with no section header |
111
+ | `name` | yes | Task display name |
112
+ | `start` | yes | Start date (ISO `YYYY-MM-DD`) |
113
+ | `end` | yes | End date (ISO `YYYY-MM-DD`). Same as `start` → milestone |
114
+ | `row_group` | no | Tasks sharing this non-empty value render on one Gantt row |
115
+ | `link` | no | Plain URL or `:xlink:\`id\`` role expression |
116
+ | `tags` | no | Comma-separated tag list |
117
+
118
+ ### Subtasks
119
+
120
+ A row whose `section` column matches an existing top-level task name becomes a
121
+ subtask, inserted directly beneath the parent task in the same section.
122
+
123
+ ### Milestones
124
+
125
+ When `start == end`, the row is rendered as a Gantt milestone (diamond marker).
126
+ Milestones are never clamped to the clip window.
127
+
128
+ ## Directive Options
129
+
130
+ | Option | Type | Description |
131
+ |------------------|-------------------|-------------|
132
+ | `:file:` | path(s)/glob(s) | One or more CSV paths and/or glob patterns, space- or comma-separated. All matched files are combined into a single roadmap (sections merge by name; subtasks and row-groups work across files). Glob metacharacters (`*`, `?`, `[…]`) are expanded; within each glob matches are sorted lexicographically. A single path with no separators behaves as before. |
133
+ | `:title:` | string | Override diagram title |
134
+ | `:scale:` | choice | `daily`, `weekly`, `monthly` |
135
+ | `:start:` | ISO date | Clip window start |
136
+ | `:end:` | ISO date | Clip window end |
137
+ | `:period:` | string | Comma-separated period name(s) to zoom to |
138
+ | `:close-weekends:` | flag | Force weekend closure |
139
+ | `:clean-style:` | true/false | Override `clean_style` for this directive; bare flag or `true` enables, `false` disables (see [clean_style](#clean_style-and-the-hide-column-directives)) |
140
+ | `:tags:` | string | Tag filter (nested `[ ]` / `!` / `!!` syntax) |
141
+ | `:query:` | Python expression | Safe-eval row filter; available names: `name`, `start`, `end`, `section`, `tags`, `row_group`, `match(pattern, string)`; safe builtins: `any`, `all`, `bool`, `set`, `len` |
142
+ | `:collision-detection:` | true/false | Override collision detection for this chart; bare flag or `true` enables, `false` disables (see [Collision detection](#collision-detection)) |
143
+ | `:collision-char-width-factor:` | float | Per-chart label-width tuning knob (overrides `doxtr_roadmap_collision_char_width_factor`) |
144
+ | `:column-zoom:` | float | Per-chart gantt column width multiplier; overrides `doxtr_roadmap_column_zoom` (see [Column width (zoom)](#column-width-zoom)) |
145
+ | `:width:` | length/% | Force the rendered image to fill a specific width in HTML and PDF (e.g. `100%`); not set by default |
146
+ | `:link-appendix:` | mode | Render task links as a real list below the chart. Values: `list` (bullet list), `footnote` (real RST auto-numbered footnotes — LaTeX `\footnote` in PDF), `off` (disable). Bare flag → `list`. Overrides the global `doxtr_roadmap_link_appendix` for this chart. |
147
+ | `:link-appendix-title:` | string | Heading text placed above the link appendix. Overrides `doxtr_roadmap_link_appendix_title` for this chart. |
148
+ | `:caption:` | string | Caption text; presence wraps the chart in a `nodes.figure` so it appears in the List of Figures and can be cross-referenced. |
149
+ | `:align:` | `left`\|`center`\|`right` | Horizontal alignment of the figure; also triggers figure wrapping even without `:caption:`. |
150
+ | `:name:` | string | Cross-reference target for the figure (e.g. `:numref:\`my-fig\`` or `:ref:\`my-fig\``). Requires `:caption:` or `:align:` to produce a figure node. |
151
+
152
+ ### Period zoom
153
+
154
+ A "period" is any named task in the CSV (typically a sprint or increment row).
155
+ Specifying `:period: Set27-01` clips the diagram to exactly that task's
156
+ date range. Multiple periods are comma-separated:
157
+
158
+ ```rst
159
+ .. roadmap::
160
+ :period: Set27-01, Set27-04
161
+ ```
162
+
163
+ When exactly one distinct period is rendered and `:scale:` is not set, the
164
+ extension automatically switches to `daily` scale and closes weekends.
165
+
166
+ ### Tag filter
167
+
168
+ Uses the same nested-bracket syntax as `sphinxcontrib.xlink`:
169
+
170
+ ```rst
171
+ .. roadmap::
172
+ :tags: eng [ backend !! ], public
173
+ ```
174
+
175
+ Rows are included if at least one of their tags satisfies the filter tree.
176
+ `!tag` hides a tag; `!!` cascades to children.
177
+
178
+ ### Query filter
179
+
180
+ A Python expression evaluated per row with safe builtins:
181
+
182
+ ```rst
183
+ .. roadmap::
184
+ :query: "eng" in tags and end > start
185
+ ```
186
+
187
+ Available variables: `name`, `start` (date), `end` (date), `section`, `tags`
188
+ (set), `row_group`, `match(pattern, string)` (safe regex helper — returns
189
+ `bool(re.search(pattern, string))` without exposing the `re` module).
190
+
191
+ Safe builtins: `any`, `all`, `bool`, `set`, `len`.
192
+
193
+ ## Configuration
194
+
195
+ All config values are prefixed `doxtr_roadmap_` and set in `conf.py`:
196
+
197
+ ```python
198
+ doxtr_roadmap_default_scale = "monthly" # daily | weekly | monthly
199
+ doxtr_roadmap_scale_factor = 1.25 # PlantUML diagram scale
200
+ doxtr_roadmap_default_start = None # ISO date or None (→ today)
201
+ doxtr_roadmap_default_title = "Roadmap"
202
+ doxtr_roadmap_clean_style = True # hide start/end/duration columns — default True; requires PlantUML V1.2026.7+
203
+ doxtr_roadmap_close_weekends_on_single_period = True
204
+
205
+ doxtr_roadmap_bar = {
206
+ "done_color": "#FF8C00",
207
+ "undone_color": "#FFF3E0",
208
+ "frame_color": None,
209
+ }
210
+
211
+ doxtr_roadmap_sections = {
212
+ "My Section": {
213
+ "done": "#1976D2",
214
+ "frame": None,
215
+ "frame_overrides": {"Special Task": "#E53935"},
216
+ },
217
+ }
218
+
219
+ doxtr_roadmap_today = {"color": "#E53935"}
220
+
221
+ doxtr_roadmap_fonts = {
222
+ "title": {"name": None, "size": 24, "style": "bold", "color": None},
223
+ "task": {"name": None, "size": 14, "style": None, "color": None},
224
+ "separator": {"name": None, "size": 16, "style": "bold", "color": None},
225
+ }
226
+
227
+ doxtr_roadmap_closed = {"background_color": None}
228
+
229
+ # PlantUML version check (default: fail build if PlantUML < v1.2026.7)
230
+ # Values: "error" (default) | "warn" | "off" / False
231
+ # See [PlantUML Version](#plantuml-version) section.
232
+ doxtr_roadmap_require_plantuml_version = "error"
233
+
234
+ # Collision detection (default: True — split same-row-group tasks that collide)
235
+ doxtr_roadmap_collision_detection = True
236
+ doxtr_roadmap_collision_char_width_factor = 1.0 # label-width tuning knob
237
+ doxtr_roadmap_collision_gap_days = 2 # minimum gap between tasks on a lane
238
+
239
+ # Column zoom — widen each gantt time column (default 1 = unchanged)
240
+ doxtr_roadmap_column_zoom = 1
241
+
242
+ # Link appendix — render task links as a list below the chart.
243
+ # Defaults to "list" but only for PDF/latex (see builders below); set to
244
+ # False to disable it everywhere including PDF.
245
+ doxtr_roadmap_link_appendix = "list" # "list" | "footnote" | False
246
+ doxtr_roadmap_link_appendix_builders = ["latex"] # default: PDF only
247
+ doxtr_roadmap_link_appendix_title = "Links"
248
+
249
+ # Figure / List-of-Figures (default off)
250
+ # Set True to wrap every roadmap as a numbered figure automatically.
251
+ doxtr_roadmap_figure = False
252
+ # Optional default caption when doxtr_roadmap_figure=True and no :caption: given.
253
+ # None → use the chart title as the caption.
254
+ doxtr_roadmap_figure_caption = None
255
+
256
+ # Tag allow-lists (optional)
257
+ doxtr_roadmap_allowed_tags = {
258
+ "eng": "Engineering",
259
+ "code": "Code",
260
+ "security": "Security",
261
+ }
262
+ doxtr_roadmap_allowed_tag_patterns = {}
263
+ ```
264
+
265
+ ### Theme-core integration
266
+
267
+ When `doxtr_pdf_theme_core` is in `extensions`, the roadmap extension
268
+ automatically reads its semantic palette and typography:
269
+
270
+ ```python
271
+ doxtr_roadmap_use_theme_core = "auto" # "auto" | True | False
272
+ ```
273
+
274
+ | Palette key | Maps to |
275
+ |--------------|----------------------|
276
+ | `primary` | bar `done_color` |
277
+ | `secondary` | today line `color` |
278
+ | `main_font` | task font name |
279
+ | `sans_font` | separator font name |
280
+
281
+ User-configured `doxtr_roadmap_*` values always win over theme-core.
282
+
283
+ ### xlink integration
284
+
285
+ When `sphinxcontrib.xlink` is in `extensions`, the `link` CSV column accepts
286
+ `:xlink:\`id\`` role expressions that are resolved against the project's
287
+ `.xlink` files. The resolved URL and title are emitted as a PlantUML hyperlink:
288
+
289
+ ```
290
+ [Task] links to [[https://... Resolved Title]]
291
+ ```
292
+
293
+ If xlink is not loaded, xlink-style cells produce a one-time warning and the
294
+ link is silently skipped.
295
+
296
+ ## Figures and the List of Figures
297
+
298
+ To render a roadmap as a **numbered figure** that appears in the List of
299
+ Figures (`\listoffigures` in PDF, or via Sphinx's `numfig` feature in HTML),
300
+ add a `:caption:` to the directive:
301
+
302
+ ```rst
303
+ .. roadmap::
304
+ :caption: Q1 Sprint Roadmap
305
+ :name: fig-q1-sprint
306
+ :align: center
307
+ :start: 2026-01-01
308
+
309
+ section,name,start,end,row_group,link,tags
310
+ Work,Task A,2026-01-01,2026-03-31,,,
311
+ ```
312
+
313
+ With `numfig = True` in `conf.py`, Sphinx numbers the figure automatically
314
+ ("Fig. 1", "Fig. 2", …) and you can cross-reference it:
315
+
316
+ ```rst
317
+ See :numref:`fig-q1-sprint` for the full timeline.
318
+ ```
319
+
320
+ **Caption default.** When a roadmap is rendered as a figure, the caption text
321
+ follows this precedence:
322
+
323
+ 1. Explicit `:caption:` option — highest priority (used verbatim).
324
+ 2. `doxtr_roadmap_figure_caption` if non-empty — global override.
325
+ 3. The chart `:title:` (or `doxtr_roadmap_default_title`) — default.
326
+
327
+ This means that an `:align:`-only wrap (no explicit `:caption:`) now also
328
+ gets the chart title as its caption, making the roadmap appear in the List
329
+ of Figures without any extra configuration.
330
+
331
+ ### Options
332
+
333
+ | Option | Description |
334
+ |--------|-------------|
335
+ | `:caption:` | Caption text; presence wraps the chart in a `nodes.figure` so it appears in the List of Figures. |
336
+ | `:align:` | `left`, `center`, or `right`; also triggers figure wrapping with the chart title as the default caption. |
337
+ | `:name:` | Cross-reference target (`:numref:` / `:ref:`). |
338
+
339
+ ### Making all roadmaps figures automatically
340
+
341
+ Set `doxtr_roadmap_figure = True` in `conf.py` to wrap **every** roadmap as a
342
+ figure without adding `:caption:` to each directive. The chart's `:title:`
343
+ becomes the caption by default:
344
+
345
+ ```python
346
+ # conf.py
347
+ doxtr_roadmap_figure = True
348
+ # Optional: override the default caption text for all auto-wrapped figures.
349
+ # None (default) → use each chart's :title: as the caption.
350
+ doxtr_roadmap_figure_caption = None
351
+ ```
352
+
353
+ When `doxtr_roadmap_figure_caption` is set to a non-empty string, that string
354
+ is used verbatim as the caption for every roadmap that does not carry its own
355
+ per-directive `:caption:`.
356
+
357
+ ### numfig and the List of Figures
358
+
359
+ - Enable `numfig = True` in `conf.py` for "Figure N" numbering and
360
+ `:numref:` cross-references.
361
+ - The HTML builder does not generate a standalone List of Figures page by
362
+ default; use a `.. contents::` directive or an additional index page.
363
+ - The **LaTeX/PDF builder** emits `\begin{figure}...\caption{...}\end{figure}`
364
+ for every captioned roadmap, which feeds into `\listoffigures` automatically
365
+ when your LaTeX preamble includes it.
366
+ - If `doxtr_pdf_theme_core` is in use, its `doxtr_globals` `show_list_of_figures`
367
+ key controls whether `\listoffigures` is emitted in the generated PDF.
368
+
369
+ ## Link appendix
370
+
371
+ Links embedded inside the PlantUML image are **not clickable in PDF/LaTeX
372
+ output** — the image is a static graphic with no active hyperlink areas. The
373
+ link appendix feature solves this by rendering task links as real, clickable
374
+ docutils nodes placed *after* the chart image.
375
+
376
+ By default the appendix is **on for PDF/LaTeX output** (rendered as a bullet
377
+ list) and **off for every other builder** (HTML, epub, etc.), whose image
378
+ links are already clickable. It is controlled by three config values:
379
+
380
+ ```python
381
+ # conf.py
382
+
383
+ # Appendix mode. Defaults to "list"; the builder restriction below limits it
384
+ # to PDF by default. Set to False to disable it everywhere (including PDF).
385
+ doxtr_roadmap_link_appendix = "list" # "list" | "footnote" | False
386
+
387
+ # Which builders render the appendix. Checked against builder.name AND
388
+ # builder.format. Default ["latex"] → PDF only. Use "all" (or ["*"]) for
389
+ # every builder, or e.g. ["latex", "epub"] to add more.
390
+ doxtr_roadmap_link_appendix_builders = ["latex"] # default: PDF only
391
+
392
+ # Heading shown above the list (empty string → no heading)
393
+ doxtr_roadmap_link_appendix_title = "Links" # default
394
+ ```
395
+
396
+ To turn the appendix off for PDF as well, set
397
+ ``doxtr_roadmap_link_appendix = False`` (or ``:link-appendix: off`` on an
398
+ individual chart). To also show it in HTML/epub, add those builders to
399
+ ``doxtr_roadmap_link_appendix_builders`` (or use ``"all"``).
400
+
401
+ **Mode `"list"`** renders a `nodes.bullet_list`; each item is
402
+ `<Task Name>: <clickable link>`. **Mode `"footnote"`** renders real
403
+ reStructuredText auto-numbered footnotes. In PDF/LaTeX output Sphinx's LaTeX
404
+ writer emits these as page-bottom `\footnote{}` commands. In HTML they
405
+ render as standard numbered footnotes with back-references.
406
+
407
+ **Footnote mode with a figure caption.** When the roadmap is rendered as a
408
+ figure (via `:caption:`, `:align:`, or `doxtr_roadmap_figure = True`) *and*
409
+ `link_appendix = "footnote"` *and* the builder is in `link_appendix_builders`,
410
+ the footnote links are embedded **compactly inside the figure caption** instead
411
+ of a separate "Links" block below the chart. The caption shows a single
412
+ label word (the `link_appendix_title`, default `"Links"`) followed by one
413
+ auto-numbered footnote marker per link:
414
+
415
+ ```
416
+ <caption text> (Links [1], [2])
417
+ ```
418
+
419
+ For example, with `numfig = True`: `Fig. 13.19: Projects with Links (Links⁹,¹⁰)`.
420
+
421
+ Each footnote body carries the task name, the link title (when available),
422
+ and the URL:
423
+
424
+ ```
425
+ [1] API Redesign: https://example.com/api ← plain URL, no title
426
+ [2] Sphinx Migration >> Sphinx Docs: https://... ← xlink with resolved title
427
+ ```
428
+
429
+ The footnote definitions are emitted as sibling nodes after the figure. The
430
+ separate "Links" heading/container is **not** emitted in this case. If there
431
+ are no resolvable links the caption is the plain title with no brackets.
432
+
433
+ `"list"` mode always produces the standalone bullet list regardless of whether
434
+ a figure is present — list links are not compact enough for a caption.
435
+
436
+ The same source produces an image-only HTML page and an image-plus-appendix
437
+ PDF page — no per-format conditionals needed.
438
+
439
+ Per-directive overrides (`:link-appendix:` and `:link-appendix-title:`) let
440
+ you enable, change, or disable the appendix for a single chart:
441
+
442
+ ```rst
443
+ .. roadmap::
444
+ :file: roadmap.csv
445
+ :link-appendix: footnote
446
+ :link-appendix-title: External References
447
+
448
+ .. roadmap::
449
+ :file: roadmap.csv
450
+ :link-appendix: off ← disable even when globally enabled
451
+ ```
452
+
453
+ A bare `:link-appendix:` flag (no value) defaults to `"list"`.
454
+
455
+ Tasks with no `link` cell, or whose link cell does not resolve, are silently
456
+ skipped. If no tasks have resolvable links, no container or heading is
457
+ appended.
458
+
459
+ ## PlantUML Version
460
+
461
+ ### Minimum version requirement — v1.2026.7
462
+
463
+ The extension requires **PlantUML v1.2026.7 or newer** for full feature
464
+ support (see [release notes](https://github.com/plantuml/plantuml/releases/tag/v1.2026.7)).
465
+ At startup (`builder-inited`) the extension runs `plantuml -version` and
466
+ compares the result against the minimum tuple `(1, 2026, 7)`.
467
+
468
+ The check behaviour is controlled by the `doxtr_roadmap_require_plantuml_version`
469
+ config value:
470
+
471
+ | Value | Effect |
472
+ |-------|--------|
473
+ | `"error"` (default) | An `ExtensionError` is raised, aborting the build if the version is below the minimum. |
474
+ | `"warn"` | A `WARNING` is emitted if the installed version is below the minimum; the build continues normally. |
475
+ | `"off"` / `False` | The check is skipped entirely; no subprocess call is made. |
476
+
477
+ ```python
478
+ # conf.py
479
+ # Default — fail the build if PlantUML is below v1.2026.7:
480
+ doxtr_roadmap_require_plantuml_version = "error"
481
+
482
+ # Lenient — warn but never fail the build:
483
+ doxtr_roadmap_require_plantuml_version = "warn"
484
+
485
+ # Opt out — skip the check entirely:
486
+ doxtr_roadmap_require_plantuml_version = "off"
487
+ ```
488
+
489
+ If the PlantUML binary cannot be found or its version output is unrecognisable,
490
+ an informational message is logged and the build continues regardless of the
491
+ setting — the check never crashes a build over an undeterminable version.
492
+
493
+ `[Display] as [pid]` same-name-distinct-bar support requires **PlantUML
494
+ V1.2024.6 or newer**. Older versions will silently merge tasks that share a
495
+ display name (e.g. duplicate period rows).
496
+
497
+ ### `clean_style` and the `hide column` directives
498
+
499
+ `doxtr_roadmap_clean_style = True` hides the per-task **Start**, **End**, and
500
+ **Duration** columns in the rendered Gantt table. This is especially important
501
+ when `row_group` same-row grouping is used: without it the date stamps from
502
+ multiple tasks sharing a row overlap and become unreadable.
503
+
504
+ `clean_style` **defaults to `True`** because PlantUML v1.2026.7 is the
505
+ extension's minimum baseline and fully supports the `hide column
506
+ start/end/duration` syntax.
507
+
508
+ To disable it globally:
509
+
510
+ ```python
511
+ # conf.py
512
+ doxtr_roadmap_clean_style = False
513
+ ```
514
+
515
+ The `:clean-style:` directive option overrides the global setting per
516
+ directive instance:
517
+
518
+ ```rst
519
+ .. roadmap:: # bare flag or explicit true — enables clean_style
520
+ :clean-style:
521
+ :file: roadmap.csv
522
+
523
+ .. roadmap:: # explicit false — disables clean_style for this block
524
+ :clean-style: false
525
+ :file: roadmap.csv
526
+ ```
527
+
528
+
529
+ ## Collision detection
530
+
531
+ When two or more tasks share a `row_group`, PlantUML draws their text labels
532
+ starting at (or across) their bars. If the bars overlap in time, or if an
533
+ earlier bar's label is long enough to overrun rightward into a later bar, the
534
+ labels collide and become unreadable.
535
+
536
+ By default, the extension detects these collisions and automatically splits
537
+ tasks within a `row_group` onto as few additional rows as needed so that no
538
+ two tasks on the same row would have overlapping labels. Tasks that fit
539
+ together stay together (no unnecessary splits); only tasks that would collide
540
+ are moved to a new row. This preserves the space-saving intent of `row_group`
541
+ while keeping text readable.
542
+
543
+ ```python
544
+ # conf.py
545
+
546
+ # Default True — split same-row-group tasks onto extra rows when they collide.
547
+ doxtr_roadmap_collision_detection = True
548
+
549
+ # Set False to force ALL same-row-group tasks onto one row regardless of
550
+ # overlap (for users who deliberately want overlap / the old behaviour).
551
+ doxtr_roadmap_collision_detection = False
552
+
553
+ # Tuning knob: higher value reserves more space per character → splits sooner.
554
+ # Default 1.0 corresponds to the calibrated base days-per-character.
555
+ doxtr_roadmap_collision_char_width_factor = 1.0
556
+
557
+ # Minimum gap (calendar days) between adjacent tasks on the same lane.
558
+ doxtr_roadmap_collision_gap_days = 2
559
+ ```
560
+
561
+ The `:collision-detection:` directive option overrides the global setting for
562
+ a single chart:
563
+
564
+ ```rst
565
+ .. roadmap:: # detection on for this chart only
566
+ :collision-detection: true
567
+ :file: roadmap.csv
568
+
569
+ .. roadmap:: # detection off — force overlap for this chart
570
+ :collision-detection: false
571
+ :file: roadmap.csv
572
+
573
+ .. roadmap:: # widen label estimate for this chart
574
+ :collision-char-width-factor: 1.5
575
+ :file: roadmap.csv
576
+ ```
577
+
578
+ The detection algorithm estimates each label's horizontal footprint using a
579
+ per-scale days-per-character mapping (`daily` ≈ 1.1, `weekly` ≈ 4, `monthly`
580
+ ≈ 9), multiplied by `collision_char_width_factor`. Within each `row_group`
581
+ tasks are packed into lanes using a greedy first-fit strategy: each task is
582
+ placed on the first lane whose last task's footprint does not overlap; if all
583
+ lanes are blocked, a new lane is opened. Different lanes render as separate
584
+ Gantt rows.
585
+
586
+
587
+ ## Column width (zoom)
588
+
589
+ By default PlantUML sizes Gantt columns compactly. On wide pages the chart
590
+ may not use the full available width, resulting in a horizontally compressed
591
+ look — especially on monthly or weekly scale.
592
+
593
+ The `doxtr_roadmap_column_zoom` config value (default `1`) multiplies the
594
+ width of each time column by appending `zoom <factor>` to the PlantUML
595
+ `projectscale` line. A value of `1` (or absent) produces the current
596
+ unchanged behaviour. Example:
597
+
598
+ ```python
599
+ # conf.py — make each month column 3× wider
600
+ doxtr_roadmap_column_zoom = 3
601
+ ```
602
+
603
+ The `:column-zoom:` directive option overrides the global setting per chart:
604
+
605
+ ```rst
606
+ .. roadmap::
607
+ :column-zoom: 3
608
+ :file: roadmap.csv
609
+ ```
610
+
611
+ The `:width:` directive option (e.g. `:width: 100%`) forces the rendered
612
+ image to fill the available text/page width in HTML and PDF. This is handled
613
+ by `sphinxcontrib.plantuml` (maps to an HTML `style="width:100%"` attribute
614
+ or a LaTeX `adjustbox` width). By default no width is set (current
615
+ behaviour).
616
+
617
+ ```rst
618
+ .. roadmap::
619
+ :column-zoom: 3
620
+ :width: 100%
621
+ :file: roadmap.csv
622
+ ```
623
+
624
+ Combining both options gives full control: `:column-zoom:` widens each source
625
+ column so the chart is drawn at the desired detail level, and `:width: 100%`
626
+ stretches the resulting image to fill the page.
627
+
628
+ > **PlantUML version note:** The `zoom <factor>` keyword form requires
629
+ > PlantUML v1.2026.7 or newer — already the extension's minimum baseline.
630
+ > The older `projectscale monthly 3` form (no `zoom` keyword) errors on
631
+ > v1.2026.7 and is never emitted by this extension.
632
+
633
+
634
+ ## Development
635
+
636
+ ```bash
637
+ git clone https://github.com/doxtr/doxtr-roadmap
638
+ cd doxtr-roadmap
639
+ pip install -e ".[dev]"
640
+ pip install sphinxcontrib-plantuml
641
+ python -m pytest tests/ -v
642
+ ```
643
+
644
+ Run the Sphinx HTML smoke-build:
645
+
646
+ ```bash
647
+ cd test_harness
648
+ python test_runner.py
649
+ ```
650
+
651
+ ## License
652
+
653
+ MIT