Skip to content

Commit

Permalink
Use more flexible implementation for user mention link documentation …
Browse files Browse the repository at this point in the history
…extension (#3543)

* Add whitespace and remove unnecessary configuration

* Use more flexible username link extension implementation
  • Loading branch information
sarayourfriend authored Dec 19, 2023
1 parent d8a60ad commit 62807d3
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 21 deletions.
6 changes: 5 additions & 1 deletion documentation/_ext/link_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,18 @@ 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)):
# ignore inline and block literal text
continue
if isinstance(parent, nodes.reference):
continue

text = str(node)
new_nodes = []
last_issue_ref_end = 0
Expand All @@ -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()
Expand Down Expand Up @@ -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")
Expand Down
108 changes: 88 additions & 20 deletions documentation/_ext/link_usernames.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
1 change: 1 addition & 0 deletions documentation/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 62807d3

Please sign in to comment.