diff --git a/CHANGES b/CHANGES
index d615dc0c7a..c65567baef 100644
--- a/CHANGES
+++ b/CHANGES
@@ -35,6 +35,17 @@ $ pipx install --suffix=@next 'tmuxp' --pip-args '\--pre' --force
_Notes on the upcoming release will go here._
+### Documentation
+
+#### Linkable CLI arguments and options (#1010)
+
+CLI documentation now supports direct linking to specific arguments:
+
+- **Linkable options**: Each `--option` and positional argument has a permanent URL anchor (e.g., `cli/load.html#-d`)
+- **Headerlinks**: Hover over any argument to reveal a ¶ link for easy sharing
+- **Visual styling**: Argument names displayed with syntax-highlighted backgrounds for better readability
+- **Default value badges**: Default values shown as styled inline code (e.g., `auto`)
+
## tmuxp 1.64.0 (2026-01-24)
### Documentation
diff --git a/docs/_ext/sphinx_argparse_neo/nodes.py b/docs/_ext/sphinx_argparse_neo/nodes.py
index 55039db639..f34f864f9c 100644
--- a/docs/_ext/sphinx_argparse_neo/nodes.py
+++ b/docs/_ext/sphinx_argparse_neo/nodes.py
@@ -26,6 +26,48 @@
from sphinx_argparse_neo.utils import strip_ansi # noqa: E402
+def _generate_argument_id(names: list[str], id_prefix: str = "") -> str:
+ """Generate unique ID for an argument based on its names.
+
+ Creates a slug-style ID suitable for HTML anchors by:
+ 1. Stripping leading dashes from option names
+ 2. Joining multiple names with hyphens
+ 3. Prepending optional prefix for namespace isolation
+
+ Parameters
+ ----------
+ names : list[str]
+ List of argument names (e.g., ["-L", "--socket-name"]).
+ id_prefix : str
+ Optional prefix for uniqueness (e.g., "shell" -> "shell-L-socket-name").
+
+ Returns
+ -------
+ str
+ A slug-style ID suitable for HTML anchors.
+
+ Examples
+ --------
+ >>> _generate_argument_id(["-L"])
+ 'L'
+ >>> _generate_argument_id(["--help"])
+ 'help'
+ >>> _generate_argument_id(["-v", "--verbose"])
+ 'v-verbose'
+ >>> _generate_argument_id(["-L"], "shell")
+ 'shell-L'
+ >>> _generate_argument_id(["filename"])
+ 'filename'
+ >>> _generate_argument_id([])
+ ''
+ """
+ clean_names = [name.lstrip("-") for name in names if name.lstrip("-")]
+ if not clean_names:
+ return ""
+ name_part = "-".join(clean_names)
+ return f"{id_prefix}-{name_part}" if id_prefix else name_part
+
+
def _token_to_css_class(token_type: t.Any) -> str:
"""Map a Pygments token type to its CSS class abbreviation.
@@ -419,6 +461,9 @@ def visit_argparse_argument_html(
- Positional arguments get class 'nl' (Name.Label)
- Metavars get class 'nv' (Name.Variable)
+ The argument is wrapped in a container div with a unique ID for linking.
+ A headerlink anchor (¶) is added for direct navigation.
+
Parameters
----------
self : HTML5Translator
@@ -428,11 +473,28 @@ def visit_argparse_argument_html(
"""
names: list[str] = node.get("names", [])
metavar = node.get("metavar")
+ id_prefix: str = node.get("id_prefix", "")
+
+ # Generate unique ID for this argument
+ arg_id = _generate_argument_id(names, id_prefix)
+
+ # Open wrapper div with ID for linking
+ if arg_id:
+ self.body.append(f'
\n')
+ else:
+ self.body.append('
\n')
# Build the argument signature with syntax highlighting
highlighted_sig = _highlight_argument_names(names, metavar, self.encode)
- self.body.append(f'
{highlighted_sig}\n')
+ # Add headerlink anchor inside dt for navigation
+ headerlink = ""
+ if arg_id:
+ headerlink = f''
+
+ self.body.append(
+ f'
{highlighted_sig}{headerlink}\n'
+ )
self.body.append('
')
# Add help text
@@ -447,6 +509,7 @@ def depart_argparse_argument_html(
"""Depart argparse_argument node - close argument entry.
Adds default, choices, and type information if present.
+ Default values are wrapped in ```` for styled display.
Parameters
----------
@@ -460,7 +523,8 @@ def depart_argparse_argument_html(
default = node.get("default_string")
if default is not None:
- metadata.append(f"Default: {self.encode(default)}")
+ # Wrap default value in nv span for yellow/italic styling
+ metadata.append(f'Default: {self.encode(default)}')
choices = node.get("choices")
if choices:
@@ -480,6 +544,8 @@ def depart_argparse_argument_html(
self.body.append(f'{meta_str}
')
self.body.append("\n")
+ # Close wrapper div
+ self.body.append("
\n")
def visit_argparse_subcommands_html(
diff --git a/docs/_ext/sphinx_argparse_neo/renderer.py b/docs/_ext/sphinx_argparse_neo/renderer.py
index 69272b5896..f6c313f9f1 100644
--- a/docs/_ext/sphinx_argparse_neo/renderer.py
+++ b/docs/_ext/sphinx_argparse_neo/renderer.py
@@ -358,13 +358,18 @@ def render_group_section(
section += nodes.title(title, title)
# Create the styled group container (with empty title - section provides it)
- group_node = self.render_group(group, include_title=False)
+ # Pass id_prefix to render_group so arguments get unique IDs
+ group_node = self.render_group(group, include_title=False, id_prefix=id_prefix)
section += group_node
return section
def render_group(
- self, group: ArgumentGroup, include_title: bool = True
+ self,
+ group: ArgumentGroup,
+ include_title: bool = True,
+ *,
+ id_prefix: str = "",
) -> argparse_group:
"""Render an argument group.
@@ -376,6 +381,10 @@ def render_group(
Whether to include the title in the group node. When False,
the title is assumed to come from a parent section node.
Default is True for backwards compatibility.
+ id_prefix : str
+ Optional prefix for argument IDs (e.g., "shell" -> "shell-h").
+ Used to ensure unique IDs when multiple argparse directives exist
+ on the same page.
Returns
-------
@@ -397,23 +406,29 @@ def render_group(
# Add individual arguments
for arg in group.arguments:
- arg_node = self.render_argument(arg)
+ arg_node = self.render_argument(arg, id_prefix=id_prefix)
group_node.append(arg_node)
# Add mutually exclusive groups
for mutex in group.mutually_exclusive:
- mutex_nodes = self.render_mutex_group(mutex)
+ mutex_nodes = self.render_mutex_group(mutex, id_prefix=id_prefix)
group_node.extend(mutex_nodes)
return group_node
- def render_argument(self, arg: ArgumentInfo) -> argparse_argument:
+ def render_argument(
+ self, arg: ArgumentInfo, *, id_prefix: str = ""
+ ) -> argparse_argument:
"""Render a single argument.
Parameters
----------
arg : ArgumentInfo
The argument to render.
+ id_prefix : str
+ Optional prefix for the argument ID (e.g., "shell" -> "shell-L").
+ Used to ensure unique IDs when multiple argparse directives exist
+ on the same page.
Returns
-------
@@ -425,6 +440,7 @@ def render_argument(self, arg: ArgumentInfo) -> argparse_argument:
arg_node["help"] = arg.help
arg_node["metavar"] = arg.metavar
arg_node["required"] = arg.required
+ arg_node["id_prefix"] = id_prefix
if self.config.show_defaults:
arg_node["default_string"] = arg.default_string
@@ -438,7 +454,7 @@ def render_argument(self, arg: ArgumentInfo) -> argparse_argument:
return arg_node
def render_mutex_group(
- self, mutex: MutuallyExclusiveGroup
+ self, mutex: MutuallyExclusiveGroup, *, id_prefix: str = ""
) -> list[argparse_argument]:
"""Render a mutually exclusive group.
@@ -446,6 +462,8 @@ def render_mutex_group(
----------
mutex : MutuallyExclusiveGroup
The mutually exclusive group.
+ id_prefix : str
+ Optional prefix for argument IDs (e.g., "shell" -> "shell-h").
Returns
-------
@@ -454,7 +472,7 @@ def render_mutex_group(
"""
result: list[argparse_argument] = []
for arg in mutex.arguments:
- arg_node = self.render_argument(arg)
+ arg_node = self.render_argument(arg, id_prefix=id_prefix)
# Mark as part of mutex group
arg_node["mutex"] = True
arg_node["mutex_required"] = mutex.required
diff --git a/docs/_static/css/argparse-highlight.css b/docs/_static/css/argparse-highlight.css
index dce008dc36..a0a77ec795 100644
--- a/docs/_static/css/argparse-highlight.css
+++ b/docs/_static/css/argparse-highlight.css
@@ -21,21 +21,25 @@
Inline Role Styles
========================================================================== */
+/*
+ * Shared monospace font for all CLI inline roles
+ */
+.cli-option,
+.cli-metavar,
+.cli-command,
+.cli-default,
+.cli-choice {
+ font-family: var(--font-stack--monospace);
+}
+
/*
* CLI Options
*
* Long options (--verbose) and short options (-h) both use teal color.
*/
-.cli-option {
- font-family: var(--font-stack--monospace);
-}
-
-.cli-option-long {
- color: #56B6C2;
-}
-
+.cli-option-long,
.cli-option-short {
- color: #56B6C2;
+ color: #56b6c2;
}
/*
@@ -45,21 +49,24 @@
* Yellow/amber to indicate "replace me" - distinct from flags (teal).
*/
.cli-metavar {
- font-family: var(--font-stack--monospace);
- color: #E5C07B;
- font-style: italic;
+ color: #e5c07b;
+ font-style: italic;
}
/*
- * CLI Commands
+ * CLI Commands and Choices
*
- * Subcommand names like sync, add, list.
- * Green (same as choices) - they're enumerated command options.
+ * Both use green to indicate valid enumerated values.
+ * Commands: subcommand names like sync, add, list
+ * Choices: enumeration values like json, yaml, table
*/
+.cli-command,
+.cli-choice {
+ color: #98c379;
+}
+
.cli-command {
- font-family: var(--font-stack--monospace);
- color: #98C379;
- font-weight: bold;
+ font-weight: bold;
}
/*
@@ -69,20 +76,8 @@
* Subtle styling to not distract from options.
*/
.cli-default {
- font-family: var(--font-stack--monospace);
- color: #CCCED4;
- font-style: italic;
-}
-
-/*
- * CLI Choice Values
- *
- * Choice enumeration values like json, yaml, table.
- * Green color to show they are valid literal values.
- */
-.cli-choice {
- font-family: var(--font-stack--monospace);
- color: #98C379;
+ color: #ccced4;
+ font-style: italic;
}
/* ==========================================================================
@@ -98,55 +93,55 @@
.highlight-argparse .gh,
.highlight-argparse-usage .gh,
.highlight-argparse-help .gh {
- color: #61AFEF;
- font-weight: bold;
+ color: #61afef;
+ font-weight: bold;
}
/* Section headers like "positional arguments:", "options:" - neutral bright */
.highlight-argparse .gs,
.highlight-argparse-help .gs {
- color: #E5E5E5;
- font-weight: bold;
+ color: #e5e5e5;
+ font-weight: bold;
}
/* Long options --foo - teal */
.highlight-argparse .nt,
.highlight-argparse-usage .nt,
.highlight-argparse-help .nt {
- color: #56B6C2;
- font-weight: normal;
+ color: #56b6c2;
+ font-weight: normal;
}
/* Short options -h - teal (same as long) */
.highlight-argparse .na,
.highlight-argparse-usage .na,
.highlight-argparse-help .na {
- color: #56B6C2;
- font-weight: normal;
+ color: #56b6c2;
+ font-weight: normal;
}
/* Metavar placeholders FILE, PATH - yellow/amber italic */
.highlight-argparse .nv,
.highlight-argparse-usage .nv,
.highlight-argparse-help .nv {
- color: #E5C07B;
- font-style: italic;
+ color: #e5c07b;
+ font-style: italic;
}
/* Command/program names - purple bold */
.highlight-argparse .nl,
.highlight-argparse-usage .nl,
.highlight-argparse-help .nl {
- color: #C678DD;
- font-weight: bold;
+ color: #c678dd;
+ font-weight: bold;
}
/* Subcommands - bold green */
.highlight-argparse .nf,
.highlight-argparse-usage .nf,
.highlight-argparse-help .nf {
- color: #98C379;
- font-weight: bold;
+ color: #98c379;
+ font-weight: bold;
}
/* Choice values - green */
@@ -156,22 +151,22 @@
.highlight-argparse .nc,
.highlight-argparse-usage .nc,
.highlight-argparse-help .nc {
- color: #98C379;
+ color: #98c379;
}
/* Punctuation [], {}, () - neutral gray */
.highlight-argparse .p,
.highlight-argparse-usage .p,
.highlight-argparse-help .p {
- color: #CCCED4;
+ color: #ccced4;
}
/* Operators like | - neutral gray */
.highlight-argparse .o,
.highlight-argparse-usage .o,
.highlight-argparse-help .o {
- color: #CCCED4;
- font-weight: normal;
+ color: #ccced4;
+ font-weight: normal;
}
/* ==========================================================================
@@ -195,56 +190,56 @@
/* Usage block container - match Pygments monokai background and code block styling */
pre.argparse-usage {
- background: #272822; /* Monokai - same as .highlight */
- padding: .625rem .875rem;
- line-height: 1.5;
- border-radius: .2rem;
- scrollbar-color: var(--color-foreground-border) transparent;
- scrollbar-width: thin;
+ background: var(--argparse-code-background);
+ padding: 0.625rem 0.875rem;
+ line-height: 1.5;
+ border-radius: 0.2rem;
+ scrollbar-color: var(--color-foreground-border) transparent;
+ scrollbar-width: thin;
}
.argparse-usage .gh {
- color: #61AFEF;
- font-weight: bold;
+ color: #61afef;
+ font-weight: bold;
}
.argparse-usage .nt {
- color: #56B6C2;
- font-weight: normal;
+ color: #56b6c2;
+ font-weight: normal;
}
.argparse-usage .na {
- color: #56B6C2;
- font-weight: normal;
+ color: #56b6c2;
+ font-weight: normal;
}
.argparse-usage .nv {
- color: #E5C07B;
- font-style: italic;
+ color: #e5c07b;
+ font-style: italic;
}
.argparse-usage .nl {
- color: #C678DD;
- font-weight: bold;
+ color: #c678dd;
+ font-weight: bold;
}
.argparse-usage .nf {
- color: #98C379;
- font-weight: bold;
+ color: #98c379;
+ font-weight: bold;
}
.argparse-usage .no,
.argparse-usage .nc {
- color: #98C379;
+ color: #98c379;
}
.argparse-usage .o {
- color: #CCCED4;
- font-weight: normal;
+ color: #ccced4;
+ font-weight: normal;
}
.argparse-usage .p {
- color: #CCCED4;
+ color: #ccced4;
}
/*
@@ -254,21 +249,121 @@ pre.argparse-usage {
* semantic spans for options and metavars.
*/
.argparse-argument-name .nt {
- color: #56B6C2;
- font-weight: normal;
+ color: #56b6c2;
+ font-weight: normal;
}
.argparse-argument-name .na {
- color: #56B6C2;
- font-weight: normal;
+ color: #56b6c2;
+ font-weight: normal;
}
.argparse-argument-name .nv {
- color: #E5C07B;
- font-style: italic;
+ color: #e5c07b;
+ font-style: italic;
}
.argparse-argument-name .nl {
- color: #C678DD;
- font-weight: bold;
+ color: #c678dd;
+ font-weight: bold;
+}
+
+/* ==========================================================================
+ Argument Wrapper and Linking Styles
+ ========================================================================== */
+
+/*
+ * Argparse-specific code background (monokai #272822)
+ * Uses a custom variable to avoid affecting Furo's --color-inline-code-background.
+ */
+:root {
+ --argparse-code-background: #272822;
+}
+
+/*
+ * Background styling for argument names - subtle background like code.literal
+ * This provides visual weight and hierarchy for argument definitions.
+ */
+.argparse-argument-name {
+ background: var(--argparse-code-background);
+ border-radius: 0.2rem;
+ padding: 0.485rem 0.875rem;
+ font-family: var(--font-stack--monospace);
+ width: fit-content;
+ position: relative;
+}
+
+/*
+ * Wrapper for linking - enables scroll-margin for fixed header navigation
+ * and :target pseudo-class for highlighting when linked.
+ */
+.argparse-argument-wrapper {
+ scroll-margin-top: 2.5rem;
+}
+
+/*
+ * Headerlink anchor (¶) - hidden until hover
+ * Positioned outside the dt element to the right.
+ * Follows Sphinx documentation convention for linkable headings.
+ */
+.argparse-argument-name .headerlink {
+ visibility: hidden;
+ position: absolute;
+ right: -1.5rem;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--color-foreground-secondary);
+ text-decoration: none;
+}
+
+/*
+ * Show headerlink on hover or when targeted via URL fragment
+ */
+.argparse-argument-wrapper:hover .headerlink,
+.argparse-argument-wrapper:target .headerlink {
+ visibility: visible;
+}
+
+.argparse-argument-name .headerlink:hover:not(:visited) {
+ color: var(--color-foreground-primary);
+}
+
+/*
+ * Light mode headerlink color overrides
+ */
+body:not([data-theme="dark"]) .argparse-argument-name .headerlink {
+ color: #9ca0a5;
+
+ &:hover:not(:visited) {
+ color: #cfd0d0;
+ }
+}
+
+/*
+ * Highlight when targeted via URL fragment
+ * Uses Furo's highlight-on-target color for consistency.
+ */
+.argparse-argument-wrapper:target .argparse-argument-name {
+ background-color: var(--color-highlight-on-target);
+}
+
+/*
+ * Default value styling in metadata
+ * Styled like inline code with monokai background.
+ */
+.argparse-argument-meta .nv {
+ background: var(--argparse-code-background);
+ border-radius: 0.2rem;
+ padding: 0.1405rem 0.3rem;
+ font-family: var(--font-stack--monospace);
+ font-size: var(--font-size--small);
+ color: #e5c07b;
+}
+
+/*
+ * Help text description
+ * Adds spacing above for visual separation from argument name.
+ */
+.argparse-argument-help {
+ padding-block-start: 0.5rem;
}
diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css
index b420cee944..00a15fdc22 100644
--- a/docs/_static/css/custom.css
+++ b/docs/_static/css/custom.css
@@ -1,20 +1,21 @@
.sidebar-tree p.indented-block {
- padding: var(--sidebar-item-spacing-vertical) var(--sidebar-item-spacing-horizontal) 0
- var(--sidebar-item-spacing-horizontal);
- margin-bottom: 0;
+ padding: var(--sidebar-item-spacing-vertical)
+ var(--sidebar-item-spacing-horizontal) 0
+ var(--sidebar-item-spacing-horizontal);
+ margin-bottom: 0;
}
.sidebar-tree p.indented-block span.indent {
- margin-left: var(--sidebar-item-spacing-horizontal);
- display: block;
+ margin-left: var(--sidebar-item-spacing-horizontal);
+ display: block;
}
.sidebar-tree p.indented-block .project-name {
- font-size: var(--sidebar-item-font-size);
- font-weight: bold;
- margin-right: calc(var(--sidebar-item-spacing-horizontal) / 2.5);
+ font-size: var(--sidebar-item-font-size);
+ font-weight: bold;
+ margin-right: calc(var(--sidebar-item-spacing-horizontal) / 2.5);
}
.sidebar-tree .active {
- font-weight: bold;
+ font-weight: bold;
}
diff --git a/tests/docs/_ext/sphinx_argparse_neo/test_nodes.py b/tests/docs/_ext/sphinx_argparse_neo/test_nodes.py
index c473c9974c..e30306b6a7 100644
--- a/tests/docs/_ext/sphinx_argparse_neo/test_nodes.py
+++ b/tests/docs/_ext/sphinx_argparse_neo/test_nodes.py
@@ -2,8 +2,13 @@
from __future__ import annotations
+import re
+import typing as t
+
+import pytest
from docutils import nodes
from sphinx_argparse_neo.nodes import (
+ _generate_argument_id,
argparse_argument,
argparse_group,
argparse_program,
@@ -257,3 +262,264 @@ def test_full_node_tree() -> None:
assert isinstance(program.children[1], argparse_group)
assert isinstance(program.children[2], argparse_group)
assert isinstance(program.children[3], argparse_subcommands)
+
+
+# --- ID generation tests ---
+
+
+def test_generate_argument_id_short_option() -> None:
+ """Test ID generation for short option."""
+ assert _generate_argument_id(["-L"]) == "L"
+
+
+def test_generate_argument_id_long_option() -> None:
+ """Test ID generation for long option."""
+ assert _generate_argument_id(["--help"]) == "help"
+
+
+def test_generate_argument_id_multiple_names() -> None:
+ """Test ID generation for argument with multiple names."""
+ assert _generate_argument_id(["-v", "--verbose"]) == "v-verbose"
+
+
+def test_generate_argument_id_with_prefix() -> None:
+ """Test ID generation with prefix for namespace isolation."""
+ assert _generate_argument_id(["-L"], "shell") == "shell-L"
+ assert _generate_argument_id(["--help"], "load") == "load-help"
+
+
+def test_generate_argument_id_positional() -> None:
+ """Test ID generation for positional argument."""
+ assert _generate_argument_id(["filename"]) == "filename"
+
+
+def test_generate_argument_id_empty() -> None:
+ """Test ID generation with empty names list."""
+ assert _generate_argument_id([]) == ""
+
+
+def test_generate_argument_id_prefix_no_names() -> None:
+ """Test that prefix alone doesn't create ID when no names."""
+ assert _generate_argument_id([], "shell") == ""
+
+
+# --- HTML rendering tests using NamedTuple for parametrization ---
+
+
+class ArgumentHTMLCase(t.NamedTuple):
+ """Test case for argument HTML rendering."""
+
+ test_id: str
+ names: list[str]
+ metavar: str | None
+ help_text: str | None
+ default: str | None
+ id_prefix: str
+ expected_patterns: list[str] # Regex patterns to match
+
+
+ARGUMENT_HTML_CASES = [
+ ArgumentHTMLCase(
+ test_id="short_option_with_metavar",
+ names=["-L"],
+ metavar="socket-name",
+ help_text="pass-through for tmux -L",
+ default="None",
+ id_prefix="shell",
+ expected_patterns=[
+ r'
',
+ r'
',
+ r'-L',
+ r'socket-name',
+ r'',
+ r'Default: None',
+ r"",
+ ],
+ ),
+ ArgumentHTMLCase(
+ test_id="long_option",
+ names=["--help"],
+ metavar=None,
+ help_text="show help",
+ default=None,
+ id_prefix="",
+ expected_patterns=[
+ r'
--help',
+ r'id="help"',
+ r'href="#help"',
+ ],
+ ),
+ ArgumentHTMLCase(
+ test_id="positional_argument",
+ names=["filename"],
+ metavar=None,
+ help_text="input file",
+ default=None,
+ id_prefix="",
+ expected_patterns=[
+ r'
filename',
+ r'id="filename"',
+ ],
+ ),
+ ArgumentHTMLCase(
+ test_id="multiple_names",
+ names=["-v", "--verbose"],
+ metavar=None,
+ help_text="Enable verbose mode",
+ default=None,
+ id_prefix="load",
+ expected_patterns=[
+ r'id="load-v-verbose"',
+ r'
-v',
+ r'
--verbose',
+ r'href="#load-v-verbose"',
+ ],
+ ),
+]
+
+
+class MockTranslator:
+ """Mock HTML5Translator for testing HTML generation."""
+
+ def __init__(self) -> None:
+ """Initialize mock translator."""
+ self.body: list[str] = []
+
+ def encode(self, text: str) -> str:
+ """HTML encode text."""
+ return str(text).replace("&", "&").replace("<", "<").replace(">", ">")
+
+
+def render_argument_to_html(
+ names: list[str],
+ metavar: str | None,
+ help_text: str | None,
+ default: str | None,
+ id_prefix: str,
+) -> str:
+ """Render an argument node to HTML string for testing.
+
+ Parameters
+ ----------
+ names
+ Argument names (e.g., ["-v", "--verbose"]).
+ metavar
+ Optional metavar (e.g., "FILE").
+ help_text
+ Help text for the argument.
+ default
+ Default value string.
+ id_prefix
+ Prefix for the argument ID.
+
+ Returns
+ -------
+ str
+ HTML string from the mock translator's body.
+ """
+ from sphinx_argparse_neo.nodes import (
+ depart_argparse_argument_html,
+ visit_argparse_argument_html,
+ )
+
+ node = argparse_argument()
+ node["names"] = names
+ node["metavar"] = metavar
+ node["help"] = help_text
+ node["default_string"] = default
+ node["id_prefix"] = id_prefix
+
+ translator = MockTranslator()
+ visit_argparse_argument_html(translator, node)
+ depart_argparse_argument_html(translator, node)
+
+ return "".join(translator.body)
+
+
+@pytest.mark.parametrize(
+ "case",
+ ARGUMENT_HTML_CASES,
+ ids=lambda c: c.test_id,
+)
+def test_argument_html_rendering(case: ArgumentHTMLCase) -> None:
+ """Test HTML output for argument nodes."""
+ html = render_argument_to_html(
+ names=case.names,
+ metavar=case.metavar,
+ help_text=case.help_text,
+ default=case.default,
+ id_prefix=case.id_prefix,
+ )
+
+ for pattern in case.expected_patterns:
+ assert re.search(pattern, html), f"Pattern not found: {pattern}\nHTML: {html}"
+
+
+def test_argument_wrapper_has_id() -> None:
+ """Verify wrapper div has correct ID attribute."""
+ html = render_argument_to_html(
+ names=["-f", "--file"],
+ metavar="PATH",
+ help_text="Input file",
+ default=None,
+ id_prefix="convert",
+ )
+
+ assert 'id="convert-f-file"' in html
+ assert '
None:
+ """Verify headerlink anchor exists with correct href."""
+ html = render_argument_to_html(
+ names=["--output"],
+ metavar="FILE",
+ help_text="Output file",
+ default=None,
+ id_prefix="freeze",
+ )
+
+ assert '' in html
+
+
+def test_default_value_styled() -> None:
+ """Verify default value is wrapped in nv span."""
+ html = render_argument_to_html(
+ names=["--format"],
+ metavar=None,
+ help_text="Output format",
+ default="json",
+ id_prefix="",
+ )
+
+ assert 'Default:
json' in html
+
+
+def test_wrapper_div_closed() -> None:
+ """Verify wrapper div is properly closed."""
+ html = render_argument_to_html(
+ names=["-v"],
+ metavar=None,
+ help_text="Verbose",
+ default=None,
+ id_prefix="",
+ )
+
+ # Count opening and closing div tags
+ open_divs = html.count("
")
+ assert open_divs == close_divs, f"Unbalanced divs in HTML: {html}"
+
+
+def test_argument_no_id_prefix() -> None:
+ """Test argument rendering without ID prefix."""
+ html = render_argument_to_html(
+ names=["--debug"],
+ metavar=None,
+ help_text="Enable debug mode",
+ default=None,
+ id_prefix="",
+ )
+
+ assert 'id="debug"' in html
+ assert 'href="#debug"' in html