================
@@ -1,19 +1,86 @@
#!/usr/bin/env python3
-from sphinx.application import Sphinx
+"""Script to link #GH123 to our github issue tracker.
+
+We use MyST and reST, so this Sphinx plugin operates on the shared doctree
+representation of the documentation, which is effectively a documentation-AST.
+
+Docutils doctree node reference:
+https://docutils.sourceforge.io/docs/ref/doctree.html
+"""
+
import re
+from docutils import nodes
+from sphinx.application import Sphinx
__version__ = "1.0"
+GH_LINK_RE = re.compile("#GH([0-9]+)")
+GH_LINK_URL = "https://github.com/llvm/llvm-project/issues/{}"
+SKIP_NODES: tuple[type[nodes.Node], ...] = (
+ nodes.FixedTextElement,
+ nodes.literal,
+ nodes.raw,
+ nodes.reference,
+)
+
+
+def make_gh_link(issue: str) -> nodes.reference:
+ """Create the docutils node that writers render as an external link."""
+ return nodes.reference("", "#" + issue, refuri=GH_LINK_URL.format(issue))
+
+
+def replace_gh_links(node: nodes.Text) -> None:
+ """Replace one text node with text fragments and GitHub issue links.
+
+ docutils text nodes cannot contain child nodes, so a single string like
+ "See #GH123." has to become a sibling list: Text("See "), reference(...),
+ Text("."). The parent node owns that sibling list.
+ """
+ remaining = str(node)
+ replacements = []
+
+ while GH_LINK_RE.search(remaining):
+ before, issue, remaining = GH_LINK_RE.split(remaining, maxsplit=1)
+ if before:
+ replacements.append(nodes.Text(before))
+ replacements.append(make_gh_link(issue))
+
+ # If we found matches, do the replacement.
+ if replacements:
+ if remaining:
+ replacements.append(nodes.Text(remaining))
+ node.parent.replace(node, replacements)
+
+
+def replace_gh_links_in_subtree(node: nodes.Node) -> None:
+ """Rewrite linkable text nodes under node, pruning ignored subtrees.
+
+ The doctree is the markup-language neutral documentation AST, so it
+ handles both reST and MyST (markdown). It helps us avoid rewriting #GH123
+ in code and literal blocks.
+ """
+ # DFS cutoff for code blocks etc.
+ if isinstance(node, SKIP_NODES):
+ return
+
+ # Find #GH123 links in text blocks and linkify them.
+ if isinstance(node, nodes.Text):
+ if GH_LINK_RE.search(str(node)):
+ replace_gh_links(node)
+ return
+
+ # Recursive DFS traversal of children.
+ if isinstance(node, nodes.Element):
+ for child in list(node.children):
+ replace_gh_links_in_subtree(child)
+
-def subst_gh_links(app: Sphinx, docname, source):
- regex = re.compile("#GH([0-9]+)")
- out_pattern = r"`#\1 <https://github.com/llvm/llvm-project/issues/\1>`_"
- result = source[0]
- result = regex.sub(out_pattern, result)
- source[0] = result
+def subst_gh_links(_app: Sphinx, doctree: nodes.document) -> None:
----------------
nigham wrote:
Would be nice to add tests, even basic ones that LLM can add.
Also: how are we testing this works, is it via building the docs and verifying links are substituted correctly?
https://github.com/llvm/llvm-project/pull/199076