diff --git a/src/pydata_sphinx_theme/translator.py b/src/pydata_sphinx_theme/translator.py index eb2046bd4..caed17565 100644 --- a/src/pydata_sphinx_theme/translator.py +++ b/src/pydata_sphinx_theme/translator.py @@ -1,8 +1,10 @@ """A custom Sphinx HTML Translator for Bootstrap layout.""" import types +from functools import partial import sphinx +from docutils.nodes import Element from packaging.version import Version from sphinx.application import Sphinx from sphinx.ext.autosummary import autosummary_table @@ -59,6 +61,86 @@ def visit_table(self, node): tag = self.starttag(node, "table", CLASS=" ".join(classes), **atts) self.body.append(tag) + # NOTE: `visit_section`, `visit_desc_signature` & `visit_reference` are extended + # here to resolve #1026 & #1207. There is an open issue with Sphinx to address this: + # https://github.com/sphinx-doc/sphinx/issues/11208 + # If the issue is resolved within Sphinx, these methods can be removed. + + def visit_section(self, node): + """Handle section nodes to replace dots with underscores. + + This will modify the ``id`` of HTML ``
`` tags, 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 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 to replace dots with underscores. + + This will modify the ``href`` attribute of any 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) + def setup_translators(app: Sphinx): """Add bootstrap HTML functionality if we are using an HTML translator.