From 1a8afd4edca8795990faf25e0a722dc9a16441a1 Mon Sep 17 00:00:00 2001 From: Brendan Heberlein Date: Tue, 21 Feb 2023 10:14:07 -0600 Subject: [PATCH] Override generation of `id` & `href` attributes for API documentation This will replace dots with underscores in qualified names of Python modules & functions (e.g. `mypackage.mymodule.myfunc`), allowing ScrollSpy to identify these tags as navigation targets. Addresses #1207 and fixes #1026 --- src/pydata_sphinx_theme/translator.py | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/pydata_sphinx_theme/translator.py b/src/pydata_sphinx_theme/translator.py index 3c9ef46a9..33381a165 100644 --- a/src/pydata_sphinx_theme/translator.py +++ b/src/pydata_sphinx_theme/translator.py @@ -1,11 +1,13 @@ """ A custom Sphinx HTML Translator for Bootstrap layout """ +from functools import partial from packaging.version import Version import sphinx from sphinx.util import logging from sphinx.ext.autosummary import autosummary_table +from docutils.nodes import Element logger = logging.getLogger(__name__) @@ -58,3 +60,72 @@ def visit_table(self, node): tag = self.starttag(node, "table", CLASS=" ".join(classes), **atts) self.body.append(tag) + + def visit_section(self, node): + """Handle section nodes, parsing ``ids`` to replace dots with underscores. + + This will modify the ``id`` of HTML ``
`` tags, e.g. where Python modules are documented. + Replacing dots with underscores allows the tags to be recognized as navigation targets by ScrollSpy. + """ + if "ids" in node: + node["ids"] = [id_.replace(".", "_") for id_ in node["ids"]] + super().visit_section(node) + + def visit_desc_signature(self, node): + """Handle function & method signature nodes, parsing ``ids`` to replace dots with underscores. + + This will modify the ``id`` attribute of HTML ``
`` & ``
`` tags, where Python functions are documented. + Replacing dots with underscores allows the tags to be recognized as navigation targets by ScrollSpy. + """ + if "ids" in node: + ids = node["ids"] + for i, id_ in enumerate(ids): + ids[i] = id_.replace(".", "_") + super().visit_desc_signature(node) + + def visit_reference(self, node): + """Handle reference nodes, parsing ``refuri`` and ``anchorname`` attributes to replace dots with underscores. + + This will modify the ``href`` attribute of internal HTML ```` tags, e.g. the sidebar navigation links. + """ + try: + # We are only interested in internal anchor references + internal, anchorname = node["internal"], node["anchorname"] + if internal and anchorname.startswith("#") and "." in anchorname: + # Get the root node of the current document + document = self.builder.env.get_doctree(self.builder.current_docname) + + # Get the target anchor ID + target_id = anchorname.lstrip("#") + sanitized_id = target_id.replace(".", "_") + # Update the node `href` + node["refuri"] = node["anchorname"] = "#" + sanitized_id + + # Define a search condition to find the target node by ID + def find_target(search_id, node): + return ( + isinstance(node, Element) + and ("ids" in node) + and (search_id in node["ids"]) + ) + + # NOTE: Replacing with underscores creates the possibility for conflicting references + # We should check for these and warn the user if any are found + if any(document.traverse(condition=partial(find_target, sanitized_id))): + logger.warning( + f'Sanitized reference "{sanitized_id}" for "{target_id}" conflicts with an existing reference!' + ) + + # Find nodes with the given ID (there should only be one) + targets = document.traverse(condition=partial(find_target, target_id)) + # Replace dots with underscores in the target node ID + for target in targets: + # NOTE: By itself, modifying the target `ids` here seems to be insufficient, however it helps + # ensure that the reference `refuri` and target `ids` remain consistent during the build process + target["ids"] = [ + sanitized_id if id_ == target_id else id_ + for id_ in target["ids"] + ] + except KeyError: + pass + super().visit_reference(node)