diff --git a/documentation/_ext/link_issues.py b/documentation/_ext/link_issues.py index 30ce6854b53..6e078da7690 100644 --- a/documentation/_ext/link_issues.py +++ b/documentation/_ext/link_issues.py @@ -102,8 +102,10 @@ def apply(self) -> None: tracker_config = TrackerConfig.from_sphinx_config(config) issue_pattern = config.issuetracker_issue_pattern title_template = None + if isinstance(issue_pattern, str): issue_pattern = re.compile(issue_pattern) + for node in self.document.traverse(nodes.Text): parent = node.parent if isinstance(parent, (nodes.literal, nodes.FixedTextElement)): @@ -111,6 +113,7 @@ def apply(self) -> None: continue if isinstance(parent, nodes.reference): continue + text = str(node) new_nodes = [] last_issue_ref_end = 0 @@ -121,11 +124,13 @@ def apply(self) -> None: "issuetracker_issue_pattern must have " "exactly one group: {!r}".format(match.groups()) ) + # extract the text between the last issue reference and the # current issue reference and put it into a new text node head = text[last_issue_ref_end : match.start()] if head: new_nodes.append(nodes.Text(head)) + # adjust the position of the last issue reference in the # text last_issue_ref_end = match.end() @@ -366,7 +371,6 @@ def connect_builtin_tracker(app: Sphinx) -> None: def setup(app: Sphinx) -> t.Dict[str, t.Any]: - app.add_config_value("mybase", "https://github.com/cihai/unihan-etl", "env") app.add_event("issuetracker-lookup-issue") app.connect("builder-inited", connect_builtin_tracker) app.add_config_value("issuetracker", None, "env") diff --git a/documentation/_ext/link_usernames.py b/documentation/_ext/link_usernames.py index b5ec3e5e1c3..4ed01b89c50 100644 --- a/documentation/_ext/link_usernames.py +++ b/documentation/_ext/link_usernames.py @@ -4,47 +4,115 @@ and: https://stackoverflow.com/a/31924901/3277713 CC BY-SA 3.0 by C_Z_ This extension replaces username references of the form `@username` with a link -to the user's GitHub profile. In order to prevent adding references to existing -existing links (e.g. `[@username](...)`, whitespace is required in front of the `@`. +to the user's GitHub profile. -The GITHUB_IGNORE_USERNAMES set contains usernames that should not be linked but may -appear to be usernames in the documentation. +The plugin ignores code inside of fixed text blocks, including code blocks and backticks. """ import re +from docutils import nodes from sphinx.application import Sphinx +from sphinx.transforms import SphinxTransform from sphinx.util import logging logger = logging.getLogger(__name__) # Format to use for the link to the GitHub profile -GITHUB_USERNAME_TEMPLATE = "https://github.com/{}" +GITHUB_USERNAME_TEMPLATE = "https://github.com/{username}" +# Format to use for team mentions +GITHUB_TEAM_TEMPLATE = "https://github.com/orgs/{org}/teams/{team}" + # Regex to match username references GITHUB_USERNAME_REGEX = re.compile( r""" -\ # Required initial whitespace -@([A-Za-z0-9-]+) # Match @ then any alphanumeric character or - for the username +@(?P<username>[A-Za-z0-9-]+) # Match @ then any alphanumeric character or - for the username +(?P<team>/[A-Za-z0-9-]+)? # If this is a team reference, match the team separately """, flags=re.VERBOSE, ) -GITHUB_IGNORE_USERNAMES = {"todo", "WordPress", "username", "defaultValue"} -def _replace_username(match: re.Match) -> str: - username = match.group(1) - if username in GITHUB_IGNORE_USERNAMES: - logger.debug(f"Ignoring username reference: {username}") - return match.group(0) - logger.debug(f"Replacing username reference: {username}") - return f" [@{username}]({GITHUB_USERNAME_TEMPLATE.format(username)})" +class GitHubUserMentions(SphinxTransform): + default_priority = 999 + + def apply(self) -> None: + for node in self.document.findall(nodes.Text): + parent = node.parent + + # ignore existing links, back ticks, and code blocks + ignore_types = (nodes.reference, nodes.literal, nodes.FixedTextElement) + if isinstance(parent, ignore_types): + continue + + text = str(node) + new_nodes = [] + prev_mention_node_ref = 0 + for match in GITHUB_USERNAME_REGEX.finditer(text): + # The full match including the leading @ + mention = match.group(0) + + username = match.group("username") + + # `team` will be None if not a team mention + # If it is a team mention, then `username` will be the org + team = match.group("team") + + # extract the text between the last user mention and the + # current user mention and put it into a new text node + head = text[prev_mention_node_ref : match.start()] + if head: + new_nodes.append(nodes.Text(head)) + + # adjust the position of the last user mention in the + # text + prev_mention_node_ref = match.end() + + textnode = nodes.Text(mention) + refnode = nodes.reference() + + if team: + url = GITHUB_TEAM_TEMPLATE.format( + org=username, + team=team.lstrip("/"), + ) + title = f"Team {username}{team} on GitHub" + else: + url = GITHUB_USERNAME_TEMPLATE.format( + username=username, + ) + title = f"{username} on GitHub" + + refnode["refuri"] = url + refnode["reftitle"] = title + refnode.append(textnode) + + new_nodes.append(refnode) + + if not new_nodes: + # no user mentions were found, move on to the next node + continue + + # extract the remaining text after the last user mention, and + # put it into a text node + tail = text[prev_mention_node_ref:] + if tail: + new_nodes.append(nodes.Text(tail)) + # find and remove the original node, and insert all new nodes + # instead + parent.replace(node, new_nodes) -def replace_username_references(app, docname, source): - logger.debug(f"In file: {docname}") - source[0] = GITHUB_USERNAME_REGEX.sub(_replace_username, source[0]) +def init_transformer(app: Sphinx) -> None: + if app.config.githubusermention: + app.add_transform(GitHubUserMentions) def setup(app: Sphinx): - # Hook for running the replace username references when the source file is read - app.connect("source-read", replace_username_references) + app.add_config_value("githubusermention", None, "env") + app.connect("builder-inited", init_transformer) + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/documentation/conf.py b/documentation/conf.py index db3aad819cb..fcfa4005da6 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -78,6 +78,7 @@ def add_ext_to_path(): issuetracker = "github" issuetracker_project = "WordPress/openverse" +githubusermention = True # The default for this is a sensible one for ReadTheDocs but # our site is served directly at the root docs.openverse.org URL