import re from docutils import nodes from sphinx.transforms.post_transforms import SphinxPostTransform from sphinx_design.dropdown import dropdown_main, dropdown_title class DropdownAnchorAdder(SphinxPostTransform): """Insert anchor links to the sphinx-design dropdowns. Some of the dropdowns were originally headers that had automatic anchors, so we need to make sure that the old anchors still work. See the original implementation (in JS): https://github.com/scikit-learn/scikit-learn/pull/27409 The structure of each sphinx-design dropdown node is expected to be: ...icon <-- This exists if the "icon" option of the sphinx-design dropdown is set; we do not use it in our documentation ...title <-- This may contain multiple nodes, e.g. literal nodes if there are inline codes; we use the concatenated text of all these nodes to generate the anchor ID Here we insert the anchor link! <-- The "dropdown closed" marker <-- The "dropdown open" marker ...main contents """ default_priority = 9999 # Apply later than everything else formats = ["html"] def run(self): """Run the post transformation.""" # Counter to store the duplicated summary text to add it as a suffix in the # anchor ID anchor_id_counters = {} for sd_dropdown in self.document.findall(dropdown_main): # Grab the dropdown title sd_dropdown_title = sd_dropdown.next_node(dropdown_title) # Concatenate the text of relevant nodes as the title text # Since we do not have the prefix icon, the relevant nodes are the very # first child node until the third last node (last two are markers) title_text = "".join( node.astext() for node in sd_dropdown_title.children[:-2] ) # The ID uses the first line, lowercased, with spaces replaced by dashes; # suffix the anchor ID with a counter if it already exists anchor_id = re.sub(r"\s+", "-", title_text.strip().split("\n")[0]).lower() if anchor_id in anchor_id_counters: anchor_id_counters[anchor_id] += 1 anchor_id = f"{anchor_id}-{anchor_id_counters[anchor_id]}" else: anchor_id_counters[anchor_id] = 1 sd_dropdown["ids"].append(anchor_id) # Create the anchor element and insert after the title text; we do this # directly with raw HTML anchor_html = ( f'#' ) anchor_node = nodes.raw("", anchor_html, format="html") sd_dropdown_title.insert(-2, anchor_node) # before the two markers def setup(app): app.add_post_transform(DropdownAnchorAdder)