Skip to content
Next Next commit
feat(docs): Render argparse metadata as semantic definition list
Replace inline pipe-separated metadata (Default: None | Type: str | Required)
with a semantic <dl> structure that enables independent CSS styling of keys,
values, and tags. Uses Furo's guilabel pattern for Required tag with
semi-transparent amber background for light/dark mode compatibility.
  • Loading branch information
tony committed Jan 25, 2026
commit 24f7fb400678cd9b63b8ed7728651377c3fa8fae
54 changes: 35 additions & 19 deletions docs/_ext/sphinx_argparse_neo/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,30 +518,46 @@ def depart_argparse_argument_html(
node : argparse_argument
The argument node being departed.
"""
# Add metadata (default, choices, type)
metadata: list[str] = []

# Build metadata as definition list items
default = node.get("default_string")
if default is not None:
# Wrap default value in nv span for yellow/italic styling
metadata.append(f'Default: <span class="nv">{self.encode(default)}</span>')

choices = node.get("choices")
if choices:
choices_str = ", ".join(str(c) for c in choices)
metadata.append(f"Choices: {self.encode(choices_str)}")

type_name = node.get("type_name")
if type_name:
metadata.append(f"Type: {self.encode(type_name)}")

required = node.get("required", False)
if required:
metadata.append("Required")

if metadata:
meta_str = " | ".join(metadata)
self.body.append(f'<p class="argparse-argument-meta">{meta_str}</p>')
if default is not None or choices or type_name or required:
self.body.append('<dl class="argparse-argument-meta">\n')

if default is not None:
self.body.append('<div class="argparse-meta-item">')
self.body.append('<dt class="argparse-meta-key">Default</dt>')
self.body.append(
f'<dd class="argparse-meta-value">'
f'<span class="nv">{self.encode(default)}</span></dd>'
)
self.body.append("</div>\n")

if type_name:
self.body.append('<div class="argparse-meta-item">')
self.body.append('<dt class="argparse-meta-key">Type</dt>')
self.body.append(
f'<dd class="argparse-meta-value">'
f'<span class="nv">{self.encode(type_name)}</span></dd>'
)
self.body.append("</div>\n")

if choices:
choices_str = ", ".join(str(c) for c in choices)
self.body.append('<div class="argparse-meta-item">')
self.body.append('<dt class="argparse-meta-key">Choices</dt>')
self.body.append(
f'<dd class="argparse-meta-value">{self.encode(choices_str)}</dd>'
)
self.body.append("</div>\n")

if required:
self.body.append('<dt class="argparse-meta-tag">Required</dt>\n')

self.body.append("</dl>\n")

self.body.append("</dd>\n")
# Close wrapper div
Expand Down
55 changes: 51 additions & 4 deletions docs/_static/css/argparse-highlight.css
Original file line number Diff line number Diff line change
Expand Up @@ -348,18 +348,65 @@ body:not([data-theme="dark"]) .argparse-argument-name .headerlink {
}

/*
* Default value styling in metadata
* Styled like inline code with monokai background.
* Argument metadata definition list
*
* Renders metadata (Default, Type, Choices, Required) as a horizontal
* flexbox of key-value pairs and standalone tags.
*/
.argparse-argument-meta .nv {
.argparse-argument-meta {
margin: 0.5rem 0 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
align-items: center;
}

.argparse-meta-item {
display: flex;
align-items: center;
gap: 0.25rem;
}

.argparse-meta-key {
color: var(--color-foreground-secondary, #6c757d);
font-size: var(--font-size--small);
}

.argparse-meta-key::after {
content: ":";
}

.argparse-meta-value .nv {
background: var(--argparse-code-background);
border-radius: 0.2rem;
padding: 0.1405rem 0.3rem;
padding: 0.1rem 0.3rem;
font-family: var(--font-stack--monospace);
font-size: var(--font-size--small);
color: #e5c07b;
}

/*
* Meta tag (e.g., "Required") - follows Furo's guilabel pattern
* Uses semi-transparent amber background with border for visibility
* without the harshness of solid fills. Amber conveys "needs attention".
*/
.argparse-meta-tag {
background-color: #fef3c780;
border: 1px solid #fcd34d80;
color: var(--color-foreground-primary);
font-size: var(--font-size--small);
padding: 0.1rem 0.4rem;
border-radius: 0.2rem;
font-weight: 500;
}

/* Dark mode: darker amber with adjusted border */
body[data-theme="dark"] .argparse-meta-tag {
background-color: #78350f60;
border-color: #b4530980;
}

/*
* Help text description
* Adds spacing above for visual separation from argument name.
Expand Down
95 changes: 92 additions & 3 deletions tests/docs/_ext/sphinx_argparse_neo/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ class ArgumentHTMLCase(t.NamedTuple):
metavar: str | None
help_text: str | None
default: str | None
type_name: str | None
required: bool
id_prefix: str
expected_patterns: list[str] # Regex patterns to match

Expand All @@ -325,14 +327,18 @@ class ArgumentHTMLCase(t.NamedTuple):
metavar="socket-name",
help_text="pass-through for tmux -L",
default="None",
type_name=None,
required=False,
id_prefix="shell",
expected_patterns=[
r'<div class="argparse-argument-wrapper" id="shell-L">',
r'<dt class="argparse-argument-name">',
r'<span class="na">-L</span>',
r'<span class="nv">socket-name</span>',
r'<a class="headerlink" href="#shell-L">¶</a>',
r'Default: <span class="nv">None</span>',
r'<dl class="argparse-argument-meta">',
r'<dt class="argparse-meta-key">Default</dt>',
r'<dd class="argparse-meta-value"><span class="nv">None</span></dd>',
r"</div>",
],
),
Expand All @@ -342,6 +348,8 @@ class ArgumentHTMLCase(t.NamedTuple):
metavar=None,
help_text="show help",
default=None,
type_name=None,
required=False,
id_prefix="",
expected_patterns=[
r'<span class="nt">--help</span>',
Expand All @@ -355,6 +363,8 @@ class ArgumentHTMLCase(t.NamedTuple):
metavar=None,
help_text="input file",
default=None,
type_name=None,
required=False,
id_prefix="",
expected_patterns=[
r'<span class="nl">filename</span>',
Expand All @@ -367,6 +377,8 @@ class ArgumentHTMLCase(t.NamedTuple):
metavar=None,
help_text="Enable verbose mode",
default=None,
type_name=None,
required=False,
id_prefix="load",
expected_patterns=[
r'id="load-v-verbose"',
Expand All @@ -375,6 +387,25 @@ class ArgumentHTMLCase(t.NamedTuple):
r'href="#load-v-verbose"',
],
),
ArgumentHTMLCase(
test_id="metadata_definition_list",
names=["workspace_file"],
metavar="workspace-file",
help_text="checks current tmuxp for workspace files.",
default="None",
type_name="str",
required=True,
id_prefix="edit",
expected_patterns=[
r'<dl class="argparse-argument-meta">',
r'<dt class="argparse-meta-key">Default</dt>',
r'<dd class="argparse-meta-value"><span class="nv">None</span></dd>',
r'<dt class="argparse-meta-key">Type</dt>',
r'<dd class="argparse-meta-value"><span class="nv">str</span></dd>',
r'<dt class="argparse-meta-tag">Required</dt>',
r"</dl>",
],
),
]


Expand All @@ -395,6 +426,8 @@ def render_argument_to_html(
metavar: str | None,
help_text: str | None,
default: str | None,
type_name: str | None,
required: bool,
id_prefix: str,
) -> str:
"""Render an argument node to HTML string for testing.
Expand All @@ -409,6 +442,10 @@ def render_argument_to_html(
Help text for the argument.
default
Default value string.
type_name
Type name for the argument (e.g., "str", "int").
required
Whether the argument is required.
id_prefix
Prefix for the argument ID.

Expand All @@ -427,6 +464,8 @@ def render_argument_to_html(
node["metavar"] = metavar
node["help"] = help_text
node["default_string"] = default
node["type_name"] = type_name
node["required"] = required
node["id_prefix"] = id_prefix

translator = MockTranslator()
Expand All @@ -448,6 +487,8 @@ def test_argument_html_rendering(case: ArgumentHTMLCase) -> None:
metavar=case.metavar,
help_text=case.help_text,
default=case.default,
type_name=case.type_name,
required=case.required,
id_prefix=case.id_prefix,
)

Expand All @@ -462,6 +503,8 @@ def test_argument_wrapper_has_id() -> None:
metavar="PATH",
help_text="Input file",
default=None,
type_name=None,
required=False,
id_prefix="convert",
)

Expand All @@ -476,23 +519,29 @@ def test_argument_headerlink_present() -> None:
metavar="FILE",
help_text="Output file",
default=None,
type_name=None,
required=False,
id_prefix="freeze",
)

assert '<a class="headerlink" href="#freeze-output">¶</a>' in html


def test_default_value_styled() -> None:
"""Verify default value is wrapped in nv span."""
"""Verify default value is wrapped in nv span within definition list."""
html = render_argument_to_html(
names=["--format"],
metavar=None,
help_text="Output format",
default="json",
type_name=None,
required=False,
id_prefix="",
)

assert 'Default: <span class="nv">json</span>' in html
assert '<dl class="argparse-argument-meta">' in html
assert '<dt class="argparse-meta-key">Default</dt>' in html
assert '<dd class="argparse-meta-value"><span class="nv">json</span></dd>' in html


def test_wrapper_div_closed() -> None:
Expand All @@ -502,6 +551,8 @@ def test_wrapper_div_closed() -> None:
metavar=None,
help_text="Verbose",
default=None,
type_name=None,
required=False,
id_prefix="",
)

Expand All @@ -518,8 +569,46 @@ def test_argument_no_id_prefix() -> None:
metavar=None,
help_text="Enable debug mode",
default=None,
type_name=None,
required=False,
id_prefix="",
)

assert 'id="debug"' in html
assert 'href="#debug"' in html


def test_metadata_uses_definition_list() -> None:
"""Verify metadata renders as definition list, not inline paragraph."""
html = render_argument_to_html(
names=["--format"],
metavar=None,
help_text="Output format",
default="json",
type_name="str",
required=False,
id_prefix="",
)

assert '<dl class="argparse-argument-meta">' in html
assert '<dt class="argparse-meta-key">Default</dt>' in html
assert '<dd class="argparse-meta-value"><span class="nv">json</span></dd>' in html
assert '<dt class="argparse-meta-key">Type</dt>' in html
assert '<dd class="argparse-meta-value"><span class="nv">str</span></dd>' in html


def test_required_renders_as_tag() -> None:
"""Verify Required renders as standalone tag, not key-value."""
html = render_argument_to_html(
names=["--config"],
metavar="FILE",
help_text="Config file",
default=None,
type_name=None,
required=True,
id_prefix="",
)

assert '<dt class="argparse-meta-tag">Required</dt>' in html
# Should NOT have a matching dd for Required
assert 'argparse-meta-value">Required' not in html