from sphinx.util.docutils import SphinxDirective, directives, nodes
from pathlib import Path
from .utils import kv_to_data_attr, normalize_key, logger
class SelectorGroup(nodes.General, nodes.Element):
"""
A row or dropdown within a selector container.
"""
@staticmethod
def visit_html(translator, node):
label = node["label"]
key = node["key"]
show_when_attr = kv_to_data_attr("show-when", node["show-when"])
heading_width = node["heading-width"]
list_mode = node.get("list", False)
# Standard tile mode
info_nodes = list(node.findall(SelectorInfo))
info_link = info_nodes[0]["link"] if info_nodes else None
info_icon = info_nodes[0]["icon"] if info_nodes else None
info_icon_html = ""
if info_link:
info_icon_html = f"""
"""
translator.body.append(
""
f"""
{label}{info_icon_html}
{f'" if list_mode else ""}
"""
""
)
class SelectorGroupDirective(SphinxDirective):
required_arguments = 1 # title text
final_argument_whitespace = True
has_content = True
option_spec = {
"key": directives.unchanged,
"show-when": directives.unchanged,
"heading-width": directives.nonnegative_int,
"list": directives.flag,
}
def run(self):
env = self.state.document.settings.env
app = env.app
# Add required JS and CSS if selector exists
if not hasattr(env, '_selector_js_added'):
static_assets_dir = Path(__file__).parent / "static" / "selector"
app.config.html_static_path.append(str(static_assets_dir))
app.add_js_file("selector.js", type="module", defer="defer")
app.add_css_file("selector.css")
env._selector_js_added = True
label = self.arguments[0]
node = SelectorGroup()
node["label"] = label
node["key"] = normalize_key(self.options.get("key", label))
node["show-when"] = self.options.get("show-when", "")
node["heading-width"] = self.options.get("heading-width", 3)
node["list"] = "list" in self.options
# Parse nested content (selector-info + selector-option)
self.state.nested_parse(self.content, self.content_offset, node)
option_nodes = list(node.findall(SelectorOption))
if option_nodes:
for opt in option_nodes:
opt["group_key"] = node["key"]
opt["list"] = node["list"]
# Default marking
default_options = [opt for opt in option_nodes if opt["default"]]
if not default_options:
option_nodes[0]["default"] = True
return [node]
class SelectorInfo(nodes.General, nodes.Element):
"""
Represents an informational icon/link associated with a selector group.
Appears as a clickable icon in the selector group heading.
rST usage:
.. selector:: AMD EPYC Server CPU
:key: cpu
.. selector-info:: https://www.amd.com/en/products/processors/server/epyc.html
:icon: fa-solid fa-circle-info fa-lg
.. selector-option:: EPYC 9005 (5th gen.)
:value: 9005
"""
@staticmethod
def visit_html(translator, node):
# Do nothing — rendering handled by SelectorGroup
pass
@staticmethod
def depart_html(translator, node):
# Do nothing — prevent NotImplementedError
pass
class SelectorInfoDirective(SphinxDirective):
required_arguments = 1 # link URL
final_argument_whitespace = True
has_content = False
option_spec = {"icon": directives.unchanged}
def run(self):
node = SelectorInfo()
node["link"] = self.arguments[0]
node["icon"] = self.options.get("icon", "fa-solid fa-circle-info fa-lg")
parent = getattr(self.state, "parent", None)
if not parent or not any(isinstance(p, SelectorGroup) for p in parent.traverse(include_self=True)):
logger.warning(
f"'.. selector-info::' at line {self.lineno} should be nested under a '.. selector::' directive",
location=(self.env.docname, self.lineno),
)
return [node]
class SelectorOption(nodes.General, nodes.Element):
"""
A selectable tile or list-item option within a selector group.
"""
@staticmethod
def visit_html(translator, node):
label = node["label"]
value = node["value"]
show_when_attr = kv_to_data_attr("show-when", node["show-when"])
disable_when_attr = kv_to_data_attr("disable-when", node["disable-when"])
default = node["default"]
width = node["width"]
list_mode = node.get("list", False)
if list_mode:
selected_attr = " selected" if default else ""
translator.body.append(
f''
)
return
default_class = "rocm-docs-selector-option-default" if default else ""
# Handle width: either bootstrap col-N or percentage
if isinstance(width, str) and width.endswith("%"):
width_class = ""
width_style = f' style="width: {width}"'
else:
width_class = f"col-{width}"
width_style = ""
translator.body.append(
""
f"""
{label}
""".strip()
)
@staticmethod
def depart_html(translator, node):
list_mode = node.get("list", False)
if list_mode:
return # no closing tag needed for
")
class SelectorOptionDirective(SphinxDirective):
required_arguments = 1 # text of tile
final_argument_whitespace = True
option_spec = {
"value": directives.unchanged,
"show-when": directives.unchanged,
"disable-when": directives.unchanged,
"default": directives.flag,
"width": directives.unchanged,
"icon": directives.unchanged,
}
has_content = True
def run(self):
label = self.arguments[0]
node = SelectorOption()
node["label"] = label
node["value"] = normalize_key(self.options.get("value", label))
node["show-when"] = self.options.get("show-when", "")
node["disable-when"] = self.options.get("disable-when", "")
node["default"] = self.options.get("default", False) is not False
# Parse width - supports bootstrap (1-12) or percentage (like "25%")
width_value = self.options.get("width", "6")
if isinstance(width_value, str) and width_value.endswith("%"):
try:
pct = float(width_value[:-1])
if pct <= 0 or pct > 100:
raise ValueError("must be between 0 and 100")
node["width"] = width_value
except ValueError as e:
logger.warning(
f"Invalid percentage width '{width_value}' ({e}), using default",
location=(self.env.docname, self.lineno),
)
node["width"] = 6
else:
try:
col_num = int(width_value)
if col_num < 1 or col_num > 12:
raise ValueError("must be between 1 and 12")
node["width"] = col_num
except ValueError as e:
logger.warning(
f"Invalid width '{width_value}' ({e}), using default",
location=(self.env.docname, self.lineno),
)
node["width"] = 6
node["icon"] = self.options.get("icon")
parent = getattr(self.state, "parent", None)
if not parent or not any(isinstance(p, SelectorGroup) for p in parent.traverse(include_self=True)):
logger.warning(
f"'.. selector-option::' at line {self.lineno} should be nested under a '.. selector::' directive",
location=(self.env.docname, self.lineno),
)
return [node]
class SelectedContent(nodes.General, nodes.Element):
"""
A container to hold documentation content to be shown conditionally.
rST usage::
.. selected-content:: os=ubuntu
:heading: Ubuntu Notes
"""
@staticmethod
def visit_html(translator, node):
show_when = node.get("show-when", "")
show_when_attr = kv_to_data_attr("show-when", show_when)
classes = " ".join(node.get("class", []))
heading = node.get("heading", "")
# default to
heading_level = min(node.get("heading-level") or 2, 6) # maximum depth is