We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Link topic page(s) in the rendered topic header
As a followup to #2579 implementing #2572 , it would be very useful to have the `Topic` header in the rendered PEPs actually link to said topic(s), and would be easy to implement (so long as we keep the display names consistent with the URLs, minus caps/whitespace).

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `pep_sphinx_extensions/pep_processor/transforms/pep_headers.py`
Content:
```
1 from pathlib import Path
2 import re
3 
4 from docutils import nodes
5 from docutils import transforms
6 from sphinx import errors
7 
8 from pep_sphinx_extensions.pep_processor.transforms import pep_zero
9 from pep_sphinx_extensions.pep_processor.transforms.pep_zero import _mask_email
10 
11 
12 class PEPParsingError(errors.SphinxError):
13     pass
14 
15 
16 # PEPHeaders is identical to docutils.transforms.peps.Headers excepting bdfl-delegate, sponsor & superseeded-by
17 class PEPHeaders(transforms.Transform):
18     """Process fields in a PEP's initial RFC-2822 header."""
19 
20     # Run before pep_processor.transforms.pep_title.PEPTitle
21     default_priority = 330
22 
23     def apply(self) -> None:
24         if not Path(self.document["source"]).match("pep-*"):
25             return  # not a PEP file, exit early
26 
27         if not len(self.document):
28             raise PEPParsingError("Document tree is empty.")
29 
30         header = self.document[0]
31         if not isinstance(header, nodes.field_list) or "rfc2822" not in header["classes"]:
32             raise PEPParsingError("Document does not begin with an RFC-2822 header; it is not a PEP.")
33 
34         # PEP number should be the first field
35         pep_field = header[0]
36         if pep_field[0].astext().lower() != "pep":
37             raise PEPParsingError("Document does not contain an RFC-2822 'PEP' header!")
38 
39         # Extract PEP number
40         value = pep_field[1].astext()
41         try:
42             pep_num = int(value)
43         except ValueError:
44             raise PEPParsingError(f"'PEP' header must contain an integer. '{value}' is invalid!")
45 
46         # Special processing for PEP 0.
47         if pep_num == 0:
48             pending = nodes.pending(pep_zero.PEPZero)
49             self.document.insert(1, pending)
50             self.document.note_pending(pending)
51 
52         # If there are less than two headers in the preamble, or if Title is absent
53         if len(header) < 2 or header[1][0].astext().lower() != "title":
54             raise PEPParsingError("No title!")
55 
56         fields_to_remove = []
57         for field in header:
58             name = field[0].astext().lower()
59             body = field[1]
60             if len(body) == 0:
61                 # body is empty
62                 continue
63             elif len(body) > 1:
64                 msg = f"PEP header field body contains multiple elements:\n{field.pformat(level=1)}"
65                 raise PEPParsingError(msg)
66             elif not isinstance(body[0], nodes.paragraph):  # len(body) == 1
67                 msg = f"PEP header field body may only contain a single paragraph:\n{field.pformat(level=1)}"
68                 raise PEPParsingError(msg)
69 
70             para = body[0]
71             if name in {"author", "bdfl-delegate", "pep-delegate", "sponsor"}:
72                 # mask emails
73                 for node in para:
74                     if not isinstance(node, nodes.reference):
75                         continue
76                     node.replace_self(_mask_email(node))
77             elif name in {"discussions-to", "resolution", "post-history"}:
78                 # Prettify mailing list and Discourse links
79                 for node in para:
80                     if (not isinstance(node, nodes.reference)
81                             or not node["refuri"]):
82                         continue
83                     # Have known mailto links link to their main list pages
84                     if node["refuri"].lower().startswith("mailto:"):
85                         node["refuri"] = _generate_list_url(node["refuri"])
86                     parts = node["refuri"].lower().split("/")
87                     if len(parts) <= 2 or parts[2] not in LINK_PRETTIFIERS:
88                         continue
89                     pretty_title = _make_link_pretty(str(node["refuri"]))
90                     if name == "post-history":
91                         node["reftitle"] = pretty_title
92                     else:
93                         node[0] = nodes.Text(pretty_title)
94             elif name in {"replaces", "superseded-by", "requires"}:
95                 # replace PEP numbers with normalised list of links to PEPs
96                 new_body = []
97                 for pep_str in re.split(r",?\s+", body.astext()):
98                     target = self.document.settings.pep_url.format(int(pep_str))
99                     new_body += [nodes.reference("", pep_str, refuri=target), nodes.Text(", ")]
100                 para[:] = new_body[:-1]  # drop trailing space
101             elif name in {"last-modified", "content-type", "version"}:
102                 # Mark unneeded fields
103                 fields_to_remove.append(field)
104 
105             # Remove any trailing commas and whitespace in the headers
106             if para and isinstance(para[-1], nodes.Text):
107                 last_node = para[-1]
108                 if last_node.astext().strip() == ",":
109                     last_node.parent.remove(last_node)
110                 else:
111                     para[-1] = last_node.rstrip().rstrip(",")
112 
113         # Remove unneeded fields
114         for field in fields_to_remove:
115             field.parent.remove(field)
116 
117 
118 def _generate_list_url(mailto: str) -> str:
119     list_name_domain = mailto.lower().removeprefix("mailto:").strip()
120     list_name = list_name_domain.split("@")[0]
121 
122     if list_name_domain.endswith("@googlegroups.com"):
123         return f"https://groups.google.com/g/{list_name}"
124 
125     if not list_name_domain.endswith("@python.org"):
126         return mailto
127 
128     # Active lists not yet on Mailman3; this URL will redirect if/when they are
129     if list_name in {"csv", "db-sig", "doc-sig", "python-list", "web-sig"}:
130         return f"https://mail.python.org/mailman/listinfo/{list_name}"
131     # Retired lists that are closed for posting, so only the archive matters
132     if list_name in {"import-sig", "python-3000"}:
133         return f"https://mail.python.org/pipermail/{list_name}/"
134     # The remaining lists (and any new ones) are all on Mailman3/Hyperkitty
135     return f"https://mail.python.org/archives/list/{list_name}@python.org/"
136 
137 
138 def _process_list_url(parts: list[str]) -> tuple[str, str]:
139     item_type = "list"
140 
141     # HyperKitty (Mailman3) archive structure is
142     # https://mail.python.org/archives/list/<list_name>/thread/<id>
143     if "archives" in parts:
144         list_name = (
145             parts[parts.index("archives") + 2].removesuffix("@python.org"))
146         if len(parts) > 6 and parts[6] in {"message", "thread"}:
147             item_type = parts[6]
148 
149     # Mailman3 list info structure is
150     # https://mail.python.org/mailman3/lists/<list_name>.python.org/
151     elif "mailman3" in parts:
152         list_name = (
153             parts[parts.index("mailman3") + 2].removesuffix(".python.org"))
154 
155     # Pipermail (Mailman) archive structure is
156     # https://mail.python.org/pipermail/<list_name>/<month>-<year>/<id>
157     elif "pipermail" in parts:
158         list_name = parts[parts.index("pipermail") + 1]
159         item_type = "message" if len(parts) > 6 else "list"
160 
161     # Mailman listinfo structure is
162     # https://mail.python.org/mailman/listinfo/<list_name>
163     elif "listinfo" in parts:
164         list_name = parts[parts.index("listinfo") + 1]
165 
166     # Not a link to a mailing list, message or thread
167     else:
168         raise ValueError(
169             f"{'/'.join(parts)} not a link to a list, message or thread")
170 
171     return list_name, item_type
172 
173 
174 def _process_discourse_url(parts: list[str]) -> tuple[str, str]:
175     item_name = "discourse"
176 
177     if len(parts) < 5 or ("t" not in parts and "c" not in parts):
178         raise ValueError(
179             f"{'/'.join(parts)} not a link to a Discourse thread or category")
180 
181     first_subpart = parts[4]
182     has_title = not first_subpart.isnumeric()
183 
184     if "t" in parts:
185         item_type = "post" if len(parts) > (5 + has_title) else "thread"
186     elif "c" in parts:
187         item_type = "category"
188         if has_title:
189             item_name = f"{first_subpart.replace('-', ' ')} {item_name}"
190 
191     return item_name, item_type
192 
193 
194 # Domains supported for pretty URL parsing
195 LINK_PRETTIFIERS = {
196     "mail.python.org": _process_list_url,
197     "discuss.python.org": _process_discourse_url,
198 }
199 
200 
201 def _process_pretty_url(url: str) -> tuple[str, str]:
202     parts = url.lower().strip().strip("/").split("/")
203     try:
204         item_name, item_type = LINK_PRETTIFIERS[parts[2]](parts)
205     except KeyError as error:
206         raise ValueError(
207             f"{url} not a link to a recognized domain to prettify") from error
208     item_name = item_name.title().replace("Sig", "SIG").replace("Pep", "PEP")
209     return item_name, item_type
210 
211 
212 def _make_link_pretty(url: str) -> str:
213     item_name, item_type = _process_pretty_url(url)
214     return f"{item_name} {item_type}"
215
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py
--- a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py
+++ b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py
@@ -98,6 +98,17 @@
                     target = self.document.settings.pep_url.format(int(pep_str))
                     new_body += [nodes.reference("", pep_str, refuri=target), nodes.Text(", ")]
                 para[:] = new_body[:-1]  # drop trailing space
+            elif name == "topic":
+                new_body = []
+                for topic_name in body.astext().split(","):
+                    if topic_name:
+                        target = f"/topic/{topic_name.lower().strip()}/"
+                        new_body += [
+                            nodes.reference("", topic_name, refuri=target),
+                            nodes.Text(", "),
+                        ]
+                if new_body:
+                    para[:] = new_body[:-1]  # Drop trailing space/comma
             elif name in {"last-modified", "content-type", "version"}:
                 # Mark unneeded fields
                 fields_to_remove.append(field)

{"golden_diff": "diff --git a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py\n--- a/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py\n+++ b/pep_sphinx_extensions/pep_processor/transforms/pep_headers.py\n@@ -98,6 +98,17 @@\n                     target = self.document.settings.pep_url.format(int(pep_str))\n                     new_body += [nodes.reference(\"\", pep_str, refuri=target), nodes.Text(\", \")]\n                 para[:] = new_body[:-1]  # drop trailing space\n+            elif name == \"topic\":\n+                new_body = []\n+                for topic_name in body.astext().split(\",\"):\n+                    if topic_name:\n+                        target = f\"/topic/{topic_name.lower().strip()}/\"\n+                        new_body += [\n+                            nodes.reference(\"\", topic_name, refuri=target),\n+                            nodes.Text(\", \"),\n+                        ]\n+                if new_body:\n+                    para[:] = new_body[:-1]  # Drop trailing space/comma\n             elif name in {\"last-modified\", \"content-type\", \"version\"}:\n                 # Mark unneeded fields\n                 fields_to_remove.append(field)\n", "issue": "Link topic page(s) in the rendered topic header\nAs a followup to #2579 implementing #2572 , it would be very useful to have the `Topic` header in the rendered PEPs actually link to said topic(s), and would be easy to implement (so long as we keep the display names consistent with the URLs, minus caps/whitespace).\n", "before_files": [{"content": "from pathlib import Path\nimport re\n\nfrom docutils import nodes\nfrom docutils import transforms\nfrom sphinx import errors\n\nfrom pep_sphinx_extensions.pep_processor.transforms import pep_zero\nfrom pep_sphinx_extensions.pep_processor.transforms.pep_zero import _mask_email\n\n\nclass PEPParsingError(errors.SphinxError):\n    pass\n\n\n# PEPHeaders is identical to docutils.transforms.peps.Headers excepting bdfl-delegate, sponsor & superseeded-by\nclass PEPHeaders(transforms.Transform):\n    \"\"\"Process fields in a PEP's initial RFC-2822 header.\"\"\"\n\n    # Run before pep_processor.transforms.pep_title.PEPTitle\n    default_priority = 330\n\n    def apply(self) -> None:\n        if not Path(self.document[\"source\"]).match(\"pep-*\"):\n            return  # not a PEP file, exit early\n\n        if not len(self.document):\n            raise PEPParsingError(\"Document tree is empty.\")\n\n        header = self.document[0]\n        if not isinstance(header, nodes.field_list) or \"rfc2822\" not in header[\"classes\"]:\n            raise PEPParsingError(\"Document does not begin with an RFC-2822 header; it is not a PEP.\")\n\n        # PEP number should be the first field\n        pep_field = header[0]\n        if pep_field[0].astext().lower() != \"pep\":\n            raise PEPParsingError(\"Document does not contain an RFC-2822 'PEP' header!\")\n\n        # Extract PEP number\n        value = pep_field[1].astext()\n        try:\n            pep_num = int(value)\n        except ValueError:\n            raise PEPParsingError(f\"'PEP' header must contain an integer. '{value}' is invalid!\")\n\n        # Special processing for PEP 0.\n        if pep_num == 0:\n            pending = nodes.pending(pep_zero.PEPZero)\n            self.document.insert(1, pending)\n            self.document.note_pending(pending)\n\n        # If there are less than two headers in the preamble, or if Title is absent\n        if len(header) < 2 or header[1][0].astext().lower() != \"title\":\n            raise PEPParsingError(\"No title!\")\n\n        fields_to_remove = []\n        for field in header:\n            name = field[0].astext().lower()\n            body = field[1]\n            if len(body) == 0:\n                # body is empty\n                continue\n            elif len(body) > 1:\n                msg = f\"PEP header field body contains multiple elements:\\n{field.pformat(level=1)}\"\n                raise PEPParsingError(msg)\n            elif not isinstance(body[0], nodes.paragraph):  # len(body) == 1\n                msg = f\"PEP header field body may only contain a single paragraph:\\n{field.pformat(level=1)}\"\n                raise PEPParsingError(msg)\n\n            para = body[0]\n            if name in {\"author\", \"bdfl-delegate\", \"pep-delegate\", \"sponsor\"}:\n                # mask emails\n                for node in para:\n                    if not isinstance(node, nodes.reference):\n                        continue\n                    node.replace_self(_mask_email(node))\n            elif name in {\"discussions-to\", \"resolution\", \"post-history\"}:\n                # Prettify mailing list and Discourse links\n                for node in para:\n                    if (not isinstance(node, nodes.reference)\n                            or not node[\"refuri\"]):\n                        continue\n                    # Have known mailto links link to their main list pages\n                    if node[\"refuri\"].lower().startswith(\"mailto:\"):\n                        node[\"refuri\"] = _generate_list_url(node[\"refuri\"])\n                    parts = node[\"refuri\"].lower().split(\"/\")\n                    if len(parts) <= 2 or parts[2] not in LINK_PRETTIFIERS:\n                        continue\n                    pretty_title = _make_link_pretty(str(node[\"refuri\"]))\n                    if name == \"post-history\":\n                        node[\"reftitle\"] = pretty_title\n                    else:\n                        node[0] = nodes.Text(pretty_title)\n            elif name in {\"replaces\", \"superseded-by\", \"requires\"}:\n                # replace PEP numbers with normalised list of links to PEPs\n                new_body = []\n                for pep_str in re.split(r\",?\\s+\", body.astext()):\n                    target = self.document.settings.pep_url.format(int(pep_str))\n                    new_body += [nodes.reference(\"\", pep_str, refuri=target), nodes.Text(\", \")]\n                para[:] = new_body[:-1]  # drop trailing space\n            elif name in {\"last-modified\", \"content-type\", \"version\"}:\n                # Mark unneeded fields\n                fields_to_remove.append(field)\n\n            # Remove any trailing commas and whitespace in the headers\n            if para and isinstance(para[-1], nodes.Text):\n                last_node = para[-1]\n                if last_node.astext().strip() == \",\":\n                    last_node.parent.remove(last_node)\n                else:\n                    para[-1] = last_node.rstrip().rstrip(\",\")\n\n        # Remove unneeded fields\n        for field in fields_to_remove:\n            field.parent.remove(field)\n\n\ndef _generate_list_url(mailto: str) -> str:\n    list_name_domain = mailto.lower().removeprefix(\"mailto:\").strip()\n    list_name = list_name_domain.split(\"@\")[0]\n\n    if list_name_domain.endswith(\"@googlegroups.com\"):\n        return f\"https://groups.google.com/g/{list_name}\"\n\n    if not list_name_domain.endswith(\"@python.org\"):\n        return mailto\n\n    # Active lists not yet on Mailman3; this URL will redirect if/when they are\n    if list_name in {\"csv\", \"db-sig\", \"doc-sig\", \"python-list\", \"web-sig\"}:\n        return f\"https://mail.python.org/mailman/listinfo/{list_name}\"\n    # Retired lists that are closed for posting, so only the archive matters\n    if list_name in {\"import-sig\", \"python-3000\"}:\n        return f\"https://mail.python.org/pipermail/{list_name}/\"\n    # The remaining lists (and any new ones) are all on Mailman3/Hyperkitty\n    return f\"https://mail.python.org/archives/list/{list_name}@python.org/\"\n\n\ndef _process_list_url(parts: list[str]) -> tuple[str, str]:\n    item_type = \"list\"\n\n    # HyperKitty (Mailman3) archive structure is\n    # https://mail.python.org/archives/list/<list_name>/thread/<id>\n    if \"archives\" in parts:\n        list_name = (\n            parts[parts.index(\"archives\") + 2].removesuffix(\"@python.org\"))\n        if len(parts) > 6 and parts[6] in {\"message\", \"thread\"}:\n            item_type = parts[6]\n\n    # Mailman3 list info structure is\n    # https://mail.python.org/mailman3/lists/<list_name>.python.org/\n    elif \"mailman3\" in parts:\n        list_name = (\n            parts[parts.index(\"mailman3\") + 2].removesuffix(\".python.org\"))\n\n    # Pipermail (Mailman) archive structure is\n    # https://mail.python.org/pipermail/<list_name>/<month>-<year>/<id>\n    elif \"pipermail\" in parts:\n        list_name = parts[parts.index(\"pipermail\") + 1]\n        item_type = \"message\" if len(parts) > 6 else \"list\"\n\n    # Mailman listinfo structure is\n    # https://mail.python.org/mailman/listinfo/<list_name>\n    elif \"listinfo\" in parts:\n        list_name = parts[parts.index(\"listinfo\") + 1]\n\n    # Not a link to a mailing list, message or thread\n    else:\n        raise ValueError(\n            f\"{'/'.join(parts)} not a link to a list, message or thread\")\n\n    return list_name, item_type\n\n\ndef _process_discourse_url(parts: list[str]) -> tuple[str, str]:\n    item_name = \"discourse\"\n\n    if len(parts) < 5 or (\"t\" not in parts and \"c\" not in parts):\n        raise ValueError(\n            f\"{'/'.join(parts)} not a link to a Discourse thread or category\")\n\n    first_subpart = parts[4]\n    has_title = not first_subpart.isnumeric()\n\n    if \"t\" in parts:\n        item_type = \"post\" if len(parts) > (5 + has_title) else \"thread\"\n    elif \"c\" in parts:\n        item_type = \"category\"\n        if has_title:\n            item_name = f\"{first_subpart.replace('-', ' ')} {item_name}\"\n\n    return item_name, item_type\n\n\n# Domains supported for pretty URL parsing\nLINK_PRETTIFIERS = {\n    \"mail.python.org\": _process_list_url,\n    \"discuss.python.org\": _process_discourse_url,\n}\n\n\ndef _process_pretty_url(url: str) -> tuple[str, str]:\n    parts = url.lower().strip().strip(\"/\").split(\"/\")\n    try:\n        item_name, item_type = LINK_PRETTIFIERS[parts[2]](parts)\n    except KeyError as error:\n        raise ValueError(\n            f\"{url} not a link to a recognized domain to prettify\") from error\n    item_name = item_name.title().replace(\"Sig\", \"SIG\").replace(\"Pep\", \"PEP\")\n    return item_name, item_type\n\n\ndef _make_link_pretty(url: str) -> str:\n    item_name, item_type = _process_pretty_url(url)\n    return f\"{item_name} {item_type}\"\n", "path": "pep_sphinx_extensions/pep_processor/transforms/pep_headers.py"}], "after_files": [{"content": "from pathlib import Path\nimport re\n\nfrom docutils import nodes\nfrom docutils import transforms\nfrom sphinx import errors\n\nfrom pep_sphinx_extensions.pep_processor.transforms import pep_zero\nfrom pep_sphinx_extensions.pep_processor.transforms.pep_zero import _mask_email\n\n\nclass PEPParsingError(errors.SphinxError):\n    pass\n\n\n# PEPHeaders is identical to docutils.transforms.peps.Headers excepting bdfl-delegate, sponsor & superseeded-by\nclass PEPHeaders(transforms.Transform):\n    \"\"\"Process fields in a PEP's initial RFC-2822 header.\"\"\"\n\n    # Run before pep_processor.transforms.pep_title.PEPTitle\n    default_priority = 330\n\n    def apply(self) -> None:\n        if not Path(self.document[\"source\"]).match(\"pep-*\"):\n            return  # not a PEP file, exit early\n\n        if not len(self.document):\n            raise PEPParsingError(\"Document tree is empty.\")\n\n        header = self.document[0]\n        if not isinstance(header, nodes.field_list) or \"rfc2822\" not in header[\"classes\"]:\n            raise PEPParsingError(\"Document does not begin with an RFC-2822 header; it is not a PEP.\")\n\n        # PEP number should be the first field\n        pep_field = header[0]\n        if pep_field[0].astext().lower() != \"pep\":\n            raise PEPParsingError(\"Document does not contain an RFC-2822 'PEP' header!\")\n\n        # Extract PEP number\n        value = pep_field[1].astext()\n        try:\n            pep_num = int(value)\n        except ValueError:\n            raise PEPParsingError(f\"'PEP' header must contain an integer. '{value}' is invalid!\")\n\n        # Special processing for PEP 0.\n        if pep_num == 0:\n            pending = nodes.pending(pep_zero.PEPZero)\n            self.document.insert(1, pending)\n            self.document.note_pending(pending)\n\n        # If there are less than two headers in the preamble, or if Title is absent\n        if len(header) < 2 or header[1][0].astext().lower() != \"title\":\n            raise PEPParsingError(\"No title!\")\n\n        fields_to_remove = []\n        for field in header:\n            name = field[0].astext().lower()\n            body = field[1]\n            if len(body) == 0:\n                # body is empty\n                continue\n            elif len(body) > 1:\n                msg = f\"PEP header field body contains multiple elements:\\n{field.pformat(level=1)}\"\n                raise PEPParsingError(msg)\n            elif not isinstance(body[0], nodes.paragraph):  # len(body) == 1\n                msg = f\"PEP header field body may only contain a single paragraph:\\n{field.pformat(level=1)}\"\n                raise PEPParsingError(msg)\n\n            para = body[0]\n            if name in {\"author\", \"bdfl-delegate\", \"pep-delegate\", \"sponsor\"}:\n                # mask emails\n                for node in para:\n                    if not isinstance(node, nodes.reference):\n                        continue\n                    node.replace_self(_mask_email(node))\n            elif name in {\"discussions-to\", \"resolution\", \"post-history\"}:\n                # Prettify mailing list and Discourse links\n                for node in para:\n                    if (not isinstance(node, nodes.reference)\n                            or not node[\"refuri\"]):\n                        continue\n                    # Have known mailto links link to their main list pages\n                    if node[\"refuri\"].lower().startswith(\"mailto:\"):\n                        node[\"refuri\"] = _generate_list_url(node[\"refuri\"])\n                    parts = node[\"refuri\"].lower().split(\"/\")\n                    if len(parts) <= 2 or parts[2] not in LINK_PRETTIFIERS:\n                        continue\n                    pretty_title = _make_link_pretty(str(node[\"refuri\"]))\n                    if name == \"post-history\":\n                        node[\"reftitle\"] = pretty_title\n                    else:\n                        node[0] = nodes.Text(pretty_title)\n            elif name in {\"replaces\", \"superseded-by\", \"requires\"}:\n                # replace PEP numbers with normalised list of links to PEPs\n                new_body = []\n                for pep_str in re.split(r\",?\\s+\", body.astext()):\n                    target = self.document.settings.pep_url.format(int(pep_str))\n                    new_body += [nodes.reference(\"\", pep_str, refuri=target), nodes.Text(\", \")]\n                para[:] = new_body[:-1]  # drop trailing space\n            elif name == \"topic\":\n                new_body = []\n                for topic_name in body.astext().split(\",\"):\n                    if topic_name:\n                        target = f\"/topic/{topic_name.lower().strip()}/\"\n                        new_body += [\n                            nodes.reference(\"\", topic_name, refuri=target),\n                            nodes.Text(\", \"),\n                        ]\n                if new_body:\n                    para[:] = new_body[:-1]  # Drop trailing space/comma\n            elif name in {\"last-modified\", \"content-type\", \"version\"}:\n                # Mark unneeded fields\n                fields_to_remove.append(field)\n\n            # Remove any trailing commas and whitespace in the headers\n            if para and isinstance(para[-1], nodes.Text):\n                last_node = para[-1]\n                if last_node.astext().strip() == \",\":\n                    last_node.parent.remove(last_node)\n                else:\n                    para[-1] = last_node.rstrip().rstrip(\",\")\n\n        # Remove unneeded fields\n        for field in fields_to_remove:\n            field.parent.remove(field)\n\n\ndef _generate_list_url(mailto: str) -> str:\n    list_name_domain = mailto.lower().removeprefix(\"mailto:\").strip()\n    list_name = list_name_domain.split(\"@\")[0]\n\n    if list_name_domain.endswith(\"@googlegroups.com\"):\n        return f\"https://groups.google.com/g/{list_name}\"\n\n    if not list_name_domain.endswith(\"@python.org\"):\n        return mailto\n\n    # Active lists not yet on Mailman3; this URL will redirect if/when they are\n    if list_name in {\"csv\", \"db-sig\", \"doc-sig\", \"python-list\", \"web-sig\"}:\n        return f\"https://mail.python.org/mailman/listinfo/{list_name}\"\n    # Retired lists that are closed for posting, so only the archive matters\n    if list_name in {\"import-sig\", \"python-3000\"}:\n        return f\"https://mail.python.org/pipermail/{list_name}/\"\n    # The remaining lists (and any new ones) are all on Mailman3/Hyperkitty\n    return f\"https://mail.python.org/archives/list/{list_name}@python.org/\"\n\n\ndef _process_list_url(parts: list[str]) -> tuple[str, str]:\n    item_type = \"list\"\n\n    # HyperKitty (Mailman3) archive structure is\n    # https://mail.python.org/archives/list/<list_name>/thread/<id>\n    if \"archives\" in parts:\n        list_name = (\n            parts[parts.index(\"archives\") + 2].removesuffix(\"@python.org\"))\n        if len(parts) > 6 and parts[6] in {\"message\", \"thread\"}:\n            item_type = parts[6]\n\n    # Mailman3 list info structure is\n    # https://mail.python.org/mailman3/lists/<list_name>.python.org/\n    elif \"mailman3\" in parts:\n        list_name = (\n            parts[parts.index(\"mailman3\") + 2].removesuffix(\".python.org\"))\n\n    # Pipermail (Mailman) archive structure is\n    # https://mail.python.org/pipermail/<list_name>/<month>-<year>/<id>\n    elif \"pipermail\" in parts:\n        list_name = parts[parts.index(\"pipermail\") + 1]\n        item_type = \"message\" if len(parts) > 6 else \"list\"\n\n    # Mailman listinfo structure is\n    # https://mail.python.org/mailman/listinfo/<list_name>\n    elif \"listinfo\" in parts:\n        list_name = parts[parts.index(\"listinfo\") + 1]\n\n    # Not a link to a mailing list, message or thread\n    else:\n        raise ValueError(\n            f\"{'/'.join(parts)} not a link to a list, message or thread\")\n\n    return list_name, item_type\n\n\ndef _process_discourse_url(parts: list[str]) -> tuple[str, str]:\n    item_name = \"discourse\"\n\n    if len(parts) < 5 or (\"t\" not in parts and \"c\" not in parts):\n        raise ValueError(\n            f\"{'/'.join(parts)} not a link to a Discourse thread or category\")\n\n    first_subpart = parts[4]\n    has_title = not first_subpart.isnumeric()\n\n    if \"t\" in parts:\n        item_type = \"post\" if len(parts) > (5 + has_title) else \"thread\"\n    elif \"c\" in parts:\n        item_type = \"category\"\n        if has_title:\n            item_name = f\"{first_subpart.replace('-', ' ')} {item_name}\"\n\n    return item_name, item_type\n\n\n# Domains supported for pretty URL parsing\nLINK_PRETTIFIERS = {\n    \"mail.python.org\": _process_list_url,\n    \"discuss.python.org\": _process_discourse_url,\n}\n\n\ndef _process_pretty_url(url: str) -> tuple[str, str]:\n    parts = url.lower().strip().strip(\"/\").split(\"/\")\n    try:\n        item_name, item_type = LINK_PRETTIFIERS[parts[2]](parts)\n    except KeyError as error:\n        raise ValueError(\n            f\"{url} not a link to a recognized domain to prettify\") from error\n    item_name = item_name.title().replace(\"Sig\", \"SIG\").replace(\"Pep\", \"PEP\")\n    return item_name, item_type\n\n\ndef _make_link_pretty(url: str) -> str:\n    item_name, item_type = _process_pretty_url(url)\n    return f\"{item_name} {item_type}\"\n", "path": "pep_sphinx_extensions/pep_processor/transforms/pep_headers.py"}]}