diff options
| author | Michael Williamson <mike@zwobble.org> | 2026-08-09 14:57:34 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2026-08-09 14:57:34 +0100 |
| commit | 9d73a4b20a5bb24919697e29d221fdc6c9b50c13 (patch) | |
| tree | 1d86b6e609f34c3ec21ac9eda59bd011d3c7e027 | |
| parent | 32f3d9fae7ad0a4ad7cc9c74d81a6fd670912fbe (diff) | |
Ignore numbering levels that use numStyleLink to refer to themselves
| -rw-r--r-- | NEWS | 3 | ||||
| -rw-r--r-- | mammoth/docx/numbering_xml.py | 23 | ||||
| -rw-r--r-- | tests/docx/numbering_xml_tests.py | 17 |
3 files changed, 35 insertions, 8 deletions
@@ -3,6 +3,9 @@ * Fix: on Windows, when an image's content type includes a backslash in the subpart, files may be written outside of the directory set by --output-dir. +* Detect and ignore numbering levels that use numStyleLink to refer to + themselves. + # 1.12.0 * Handle hyperlinked wp:anchor and wp:inline elements. diff --git a/mammoth/docx/numbering_xml.py b/mammoth/docx/numbering_xml.py index 0bd1952..22fa28f 100644 --- a/mammoth/docx/numbering_xml.py +++ b/mammoth/docx/numbering_xml.py @@ -101,18 +101,25 @@ class Numbering(object): self._styles = styles def find_level(self, num_id, level): + return self._find_level_with_seen_num_ids(num_id, level, seen_num_ids=set()) + + def _find_level_with_seen_num_ids(self, num_id, level, seen_num_ids): + if num_id in seen_num_ids: + return None + seen_num_ids.add(num_id) + num = self._nums.get(num_id) if num is None: return None + + abstract_num = self._abstract_nums.get(num.abstract_num_id) + if abstract_num is None: + return None + elif abstract_num.num_style_link is None: + return self._to_numbering_level(abstract_num.levels.get(level)) else: - abstract_num = self._abstract_nums.get(num.abstract_num_id) - if abstract_num is None: - return None - elif abstract_num.num_style_link is None: - return self._to_numbering_level(abstract_num.levels.get(level)) - else: - style = self._styles.find_numbering_style_by_id(abstract_num.num_style_link) - return self.find_level(style.num_id, level) + style = self._styles.find_numbering_style_by_id(abstract_num.num_style_link) + return self._find_level_with_seen_num_ids(style.num_id, level, seen_num_ids=seen_num_ids) def find_level_by_paragraph_style_id(self, style_id): return self._levels_by_paragraph_style_id.get(style_id) diff --git a/tests/docx/numbering_xml_tests.py b/tests/docx/numbering_xml_tests.py index 6cc779f..52aad5d 100644 --- a/tests/docx/numbering_xml_tests.py +++ b/tests/docx/numbering_xml_tests.py @@ -154,6 +154,23 @@ def test_when_abstract_num_has_num_style_link_then_style_is_used_to_find_num(): assert_equal(True, numbering.find_level("201", "0").is_ordered) +def test_when_abstract_num_has_self_recursive_num_style_link_then_level_is_not_found(): + numbering = _read_numbering_xml_element( + xml_element("w:numbering", {}, [ + xml_element("w:abstractNum", {"w:abstractNumId": "100"}, [ + xml_element("w:numStyleLink", {"w:val": "List1"}), + ]), + xml_element("w:num", {"w:numId": "200"}, [ + xml_element("w:abstractNumId", {"w:val": "100"}), + ]), + ]), + styles=Styles.create(numbering_styles={ + "List1": NumberingStyle(style_id="List1", num_id="200"), + }), + ) + assert_equal(None, numbering.find_level("200", "0")) + + # See: 17.9.23 pStyle (Paragraph Style's Associated Numbering Level) in ECMA-376, 4th Edition def test_numbering_level_can_be_found_by_paragraph_style_id(): numbering = _read_numbering_xml_element( |
