We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Superseded-By And Replaces link is bugged
See for example https://peps.python.org/pep-0509/.

The link to PEP 699 in the header points to a link relative to the the same document (which does not exist), instead of a link to another PEP.

Similarly, the `Replaces` link is bugged too https://peps.python.org/pep-0699/

--- 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 from pep_sphinx_extensions.pep_zero_generator.constants import (
11     SPECIAL_STATUSES,
12     STATUS_ACCEPTED,
13     STATUS_ACTIVE,
14     STATUS_DEFERRED,
15     STATUS_DRAFT,
16     STATUS_FINAL,
17     STATUS_PROVISIONAL,
18     STATUS_REJECTED,
19     STATUS_SUPERSEDED,
20     STATUS_WITHDRAWN,
21     TYPE_INFO,
22     TYPE_PROCESS,
23     TYPE_STANDARDS,
24 )
25 
26 ABBREVIATED_STATUSES = {
27     STATUS_DRAFT: "Proposal under active discussion and revision",
28     STATUS_DEFERRED: "Inactive draft that may be taken up again at a later time",
29     STATUS_ACCEPTED: "Normative proposal accepted for implementation",
30     STATUS_ACTIVE: "Currently valid informational guidance, or an in-use process",
31     STATUS_FINAL: "Accepted and implementation complete, or no longer active",
32     STATUS_WITHDRAWN: "Removed from consideration by sponsor or authors",
33     STATUS_REJECTED: "Formally declined and will not be accepted",
34     STATUS_SUPERSEDED: "Replaced by another succeeding PEP",
35     STATUS_PROVISIONAL: "Provisionally accepted but additional feedback needed",
36 }
37 
38 ABBREVIATED_TYPES = {
39     TYPE_STANDARDS: "Normative PEP with a new feature for Python, implementation "
40     "change for CPython or interoperability standard for the ecosystem",
41     TYPE_INFO: "Non-normative PEP containing background, guidelines or other "
42     "information relevant to the Python ecosystem",
43     TYPE_PROCESS: "Normative PEP describing or proposing a change to a Python "
44     "community process, workflow or governance",
45 }
46 
47 class PEPParsingError(errors.SphinxError):
48     pass
49 
50 
51 # PEPHeaders is identical to docutils.transforms.peps.Headers excepting bdfl-delegate, sponsor & superseeded-by
52 class PEPHeaders(transforms.Transform):
53     """Process fields in a PEP's initial RFC-2822 header."""
54 
55     # Run before pep_processor.transforms.pep_title.PEPTitle
56     default_priority = 330
57 
58     def apply(self) -> None:
59         if not Path(self.document["source"]).match("pep-*"):
60             return  # not a PEP file, exit early
61 
62         if not len(self.document):
63             raise PEPParsingError("Document tree is empty.")
64 
65         header = self.document[0]
66         if not isinstance(header, nodes.field_list) or "rfc2822" not in header["classes"]:
67             raise PEPParsingError("Document does not begin with an RFC-2822 header; it is not a PEP.")
68 
69         # PEP number should be the first field
70         pep_field = header[0]
71         if pep_field[0].astext().lower() != "pep":
72             raise PEPParsingError("Document does not contain an RFC-2822 'PEP' header!")
73 
74         # Extract PEP number
75         value = pep_field[1].astext()
76         try:
77             pep_num = int(value)
78         except ValueError:
79             raise PEPParsingError(f"'PEP' header must contain an integer. '{value}' is invalid!")
80 
81         # Special processing for PEP 0.
82         if pep_num == 0:
83             pending = nodes.pending(pep_zero.PEPZero)
84             self.document.insert(1, pending)
85             self.document.note_pending(pending)
86 
87         # If there are less than two headers in the preamble, or if Title is absent
88         if len(header) < 2 or header[1][0].astext().lower() != "title":
89             raise PEPParsingError("No title!")
90 
91         fields_to_remove = []
92         for field in header:
93             name = field[0].astext().lower()
94             body = field[1]
95             if len(body) == 0:
96                 # body is empty
97                 continue
98             elif len(body) > 1:
99                 msg = f"PEP header field body contains multiple elements:\n{field.pformat(level=1)}"
100                 raise PEPParsingError(msg)
101             elif not isinstance(body[0], nodes.paragraph):  # len(body) == 1
102                 msg = f"PEP header field body may only contain a single paragraph:\n{field.pformat(level=1)}"
103                 raise PEPParsingError(msg)
104 
105             para = body[0]
106             if name in {"author", "bdfl-delegate", "pep-delegate", "sponsor"}:
107                 # mask emails
108                 for node in para:
109                     if not isinstance(node, nodes.reference):
110                         continue
111                     node.replace_self(_mask_email(node))
112             elif name in {"discussions-to", "resolution", "post-history"}:
113                 # Prettify mailing list and Discourse links
114                 for node in para:
115                     if (not isinstance(node, nodes.reference)
116                             or not node["refuri"]):
117                         continue
118                     # Have known mailto links link to their main list pages
119                     if node["refuri"].lower().startswith("mailto:"):
120                         node["refuri"] = _generate_list_url(node["refuri"])
121                     parts = node["refuri"].lower().split("/")
122                     if len(parts) <= 2 or parts[2] not in LINK_PRETTIFIERS:
123                         continue
124                     pretty_title = _make_link_pretty(str(node["refuri"]))
125                     if name == "post-history":
126                         node["reftitle"] = pretty_title
127                     else:
128                         node[0] = nodes.Text(pretty_title)
129             elif name in {"replaces", "superseded-by", "requires"}:
130                 # replace PEP numbers with normalised list of links to PEPs
131                 new_body = []
132                 for pep_str in re.split(r",?\s+", body.astext()):
133                     target = self.document.settings.pep_url.format(int(pep_str))
134                     new_body += [nodes.reference("", pep_str, refuri=target), nodes.Text(", ")]
135                 para[:] = new_body[:-1]  # drop trailing space
136             elif name == "topic":
137                 new_body = []
138                 for topic_name in body.astext().split(","):
139                     if topic_name:
140                         target = f"topic/{topic_name.lower().strip()}"
141                         if self.document.settings.builder == "html":
142                             target = f"{target}.html"
143                         else:
144                             target = f"../{target}/"
145                         new_body += [
146                             nodes.reference("", topic_name, refuri=target),
147                             nodes.Text(", "),
148                         ]
149                 if new_body:
150                     para[:] = new_body[:-1]  # Drop trailing space/comma
151             elif name == "status":
152                 para[:] = [
153                     nodes.abbreviation(
154                         body.astext(),
155                         body.astext(),
156                         explanation=_abbreviate_status(body.astext()),
157                     )
158                 ]
159             elif name == "type":
160                 para[:] = [
161                     nodes.abbreviation(
162                         body.astext(),
163                         body.astext(),
164                         explanation=_abbreviate_type(body.astext()),
165                     )
166                 ]
167             elif name in {"last-modified", "content-type", "version"}:
168                 # Mark unneeded fields
169                 fields_to_remove.append(field)
170 
171             # Remove any trailing commas and whitespace in the headers
172             if para and isinstance(para[-1], nodes.Text):
173                 last_node = para[-1]
174                 if last_node.astext().strip() == ",":
175                     last_node.parent.remove(last_node)
176                 else:
177                     para[-1] = last_node.rstrip().rstrip(",")
178 
179         # Remove unneeded fields
180         for field in fields_to_remove:
181             field.parent.remove(field)
182 
183 
184 def _generate_list_url(mailto: str) -> str:
185     list_name_domain = mailto.lower().removeprefix("mailto:").strip()
186     list_name = list_name_domain.split("@")[0]
187 
188     if list_name_domain.endswith("@googlegroups.com"):
189         return f"https://groups.google.com/g/{list_name}"
190 
191     if not list_name_domain.endswith("@python.org"):
192         return mailto
193 
194     # Active lists not yet on Mailman3; this URL will redirect if/when they are
195     if list_name in {"csv", "db-sig", "doc-sig", "python-list", "web-sig"}:
196         return f"https://mail.python.org/mailman/listinfo/{list_name}"
197     # Retired lists that are closed for posting, so only the archive matters
198     if list_name in {"import-sig", "python-3000"}:
199         return f"https://mail.python.org/pipermail/{list_name}/"
200     # The remaining lists (and any new ones) are all on Mailman3/Hyperkitty
201     return f"https://mail.python.org/archives/list/{list_name}@python.org/"
202 
203 
204 def _process_list_url(parts: list[str]) -> tuple[str, str]:
205     item_type = "list"
206 
207     # HyperKitty (Mailman3) archive structure is
208     # https://mail.python.org/archives/list/<list_name>/thread/<id>
209     if "archives" in parts:
210         list_name = (
211             parts[parts.index("archives") + 2].removesuffix("@python.org"))
212         if len(parts) > 6 and parts[6] in {"message", "thread"}:
213             item_type = parts[6]
214 
215     # Mailman3 list info structure is
216     # https://mail.python.org/mailman3/lists/<list_name>.python.org/
217     elif "mailman3" in parts:
218         list_name = (
219             parts[parts.index("mailman3") + 2].removesuffix(".python.org"))
220 
221     # Pipermail (Mailman) archive structure is
222     # https://mail.python.org/pipermail/<list_name>/<month>-<year>/<id>
223     elif "pipermail" in parts:
224         list_name = parts[parts.index("pipermail") + 1]
225         item_type = "message" if len(parts) > 6 else "list"
226 
227     # Mailman listinfo structure is
228     # https://mail.python.org/mailman/listinfo/<list_name>
229     elif "listinfo" in parts:
230         list_name = parts[parts.index("listinfo") + 1]
231 
232     # Not a link to a mailing list, message or thread
233     else:
234         raise ValueError(
235             f"{'/'.join(parts)} not a link to a list, message or thread")
236 
237     return list_name, item_type
238 
239 
240 def _process_discourse_url(parts: list[str]) -> tuple[str, str]:
241     item_name = "discourse"
242 
243     if len(parts) < 5 or ("t" not in parts and "c" not in parts):
244         raise ValueError(
245             f"{'/'.join(parts)} not a link to a Discourse thread or category")
246 
247     first_subpart = parts[4]
248     has_title = not first_subpart.isnumeric()
249 
250     if "t" in parts:
251         item_type = "post" if len(parts) > (5 + has_title) else "thread"
252     elif "c" in parts:
253         item_type = "category"
254         if has_title:
255             item_name = f"{first_subpart.replace('-', ' ')} {item_name}"
256 
257     return item_name, item_type
258 
259 
260 # Domains supported for pretty URL parsing
261 LINK_PRETTIFIERS = {
262     "mail.python.org": _process_list_url,
263     "discuss.python.org": _process_discourse_url,
264 }
265 
266 
267 def _process_pretty_url(url: str) -> tuple[str, str]:
268     parts = url.lower().strip().strip("/").split("/")
269     try:
270         item_name, item_type = LINK_PRETTIFIERS[parts[2]](parts)
271     except KeyError as error:
272         raise ValueError(
273             f"{url} not a link to a recognized domain to prettify") from error
274     item_name = item_name.title().replace("Sig", "SIG").replace("Pep", "PEP")
275     return item_name, item_type
276 
277 
278 def _make_link_pretty(url: str) -> str:
279     item_name, item_type = _process_pretty_url(url)
280     return f"{item_name} {item_type}"
281 
282 
283 def _abbreviate_status(status: str) -> str:
284     if status in SPECIAL_STATUSES:
285         status = SPECIAL_STATUSES[status]
286 
287     try:
288         return ABBREVIATED_STATUSES[status]
289     except KeyError:
290         raise PEPParsingError(f"Unknown status: {status}")
291 
292 
293 def _abbreviate_type(type_: str) -> str:
294     try:
295         return ABBREVIATED_TYPES[type_]
296     except KeyError:
297         raise PEPParsingError(f"Unknown type: {type_}")
298
```
--- 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
@@ -131,6 +131,8 @@
                 new_body = []
                 for pep_str in re.split(r",?\s+", body.astext()):
                     target = self.document.settings.pep_url.format(int(pep_str))
+                    if self.document.settings.builder == "dirhtml":
+                        target = f"../{target}"
                     new_body += [nodes.reference("", pep_str, refuri=target), nodes.Text(", ")]
                 para[:] = new_body[:-1]  # drop trailing space
             elif name == "topic":

{"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@@ -131,6 +131,8 @@\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+                    if self.document.settings.builder == \"dirhtml\":\n+                        target = f\"../{target}\"\n                     new_body += [nodes.reference(\"\", pep_str, refuri=target), nodes.Text(\", \")]\n                 para[:] = new_body[:-1]  # drop trailing space\n             elif name == \"topic\":\n", "issue": "Superseded-By And Replaces link is bugged\nSee for example https://peps.python.org/pep-0509/.\r\n\r\nThe link to PEP 699 in the header points to a link relative to the the same document (which does not exist), instead of a link to another PEP.\r\n\r\nSimilarly, the `Replaces` link is bugged too https://peps.python.org/pep-0699/\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\nfrom pep_sphinx_extensions.pep_zero_generator.constants import (\n    SPECIAL_STATUSES,\n    STATUS_ACCEPTED,\n    STATUS_ACTIVE,\n    STATUS_DEFERRED,\n    STATUS_DRAFT,\n    STATUS_FINAL,\n    STATUS_PROVISIONAL,\n    STATUS_REJECTED,\n    STATUS_SUPERSEDED,\n    STATUS_WITHDRAWN,\n    TYPE_INFO,\n    TYPE_PROCESS,\n    TYPE_STANDARDS,\n)\n\nABBREVIATED_STATUSES = {\n    STATUS_DRAFT: \"Proposal under active discussion and revision\",\n    STATUS_DEFERRED: \"Inactive draft that may be taken up again at a later time\",\n    STATUS_ACCEPTED: \"Normative proposal accepted for implementation\",\n    STATUS_ACTIVE: \"Currently valid informational guidance, or an in-use process\",\n    STATUS_FINAL: \"Accepted and implementation complete, or no longer active\",\n    STATUS_WITHDRAWN: \"Removed from consideration by sponsor or authors\",\n    STATUS_REJECTED: \"Formally declined and will not be accepted\",\n    STATUS_SUPERSEDED: \"Replaced by another succeeding PEP\",\n    STATUS_PROVISIONAL: \"Provisionally accepted but additional feedback needed\",\n}\n\nABBREVIATED_TYPES = {\n    TYPE_STANDARDS: \"Normative PEP with a new feature for Python, implementation \"\n    \"change for CPython or interoperability standard for the ecosystem\",\n    TYPE_INFO: \"Non-normative PEP containing background, guidelines or other \"\n    \"information relevant to the Python ecosystem\",\n    TYPE_PROCESS: \"Normative PEP describing or proposing a change to a Python \"\n    \"community process, workflow or governance\",\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                        if self.document.settings.builder == \"html\":\n                            target = f\"{target}.html\"\n                        else:\n                            target = f\"../{target}/\"\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 == \"status\":\n                para[:] = [\n                    nodes.abbreviation(\n                        body.astext(),\n                        body.astext(),\n                        explanation=_abbreviate_status(body.astext()),\n                    )\n                ]\n            elif name == \"type\":\n                para[:] = [\n                    nodes.abbreviation(\n                        body.astext(),\n                        body.astext(),\n                        explanation=_abbreviate_type(body.astext()),\n                    )\n                ]\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\n\ndef _abbreviate_status(status: str) -> str:\n    if status in SPECIAL_STATUSES:\n        status = SPECIAL_STATUSES[status]\n\n    try:\n        return ABBREVIATED_STATUSES[status]\n    except KeyError:\n        raise PEPParsingError(f\"Unknown status: {status}\")\n\n\ndef _abbreviate_type(type_: str) -> str:\n    try:\n        return ABBREVIATED_TYPES[type_]\n    except KeyError:\n        raise PEPParsingError(f\"Unknown type: {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\nfrom pep_sphinx_extensions.pep_zero_generator.constants import (\n    SPECIAL_STATUSES,\n    STATUS_ACCEPTED,\n    STATUS_ACTIVE,\n    STATUS_DEFERRED,\n    STATUS_DRAFT,\n    STATUS_FINAL,\n    STATUS_PROVISIONAL,\n    STATUS_REJECTED,\n    STATUS_SUPERSEDED,\n    STATUS_WITHDRAWN,\n    TYPE_INFO,\n    TYPE_PROCESS,\n    TYPE_STANDARDS,\n)\n\nABBREVIATED_STATUSES = {\n    STATUS_DRAFT: \"Proposal under active discussion and revision\",\n    STATUS_DEFERRED: \"Inactive draft that may be taken up again at a later time\",\n    STATUS_ACCEPTED: \"Normative proposal accepted for implementation\",\n    STATUS_ACTIVE: \"Currently valid informational guidance, or an in-use process\",\n    STATUS_FINAL: \"Accepted and implementation complete, or no longer active\",\n    STATUS_WITHDRAWN: \"Removed from consideration by sponsor or authors\",\n    STATUS_REJECTED: \"Formally declined and will not be accepted\",\n    STATUS_SUPERSEDED: \"Replaced by another succeeding PEP\",\n    STATUS_PROVISIONAL: \"Provisionally accepted but additional feedback needed\",\n}\n\nABBREVIATED_TYPES = {\n    TYPE_STANDARDS: \"Normative PEP with a new feature for Python, implementation \"\n    \"change for CPython or interoperability standard for the ecosystem\",\n    TYPE_INFO: \"Non-normative PEP containing background, guidelines or other \"\n    \"information relevant to the Python ecosystem\",\n    TYPE_PROCESS: \"Normative PEP describing or proposing a change to a Python \"\n    \"community process, workflow or governance\",\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                    if self.document.settings.builder == \"dirhtml\":\n                        target = f\"../{target}\"\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                        if self.document.settings.builder == \"html\":\n                            target = f\"{target}.html\"\n                        else:\n                            target = f\"../{target}/\"\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 == \"status\":\n                para[:] = [\n                    nodes.abbreviation(\n                        body.astext(),\n                        body.astext(),\n                        explanation=_abbreviate_status(body.astext()),\n                    )\n                ]\n            elif name == \"type\":\n                para[:] = [\n                    nodes.abbreviation(\n                        body.astext(),\n                        body.astext(),\n                        explanation=_abbreviate_type(body.astext()),\n                    )\n                ]\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\n\ndef _abbreviate_status(status: str) -> str:\n    if status in SPECIAL_STATUSES:\n        status = SPECIAL_STATUSES[status]\n\n    try:\n        return ABBREVIATED_STATUSES[status]\n    except KeyError:\n        raise PEPParsingError(f\"Unknown status: {status}\")\n\n\ndef _abbreviate_type(type_: str) -> str:\n    try:\n        return ABBREVIATED_TYPES[type_]\n    except KeyError:\n        raise PEPParsingError(f\"Unknown type: {type_}\")\n", "path": "pep_sphinx_extensions/pep_processor/transforms/pep_headers.py"}]}