We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
xml parsing heuristics crash if a reference has anchor "STD", "FYI", "RFC", or "BCP"
### Describe the issue

I'm trying to submit a draft.  On my first attempt, I realized that I tagged the wrong commit and missed some important changes from a co-author.  So when I received the email, I cancelled the submission.

I have since tried twice (with the right commit).  Both have gotten to the point where I receive an email and both have failed when I requested that the draft be published.

For my second retry attempt, the failed submission was apparently still in progress, so that failed.  I then cancelled the first retry and tried a third time.  That has also failed.  I will leave things in that state for now.

https://datatracker.ietf.org/submit/status/136389/ (original, cancelled)
https://datatracker.ietf.org/submit/status/136390/ (retry 1, failed, also cancelled)
https://datatracker.ietf.org/submit/status/136403/ (retry 3, failed)


### Code of Conduct

- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `ietf/utils/xmldraft.py`
Content:
```
1 # Copyright The IETF Trust 2022, All Rights Reserved
2 # -*- coding: utf-8 -*-
3 import datetime
4 import io
5 import re
6 import xml2rfc
7 
8 import debug  # pyflakes: ignore
9 
10 from contextlib import ExitStack
11 from xml2rfc.util.date import augment_date, extract_date
12 from ietf.utils.timezone import date_today
13 
14 from .draft import Draft
15 
16 
17 class XMLDraft(Draft):
18     """Draft from XML source
19 
20     Not all methods from the superclass are implemented yet.
21     """
22     def __init__(self, xml_file):
23         """Initialize XMLDraft instance
24 
25         :parameter xml_file: path to file containing XML source
26         """
27         super().__init__()
28         # cast xml_file to str so, e.g., this will work with a Path
29         self.xmltree, self.xml_version = self.parse_xml(str(xml_file))
30         self.xmlroot = self.xmltree.getroot()
31         self.filename, self.revision = self._parse_docname()
32 
33     @staticmethod
34     def parse_xml(filename):
35         """Parse XML draft
36 
37         Converts to xml2rfc v3 schema, then returns the root of the v3 tree and the original
38         xml version.
39         """
40         orig_write_out = xml2rfc.log.write_out
41         orig_write_err = xml2rfc.log.write_err
42         parser_out = io.StringIO()
43         parser_err = io.StringIO()
44 
45         with ExitStack() as stack:
46             @stack.callback
47             def cleanup():  # called when context exited, even if there's an exception
48                 xml2rfc.log.write_out = orig_write_out
49                 xml2rfc.log.write_err = orig_write_err
50 
51             xml2rfc.log.write_out = parser_out
52             xml2rfc.log.write_err = parser_err
53 
54             parser = xml2rfc.XmlRfcParser(filename, quiet=True)
55             try:
56                 tree = parser.parse()
57             except Exception as e:
58                 raise XMLParseError(parser_out.getvalue(), parser_err.getvalue()) from e
59 
60             xml_version = tree.getroot().get('version', '2')
61             if xml_version == '2':
62                 v2v3 = xml2rfc.V2v3XmlWriter(tree)
63                 tree.tree = v2v3.convert2to3()
64         return tree, xml_version
65 
66     def _document_name(self, ref):
67         """Get document name from reference."""
68         series = ["rfc", "bcp", "fyi", "std"]
69         # handle xinclude first
70         # FIXME: this assumes the xinclude is a bibxml href; if it isn't, there can
71         # still be false negatives. it would be better to expand the xinclude and parse
72         # its seriesInfo.
73         if ref.tag.endswith("}include"):
74             name = re.search(
75                 rf"reference\.({'|'.join(series).upper()})\.(\d{{4}})\.xml",
76                 ref.attrib["href"],
77             )
78             if name:
79                 return f"{name.group(1)}{int(name.group(2))}".lower()
80             name = re.search(
81                 r"reference\.I-D\.(?:draft-)?(.*)\.xml", ref.attrib["href"]
82             )
83             if name:
84                 return f"draft-{name.group(1)}"
85             # can't extract the name, give up
86             return ""
87 
88         # check the anchor next
89         anchor = ref.get("anchor").lower()  # always give back lowercase
90         label = anchor.rstrip("0123456789")  # remove trailing digits
91         if label in series:
92             number = int(anchor[len(label) :])
93             return f"{label}{number}"
94 
95         # if we couldn't find a match so far, try the seriesInfo
96         series_query = " or ".join(f"@name='{x.upper()}'" for x in series)
97         for info in ref.xpath(
98             f"./seriesInfo[{series_query} or @name='Internet-Draft']"
99         ):
100             if not info.attrib["value"]:
101                 continue
102             if info.attrib["name"] == "Internet-Draft":
103                 return info.attrib["value"]
104             else:
105                 return f'{info.attrib["name"].lower()}{info.attrib["value"]}'
106         return ""
107 
108     def _reference_section_type(self, section_name):
109         """Determine reference type from name of references section"""
110         if section_name:
111             section_name = section_name.lower()
112             if 'normative' in section_name:
113                 return self.REF_TYPE_NORMATIVE
114             elif 'informative' in section_name:
115                 return self.REF_TYPE_INFORMATIVE
116         return self.REF_TYPE_UNKNOWN
117 
118     def _reference_section_name(self, section_elt):
119         section_name = section_elt.findtext('name')
120         if section_name is None and 'title' in section_elt.keys():
121             section_name = section_elt.get('title')  # fall back to title if we have it
122         return section_name
123 
124     def _parse_docname(self):
125         docname = self.xmlroot.attrib.get('docName')
126         revmatch = re.match(
127             r'^(?P<filename>.+?)(?:-(?P<rev>[0-9][0-9]))?$',
128             docname,
129 
130         )
131         if revmatch is None:
132             raise ValueError('Unable to parse docName')
133         # If a group had no match it is None
134         return revmatch.group('filename'), revmatch.group('rev')
135 
136     def get_title(self):
137         return self.xmlroot.findtext('front/title').strip()
138 
139     @staticmethod
140     def parse_creation_date(date_elt):
141         if date_elt is None:
142             return None
143         today = date_today()
144         # ths mimics handling of date elements in the xml2rfc text/html writers
145         year, month, day = extract_date(date_elt, today)
146         year, month, day = augment_date(year, month, day, today)
147         if not day:
148             # Must choose a day for a datetime.date. Per RFC 7991 sect 2.17, we use
149             # today's date if it is consistent with the rest of the date. Otherwise,
150             # arbitrariy (and consistent with the text parser) assume the 15th.
151             if year == today.year and month == today.month:
152                 day = today.day
153             else:
154                 day = 15
155         return datetime.date(year, month, day)
156 
157     def get_creation_date(self):
158         return self.parse_creation_date(self.xmlroot.find("front/date"))
159 
160     # todo fix the implementation of XMLDraft.get_abstract()
161     #
162     # This code was pulled from ietf.submit.forms where it existed for some time.
163     # It does not work, at least with modern xml2rfc. This assumes that the abstract
164     # is simply text in the front/abstract node, but the XML schema wraps the actual
165     # abstract text in <t> elements (and allows <dl>, <ol>, and <ul> as well). As a
166     # result, this method normally returns an empty string, which is later replaced by
167     # the abstract parsed from the rendered text. For now, I a commenting this out
168     # and making it explicit that the abstract always comes from the text format.
169     #
170     # def get_abstract(self):
171     #     """Extract the abstract"""
172     #     abstract = self.xmlroot.findtext('front/abstract')
173     #     return abstract.strip() if abstract else ''
174 
175     def get_author_list(self):
176         """Get detailed author list
177 
178         Returns a list of dicts with the following keys:
179             name, first_name, middle_initial, last_name,
180             name_suffix, email, country, affiliation
181         Values will be None if not available
182         """
183         result = []
184         empty_author = {
185             k: None for k in [
186                 'name', 'first_name', 'middle_initial', 'last_name',
187                 'name_suffix', 'email', 'country', 'affiliation',
188             ]
189         }
190 
191         for author in self.xmlroot.findall('front/author'):
192             info = {
193                 'name': author.attrib.get('fullname'),
194                 'email': author.findtext('address/email'),
195                 'affiliation': author.findtext('organization'),
196             }
197             elem = author.find('address/postal/country')
198             if elem is not None:
199                 ascii_country = elem.get('ascii', None)
200                 info['country'] = ascii_country if ascii_country else elem.text
201             for item in info:
202                 if info[item]:
203                     info[item] = info[item].strip()
204             result.append(empty_author | info)  # merge, preferring info
205         return result
206 
207     def get_refs(self):
208         """Extract references from the draft"""
209         refs = {}
210         # accept nested <references> sections
211         for section in self.xmlroot.findall("back//references"):
212             ref_type = self._reference_section_type(
213                 self._reference_section_name(section)
214             )
215             for ref in (
216                 section.findall("./reference")
217                 + section.findall("./referencegroup")
218                 + section.findall(
219                     "./xi:include", {"xi": "http://www.w3.org/2001/XInclude"}
220                 )
221             ):
222                 name = self._document_name(ref)
223                 if name:
224                     refs[name] = ref_type
225         return refs
226 
227 
228 class XMLParseError(Exception):
229     """An error occurred while parsing"""
230     def __init__(self, out: str, err: str, *args):
231         super().__init__(*args)
232         self._out = out
233         self._err = err
234 
235     def parser_msgs(self):
236         return self._out.splitlines() + self._err.splitlines()
237
```
--- 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/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py
--- a/ietf/utils/xmldraft.py
+++ b/ietf/utils/xmldraft.py
@@ -88,8 +88,9 @@
         # check the anchor next
         anchor = ref.get("anchor").lower()  # always give back lowercase
         label = anchor.rstrip("0123456789")  # remove trailing digits
-        if label in series:
-            number = int(anchor[len(label) :])
+        maybe_number = anchor[len(label) :]
+        if label in series and maybe_number.isdigit():
+            number = int(maybe_number)
             return f"{label}{number}"
 
         # if we couldn't find a match so far, try the seriesInfo

{"golden_diff": "diff --git a/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py\n--- a/ietf/utils/xmldraft.py\n+++ b/ietf/utils/xmldraft.py\n@@ -88,8 +88,9 @@\n         # check the anchor next\n         anchor = ref.get(\"anchor\").lower()  # always give back lowercase\n         label = anchor.rstrip(\"0123456789\")  # remove trailing digits\n-        if label in series:\n-            number = int(anchor[len(label) :])\n+        maybe_number = anchor[len(label) :]\n+        if label in series and maybe_number.isdigit():\n+            number = int(maybe_number)\n             return f\"{label}{number}\"\n \n         # if we couldn't find a match so far, try the seriesInfo\n", "issue": "xml parsing heuristics crash if a reference has anchor \"STD\", \"FYI\", \"RFC\", or \"BCP\"\n### Describe the issue\n\nI'm trying to submit a draft.  On my first attempt, I realized that I tagged the wrong commit and missed some important changes from a co-author.  So when I received the email, I cancelled the submission.\r\n\r\nI have since tried twice (with the right commit).  Both have gotten to the point where I receive an email and both have failed when I requested that the draft be published.\r\n\r\nFor my second retry attempt, the failed submission was apparently still in progress, so that failed.  I then cancelled the first retry and tried a third time.  That has also failed.  I will leave things in that state for now.\r\n\r\nhttps://datatracker.ietf.org/submit/status/136389/ (original, cancelled)\r\nhttps://datatracker.ietf.org/submit/status/136390/ (retry 1, failed, also cancelled)\r\nhttps://datatracker.ietf.org/submit/status/136403/ (retry 3, failed)\r\n\n\n### Code of Conduct\n\n- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)\n", "before_files": [{"content": "# Copyright The IETF Trust 2022, All Rights Reserved\n# -*- coding: utf-8 -*-\nimport datetime\nimport io\nimport re\nimport xml2rfc\n\nimport debug  # pyflakes: ignore\n\nfrom contextlib import ExitStack\nfrom xml2rfc.util.date import augment_date, extract_date\nfrom ietf.utils.timezone import date_today\n\nfrom .draft import Draft\n\n\nclass XMLDraft(Draft):\n    \"\"\"Draft from XML source\n\n    Not all methods from the superclass are implemented yet.\n    \"\"\"\n    def __init__(self, xml_file):\n        \"\"\"Initialize XMLDraft instance\n\n        :parameter xml_file: path to file containing XML source\n        \"\"\"\n        super().__init__()\n        # cast xml_file to str so, e.g., this will work with a Path\n        self.xmltree, self.xml_version = self.parse_xml(str(xml_file))\n        self.xmlroot = self.xmltree.getroot()\n        self.filename, self.revision = self._parse_docname()\n\n    @staticmethod\n    def parse_xml(filename):\n        \"\"\"Parse XML draft\n\n        Converts to xml2rfc v3 schema, then returns the root of the v3 tree and the original\n        xml version.\n        \"\"\"\n        orig_write_out = xml2rfc.log.write_out\n        orig_write_err = xml2rfc.log.write_err\n        parser_out = io.StringIO()\n        parser_err = io.StringIO()\n\n        with ExitStack() as stack:\n            @stack.callback\n            def cleanup():  # called when context exited, even if there's an exception\n                xml2rfc.log.write_out = orig_write_out\n                xml2rfc.log.write_err = orig_write_err\n\n            xml2rfc.log.write_out = parser_out\n            xml2rfc.log.write_err = parser_err\n\n            parser = xml2rfc.XmlRfcParser(filename, quiet=True)\n            try:\n                tree = parser.parse()\n            except Exception as e:\n                raise XMLParseError(parser_out.getvalue(), parser_err.getvalue()) from e\n\n            xml_version = tree.getroot().get('version', '2')\n            if xml_version == '2':\n                v2v3 = xml2rfc.V2v3XmlWriter(tree)\n                tree.tree = v2v3.convert2to3()\n        return tree, xml_version\n\n    def _document_name(self, ref):\n        \"\"\"Get document name from reference.\"\"\"\n        series = [\"rfc\", \"bcp\", \"fyi\", \"std\"]\n        # handle xinclude first\n        # FIXME: this assumes the xinclude is a bibxml href; if it isn't, there can\n        # still be false negatives. it would be better to expand the xinclude and parse\n        # its seriesInfo.\n        if ref.tag.endswith(\"}include\"):\n            name = re.search(\n                rf\"reference\\.({'|'.join(series).upper()})\\.(\\d{{4}})\\.xml\",\n                ref.attrib[\"href\"],\n            )\n            if name:\n                return f\"{name.group(1)}{int(name.group(2))}\".lower()\n            name = re.search(\n                r\"reference\\.I-D\\.(?:draft-)?(.*)\\.xml\", ref.attrib[\"href\"]\n            )\n            if name:\n                return f\"draft-{name.group(1)}\"\n            # can't extract the name, give up\n            return \"\"\n\n        # check the anchor next\n        anchor = ref.get(\"anchor\").lower()  # always give back lowercase\n        label = anchor.rstrip(\"0123456789\")  # remove trailing digits\n        if label in series:\n            number = int(anchor[len(label) :])\n            return f\"{label}{number}\"\n\n        # if we couldn't find a match so far, try the seriesInfo\n        series_query = \" or \".join(f\"@name='{x.upper()}'\" for x in series)\n        for info in ref.xpath(\n            f\"./seriesInfo[{series_query} or @name='Internet-Draft']\"\n        ):\n            if not info.attrib[\"value\"]:\n                continue\n            if info.attrib[\"name\"] == \"Internet-Draft\":\n                return info.attrib[\"value\"]\n            else:\n                return f'{info.attrib[\"name\"].lower()}{info.attrib[\"value\"]}'\n        return \"\"\n\n    def _reference_section_type(self, section_name):\n        \"\"\"Determine reference type from name of references section\"\"\"\n        if section_name:\n            section_name = section_name.lower()\n            if 'normative' in section_name:\n                return self.REF_TYPE_NORMATIVE\n            elif 'informative' in section_name:\n                return self.REF_TYPE_INFORMATIVE\n        return self.REF_TYPE_UNKNOWN\n\n    def _reference_section_name(self, section_elt):\n        section_name = section_elt.findtext('name')\n        if section_name is None and 'title' in section_elt.keys():\n            section_name = section_elt.get('title')  # fall back to title if we have it\n        return section_name\n\n    def _parse_docname(self):\n        docname = self.xmlroot.attrib.get('docName')\n        revmatch = re.match(\n            r'^(?P<filename>.+?)(?:-(?P<rev>[0-9][0-9]))?$',\n            docname,\n\n        )\n        if revmatch is None:\n            raise ValueError('Unable to parse docName')\n        # If a group had no match it is None\n        return revmatch.group('filename'), revmatch.group('rev')\n\n    def get_title(self):\n        return self.xmlroot.findtext('front/title').strip()\n\n    @staticmethod\n    def parse_creation_date(date_elt):\n        if date_elt is None:\n            return None\n        today = date_today()\n        # ths mimics handling of date elements in the xml2rfc text/html writers\n        year, month, day = extract_date(date_elt, today)\n        year, month, day = augment_date(year, month, day, today)\n        if not day:\n            # Must choose a day for a datetime.date. Per RFC 7991 sect 2.17, we use\n            # today's date if it is consistent with the rest of the date. Otherwise,\n            # arbitrariy (and consistent with the text parser) assume the 15th.\n            if year == today.year and month == today.month:\n                day = today.day\n            else:\n                day = 15\n        return datetime.date(year, month, day)\n\n    def get_creation_date(self):\n        return self.parse_creation_date(self.xmlroot.find(\"front/date\"))\n\n    # todo fix the implementation of XMLDraft.get_abstract()\n    #\n    # This code was pulled from ietf.submit.forms where it existed for some time.\n    # It does not work, at least with modern xml2rfc. This assumes that the abstract\n    # is simply text in the front/abstract node, but the XML schema wraps the actual\n    # abstract text in <t> elements (and allows <dl>, <ol>, and <ul> as well). As a\n    # result, this method normally returns an empty string, which is later replaced by\n    # the abstract parsed from the rendered text. For now, I a commenting this out\n    # and making it explicit that the abstract always comes from the text format.\n    #\n    # def get_abstract(self):\n    #     \"\"\"Extract the abstract\"\"\"\n    #     abstract = self.xmlroot.findtext('front/abstract')\n    #     return abstract.strip() if abstract else ''\n\n    def get_author_list(self):\n        \"\"\"Get detailed author list\n\n        Returns a list of dicts with the following keys:\n            name, first_name, middle_initial, last_name,\n            name_suffix, email, country, affiliation\n        Values will be None if not available\n        \"\"\"\n        result = []\n        empty_author = {\n            k: None for k in [\n                'name', 'first_name', 'middle_initial', 'last_name',\n                'name_suffix', 'email', 'country', 'affiliation',\n            ]\n        }\n\n        for author in self.xmlroot.findall('front/author'):\n            info = {\n                'name': author.attrib.get('fullname'),\n                'email': author.findtext('address/email'),\n                'affiliation': author.findtext('organization'),\n            }\n            elem = author.find('address/postal/country')\n            if elem is not None:\n                ascii_country = elem.get('ascii', None)\n                info['country'] = ascii_country if ascii_country else elem.text\n            for item in info:\n                if info[item]:\n                    info[item] = info[item].strip()\n            result.append(empty_author | info)  # merge, preferring info\n        return result\n\n    def get_refs(self):\n        \"\"\"Extract references from the draft\"\"\"\n        refs = {}\n        # accept nested <references> sections\n        for section in self.xmlroot.findall(\"back//references\"):\n            ref_type = self._reference_section_type(\n                self._reference_section_name(section)\n            )\n            for ref in (\n                section.findall(\"./reference\")\n                + section.findall(\"./referencegroup\")\n                + section.findall(\n                    \"./xi:include\", {\"xi\": \"http://www.w3.org/2001/XInclude\"}\n                )\n            ):\n                name = self._document_name(ref)\n                if name:\n                    refs[name] = ref_type\n        return refs\n\n\nclass XMLParseError(Exception):\n    \"\"\"An error occurred while parsing\"\"\"\n    def __init__(self, out: str, err: str, *args):\n        super().__init__(*args)\n        self._out = out\n        self._err = err\n\n    def parser_msgs(self):\n        return self._out.splitlines() + self._err.splitlines()\n", "path": "ietf/utils/xmldraft.py"}], "after_files": [{"content": "# Copyright The IETF Trust 2022, All Rights Reserved\n# -*- coding: utf-8 -*-\nimport datetime\nimport io\nimport re\nimport xml2rfc\n\nimport debug  # pyflakes: ignore\n\nfrom contextlib import ExitStack\nfrom xml2rfc.util.date import augment_date, extract_date\nfrom ietf.utils.timezone import date_today\n\nfrom .draft import Draft\n\n\nclass XMLDraft(Draft):\n    \"\"\"Draft from XML source\n\n    Not all methods from the superclass are implemented yet.\n    \"\"\"\n    def __init__(self, xml_file):\n        \"\"\"Initialize XMLDraft instance\n\n        :parameter xml_file: path to file containing XML source\n        \"\"\"\n        super().__init__()\n        # cast xml_file to str so, e.g., this will work with a Path\n        self.xmltree, self.xml_version = self.parse_xml(str(xml_file))\n        self.xmlroot = self.xmltree.getroot()\n        self.filename, self.revision = self._parse_docname()\n\n    @staticmethod\n    def parse_xml(filename):\n        \"\"\"Parse XML draft\n\n        Converts to xml2rfc v3 schema, then returns the root of the v3 tree and the original\n        xml version.\n        \"\"\"\n        orig_write_out = xml2rfc.log.write_out\n        orig_write_err = xml2rfc.log.write_err\n        parser_out = io.StringIO()\n        parser_err = io.StringIO()\n\n        with ExitStack() as stack:\n            @stack.callback\n            def cleanup():  # called when context exited, even if there's an exception\n                xml2rfc.log.write_out = orig_write_out\n                xml2rfc.log.write_err = orig_write_err\n\n            xml2rfc.log.write_out = parser_out\n            xml2rfc.log.write_err = parser_err\n\n            parser = xml2rfc.XmlRfcParser(filename, quiet=True)\n            try:\n                tree = parser.parse()\n            except Exception as e:\n                raise XMLParseError(parser_out.getvalue(), parser_err.getvalue()) from e\n\n            xml_version = tree.getroot().get('version', '2')\n            if xml_version == '2':\n                v2v3 = xml2rfc.V2v3XmlWriter(tree)\n                tree.tree = v2v3.convert2to3()\n        return tree, xml_version\n\n    def _document_name(self, ref):\n        \"\"\"Get document name from reference.\"\"\"\n        series = [\"rfc\", \"bcp\", \"fyi\", \"std\"]\n        # handle xinclude first\n        # FIXME: this assumes the xinclude is a bibxml href; if it isn't, there can\n        # still be false negatives. it would be better to expand the xinclude and parse\n        # its seriesInfo.\n        if ref.tag.endswith(\"}include\"):\n            name = re.search(\n                rf\"reference\\.({'|'.join(series).upper()})\\.(\\d{{4}})\\.xml\",\n                ref.attrib[\"href\"],\n            )\n            if name:\n                return f\"{name.group(1)}{int(name.group(2))}\".lower()\n            name = re.search(\n                r\"reference\\.I-D\\.(?:draft-)?(.*)\\.xml\", ref.attrib[\"href\"]\n            )\n            if name:\n                return f\"draft-{name.group(1)}\"\n            # can't extract the name, give up\n            return \"\"\n\n        # check the anchor next\n        anchor = ref.get(\"anchor\").lower()  # always give back lowercase\n        label = anchor.rstrip(\"0123456789\")  # remove trailing digits\n        maybe_number = anchor[len(label) :]\n        if label in series and maybe_number.isdigit():\n            number = int(maybe_number)\n            return f\"{label}{number}\"\n\n        # if we couldn't find a match so far, try the seriesInfo\n        series_query = \" or \".join(f\"@name='{x.upper()}'\" for x in series)\n        for info in ref.xpath(\n            f\"./seriesInfo[{series_query} or @name='Internet-Draft']\"\n        ):\n            if not info.attrib[\"value\"]:\n                continue\n            if info.attrib[\"name\"] == \"Internet-Draft\":\n                return info.attrib[\"value\"]\n            else:\n                return f'{info.attrib[\"name\"].lower()}{info.attrib[\"value\"]}'\n        return \"\"\n\n    def _reference_section_type(self, section_name):\n        \"\"\"Determine reference type from name of references section\"\"\"\n        if section_name:\n            section_name = section_name.lower()\n            if 'normative' in section_name:\n                return self.REF_TYPE_NORMATIVE\n            elif 'informative' in section_name:\n                return self.REF_TYPE_INFORMATIVE\n        return self.REF_TYPE_UNKNOWN\n\n    def _reference_section_name(self, section_elt):\n        section_name = section_elt.findtext('name')\n        if section_name is None and 'title' in section_elt.keys():\n            section_name = section_elt.get('title')  # fall back to title if we have it\n        return section_name\n\n    def _parse_docname(self):\n        docname = self.xmlroot.attrib.get('docName')\n        revmatch = re.match(\n            r'^(?P<filename>.+?)(?:-(?P<rev>[0-9][0-9]))?$',\n            docname,\n\n        )\n        if revmatch is None:\n            raise ValueError('Unable to parse docName')\n        # If a group had no match it is None\n        return revmatch.group('filename'), revmatch.group('rev')\n\n    def get_title(self):\n        return self.xmlroot.findtext('front/title').strip()\n\n    @staticmethod\n    def parse_creation_date(date_elt):\n        if date_elt is None:\n            return None\n        today = date_today()\n        # ths mimics handling of date elements in the xml2rfc text/html writers\n        year, month, day = extract_date(date_elt, today)\n        year, month, day = augment_date(year, month, day, today)\n        if not day:\n            # Must choose a day for a datetime.date. Per RFC 7991 sect 2.17, we use\n            # today's date if it is consistent with the rest of the date. Otherwise,\n            # arbitrariy (and consistent with the text parser) assume the 15th.\n            if year == today.year and month == today.month:\n                day = today.day\n            else:\n                day = 15\n        return datetime.date(year, month, day)\n\n    def get_creation_date(self):\n        return self.parse_creation_date(self.xmlroot.find(\"front/date\"))\n\n    # todo fix the implementation of XMLDraft.get_abstract()\n    #\n    # This code was pulled from ietf.submit.forms where it existed for some time.\n    # It does not work, at least with modern xml2rfc. This assumes that the abstract\n    # is simply text in the front/abstract node, but the XML schema wraps the actual\n    # abstract text in <t> elements (and allows <dl>, <ol>, and <ul> as well). As a\n    # result, this method normally returns an empty string, which is later replaced by\n    # the abstract parsed from the rendered text. For now, I a commenting this out\n    # and making it explicit that the abstract always comes from the text format.\n    #\n    # def get_abstract(self):\n    #     \"\"\"Extract the abstract\"\"\"\n    #     abstract = self.xmlroot.findtext('front/abstract')\n    #     return abstract.strip() if abstract else ''\n\n    def get_author_list(self):\n        \"\"\"Get detailed author list\n\n        Returns a list of dicts with the following keys:\n            name, first_name, middle_initial, last_name,\n            name_suffix, email, country, affiliation\n        Values will be None if not available\n        \"\"\"\n        result = []\n        empty_author = {\n            k: None for k in [\n                'name', 'first_name', 'middle_initial', 'last_name',\n                'name_suffix', 'email', 'country', 'affiliation',\n            ]\n        }\n\n        for author in self.xmlroot.findall('front/author'):\n            info = {\n                'name': author.attrib.get('fullname'),\n                'email': author.findtext('address/email'),\n                'affiliation': author.findtext('organization'),\n            }\n            elem = author.find('address/postal/country')\n            if elem is not None:\n                ascii_country = elem.get('ascii', None)\n                info['country'] = ascii_country if ascii_country else elem.text\n            for item in info:\n                if info[item]:\n                    info[item] = info[item].strip()\n            result.append(empty_author | info)  # merge, preferring info\n        return result\n\n    def get_refs(self):\n        \"\"\"Extract references from the draft\"\"\"\n        refs = {}\n        # accept nested <references> sections\n        for section in self.xmlroot.findall(\"back//references\"):\n            ref_type = self._reference_section_type(\n                self._reference_section_name(section)\n            )\n            for ref in (\n                section.findall(\"./reference\")\n                + section.findall(\"./referencegroup\")\n                + section.findall(\n                    \"./xi:include\", {\"xi\": \"http://www.w3.org/2001/XInclude\"}\n                )\n            ):\n                name = self._document_name(ref)\n                if name:\n                    refs[name] = ref_type\n        return refs\n\n\nclass XMLParseError(Exception):\n    \"\"\"An error occurred while parsing\"\"\"\n    def __init__(self, out: str, err: str, *args):\n        super().__init__(*args)\n        self._out = out\n        self._err = err\n\n    def parser_msgs(self):\n        return self._out.splitlines() + self._err.splitlines()\n", "path": "ietf/utils/xmldraft.py"}]}