Skip to content

Commit 3487e8b

Browse files
authored
feat(docs): Add linkable arguments with headerlinks to argparse directive (#1010)
## Summary - Add linkable argument definitions to the argparse directive output with `¶` headerlinks - Arguments get unique IDs based on their option names (e.g., `#arg--verbose`) - Headerlinks appear on hover or when targeted via URL fragment - CSS styling with monokai background, proper light/dark mode support - Consolidate and clean up CSS selectors for maintainability ## Features - **Linkable arguments**: Each argument in the argparse output gets a unique anchor ID - **Headerlinks**: Pilcrow (¶) links appear on hover, positioned outside the argument name - **URL fragment targeting**: Arguments highlight when navigated to via URL hash - **Light/dark mode**: Proper color overrides for both themes
2 parents 706bd66 + 4f22db0 commit 3487e8b

File tree

6 files changed

+555
-98
lines changed

6 files changed

+555
-98
lines changed

CHANGES

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force
3535
_Notes on the upcoming release will go here._
3636
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
3737

38+
### Documentation
39+
40+
#### Linkable CLI arguments and options (#1010)
41+
42+
CLI documentation now supports direct linking to specific arguments:
43+
44+
- **Linkable options**: Each `--option` and positional argument has a permanent URL anchor (e.g., `cli/load.html#-d`)
45+
- **Headerlinks**: Hover over any argument to reveal a ¶ link for easy sharing
46+
- **Visual styling**: Argument names displayed with syntax-highlighted backgrounds for better readability
47+
- **Default value badges**: Default values shown as styled inline code (e.g., `auto`)
48+
3849
## tmuxp 1.64.0 (2026-01-24)
3950

4051
### Documentation

docs/_ext/sphinx_argparse_neo/nodes.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,48 @@
2626
from sphinx_argparse_neo.utils import strip_ansi # noqa: E402
2727

2828

29+
def _generate_argument_id(names: list[str], id_prefix: str = "") -> str:
30+
"""Generate unique ID for an argument based on its names.
31+
32+
Creates a slug-style ID suitable for HTML anchors by:
33+
1. Stripping leading dashes from option names
34+
2. Joining multiple names with hyphens
35+
3. Prepending optional prefix for namespace isolation
36+
37+
Parameters
38+
----------
39+
names : list[str]
40+
List of argument names (e.g., ["-L", "--socket-name"]).
41+
id_prefix : str
42+
Optional prefix for uniqueness (e.g., "shell" -> "shell-L-socket-name").
43+
44+
Returns
45+
-------
46+
str
47+
A slug-style ID suitable for HTML anchors.
48+
49+
Examples
50+
--------
51+
>>> _generate_argument_id(["-L"])
52+
'L'
53+
>>> _generate_argument_id(["--help"])
54+
'help'
55+
>>> _generate_argument_id(["-v", "--verbose"])
56+
'v-verbose'
57+
>>> _generate_argument_id(["-L"], "shell")
58+
'shell-L'
59+
>>> _generate_argument_id(["filename"])
60+
'filename'
61+
>>> _generate_argument_id([])
62+
''
63+
"""
64+
clean_names = [name.lstrip("-") for name in names if name.lstrip("-")]
65+
if not clean_names:
66+
return ""
67+
name_part = "-".join(clean_names)
68+
return f"{id_prefix}-{name_part}" if id_prefix else name_part
69+
70+
2971
def _token_to_css_class(token_type: t.Any) -> str:
3072
"""Map a Pygments token type to its CSS class abbreviation.
3173
@@ -419,6 +461,9 @@ def visit_argparse_argument_html(
419461
- Positional arguments get class 'nl' (Name.Label)
420462
- Metavars get class 'nv' (Name.Variable)
421463
464+
The argument is wrapped in a container div with a unique ID for linking.
465+
A headerlink anchor (¶) is added for direct navigation.
466+
422467
Parameters
423468
----------
424469
self : HTML5Translator
@@ -428,11 +473,28 @@ def visit_argparse_argument_html(
428473
"""
429474
names: list[str] = node.get("names", [])
430475
metavar = node.get("metavar")
476+
id_prefix: str = node.get("id_prefix", "")
477+
478+
# Generate unique ID for this argument
479+
arg_id = _generate_argument_id(names, id_prefix)
480+
481+
# Open wrapper div with ID for linking
482+
if arg_id:
483+
self.body.append(f'<div class="argparse-argument-wrapper" id="{arg_id}">\n')
484+
else:
485+
self.body.append('<div class="argparse-argument-wrapper">\n')
431486

432487
# Build the argument signature with syntax highlighting
433488
highlighted_sig = _highlight_argument_names(names, metavar, self.encode)
434489

435-
self.body.append(f'<dt class="argparse-argument-name">{highlighted_sig}</dt>\n')
490+
# Add headerlink anchor inside dt for navigation
491+
headerlink = ""
492+
if arg_id:
493+
headerlink = f'<a class="headerlink" href="#{arg_id}">¶</a>'
494+
495+
self.body.append(
496+
f'<dt class="argparse-argument-name">{highlighted_sig}{headerlink}</dt>\n'
497+
)
436498
self.body.append('<dd class="argparse-argument-help">')
437499

438500
# Add help text
@@ -447,6 +509,7 @@ def depart_argparse_argument_html(
447509
"""Depart argparse_argument node - close argument entry.
448510
449511
Adds default, choices, and type information if present.
512+
Default values are wrapped in ``<span class="nv">`` for styled display.
450513
451514
Parameters
452515
----------
@@ -460,7 +523,8 @@ def depart_argparse_argument_html(
460523

461524
default = node.get("default_string")
462525
if default is not None:
463-
metadata.append(f"Default: {self.encode(default)}")
526+
# Wrap default value in nv span for yellow/italic styling
527+
metadata.append(f'Default: <span class="nv">{self.encode(default)}</span>')
464528

465529
choices = node.get("choices")
466530
if choices:
@@ -480,6 +544,8 @@ def depart_argparse_argument_html(
480544
self.body.append(f'<p class="argparse-argument-meta">{meta_str}</p>')
481545

482546
self.body.append("</dd>\n")
547+
# Close wrapper div
548+
self.body.append("</div>\n")
483549

484550

485551
def visit_argparse_subcommands_html(

docs/_ext/sphinx_argparse_neo/renderer.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -358,13 +358,18 @@ def render_group_section(
358358
section += nodes.title(title, title)
359359

360360
# Create the styled group container (with empty title - section provides it)
361-
group_node = self.render_group(group, include_title=False)
361+
# Pass id_prefix to render_group so arguments get unique IDs
362+
group_node = self.render_group(group, include_title=False, id_prefix=id_prefix)
362363
section += group_node
363364

364365
return section
365366

366367
def render_group(
367-
self, group: ArgumentGroup, include_title: bool = True
368+
self,
369+
group: ArgumentGroup,
370+
include_title: bool = True,
371+
*,
372+
id_prefix: str = "",
368373
) -> argparse_group:
369374
"""Render an argument group.
370375
@@ -376,6 +381,10 @@ def render_group(
376381
Whether to include the title in the group node. When False,
377382
the title is assumed to come from a parent section node.
378383
Default is True for backwards compatibility.
384+
id_prefix : str
385+
Optional prefix for argument IDs (e.g., "shell" -> "shell-h").
386+
Used to ensure unique IDs when multiple argparse directives exist
387+
on the same page.
379388
380389
Returns
381390
-------
@@ -397,23 +406,29 @@ def render_group(
397406

398407
# Add individual arguments
399408
for arg in group.arguments:
400-
arg_node = self.render_argument(arg)
409+
arg_node = self.render_argument(arg, id_prefix=id_prefix)
401410
group_node.append(arg_node)
402411

403412
# Add mutually exclusive groups
404413
for mutex in group.mutually_exclusive:
405-
mutex_nodes = self.render_mutex_group(mutex)
414+
mutex_nodes = self.render_mutex_group(mutex, id_prefix=id_prefix)
406415
group_node.extend(mutex_nodes)
407416

408417
return group_node
409418

410-
def render_argument(self, arg: ArgumentInfo) -> argparse_argument:
419+
def render_argument(
420+
self, arg: ArgumentInfo, *, id_prefix: str = ""
421+
) -> argparse_argument:
411422
"""Render a single argument.
412423
413424
Parameters
414425
----------
415426
arg : ArgumentInfo
416427
The argument to render.
428+
id_prefix : str
429+
Optional prefix for the argument ID (e.g., "shell" -> "shell-L").
430+
Used to ensure unique IDs when multiple argparse directives exist
431+
on the same page.
417432
418433
Returns
419434
-------
@@ -425,6 +440,7 @@ def render_argument(self, arg: ArgumentInfo) -> argparse_argument:
425440
arg_node["help"] = arg.help
426441
arg_node["metavar"] = arg.metavar
427442
arg_node["required"] = arg.required
443+
arg_node["id_prefix"] = id_prefix
428444

429445
if self.config.show_defaults:
430446
arg_node["default_string"] = arg.default_string
@@ -438,14 +454,16 @@ def render_argument(self, arg: ArgumentInfo) -> argparse_argument:
438454
return arg_node
439455

440456
def render_mutex_group(
441-
self, mutex: MutuallyExclusiveGroup
457+
self, mutex: MutuallyExclusiveGroup, *, id_prefix: str = ""
442458
) -> list[argparse_argument]:
443459
"""Render a mutually exclusive group.
444460
445461
Parameters
446462
----------
447463
mutex : MutuallyExclusiveGroup
448464
The mutually exclusive group.
465+
id_prefix : str
466+
Optional prefix for argument IDs (e.g., "shell" -> "shell-h").
449467
450468
Returns
451469
-------
@@ -454,7 +472,7 @@ def render_mutex_group(
454472
"""
455473
result: list[argparse_argument] = []
456474
for arg in mutex.arguments:
457-
arg_node = self.render_argument(arg)
475+
arg_node = self.render_argument(arg, id_prefix=id_prefix)
458476
# Mark as part of mutex group
459477
arg_node["mutex"] = True
460478
arg_node["mutex_required"] = mutex.required

0 commit comments

Comments
 (0)