diff options
| author | Michael Williamson <mike@zwobble.org> | 2025-09-11 15:21:40 +0100 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2025-09-11 15:21:40 +0100 |
| commit | 82bc0fe4769483196ce8cc8147fab16b38c52e8f (patch) | |
| tree | e53ef888e6d880ce76a3843a479231ba36de550e | |
| parent | 8216e5ae1770955ad2400cee764c7f8ec56483bf (diff) | |
Handle numbering levels defined without an index
| -rw-r--r-- | NEWS | 2 | ||||
| -rw-r--r-- | mammoth/docx/body_xml.py | 6 | ||||
| -rw-r--r-- | mammoth/docx/numbering_xml.py | 26 | ||||
| -rw-r--r-- | tests/docx/body_xml_tests.py | 6 | ||||
| -rw-r--r-- | tests/docx/numbering_xml_tests.py | 57 |
5 files changed, 88 insertions, 9 deletions
@@ -6,6 +6,8 @@ * Support disabling external file accesses using the external_file_access argument. +* Handle numbering levels defined without an index. + # 1.10.0 * Add "Heading" and "Body" styles, as found in documents created by Apple Pages, diff --git a/mammoth/docx/body_xml.py b/mammoth/docx/body_xml.py index d3dd267..e2a490a 100644 --- a/mammoth/docx/body_xml.py +++ b/mammoth/docx/body_xml.py @@ -288,6 +288,12 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f if level is not None: return level + # Some malformed documents define numbering levels without an index, and + # reference the numbering using a w:numPr element without a w:ilvl child. + # To handle such cases, we assume a level of 0 as a fallback. + if num_id is not None: + return numbering.find_level(num_id, "0") + return None def _read_paragraph_indent(element): diff --git a/mammoth/docx/numbering_xml.py b/mammoth/docx/numbering_xml.py index 7f7bf0e..4614d0e 100644 --- a/mammoth/docx/numbering_xml.py +++ b/mammoth/docx/numbering_xml.py @@ -36,15 +36,29 @@ class _AbstractNumLevel(object): def _read_abstract_num_levels(element): - levels = map(_read_abstract_num_level, element.find_children("w:lvl")) - return dict( - (level.level_index, level) - for level in levels - ) + levels = {} + + # Some malformed documents define numbering levels without an index, and + # reference the numbering using a w:numPr element without a w:ilvl child. + # To handle such cases, we assume a level of 0 as a fallback. + level_without_index = None + + for level_element in element.find_children("w:lvl"): + level = _read_abstract_num_level(level_element) + if level.level_index is None: + level.level_index = "0" + level_without_index = level + else: + levels[level.level_index] = level + + if "0" not in levels and level_without_index is not None: + levels[level_without_index.level_index] = level_without_index + + return levels def _read_abstract_num_level(element): - level_index = element.attributes["w:ilvl"] + level_index = element.attributes.get("w:ilvl") num_fmt = element.find_child_or_null("w:numFmt").attributes.get("w:val") is_ordered = num_fmt != "bullet" paragraph_style_id = element.find_child_or_null("w:pStyle").attributes.get("w:val") diff --git a/tests/docx/body_xml_tests.py b/tests/docx/body_xml_tests.py index e9decc4..5d9766b 100644 --- a/tests/docx/body_xml_tests.py +++ b/tests/docx/body_xml_tests.py @@ -138,15 +138,15 @@ class ParagraphTests(object): assert_equal("1", paragraph.numbering.level_index) - def test_numbering_properties_are_ignored_if_lvl_is_missing(self): + def test_when_numbering_properties_are_missing_level_then_level_of_0_is_assumed(self): paragraph_xml = self._paragraph_with_numbering_properties([ xml_element("w:numId", {"w:val": "42"}), ]) - numbering = _NumberingMap({"42": {"1": documents.numbering_level("1", True)}}) + numbering = _NumberingMap({"42": {"0": documents.numbering_level("0", True)}}) paragraph = _read_and_get_document_xml_element(paragraph_xml, numbering=numbering) - assert_equal(None, paragraph.numbering) + assert_equal(documents.numbering_level("0", True), paragraph.numbering) def test_numbering_properties_are_ignored_if_num_id_is_missing(self): paragraph_xml = self._paragraph_with_numbering_properties([ diff --git a/tests/docx/numbering_xml_tests.py b/tests/docx/numbering_xml_tests.py index 6f1e447..6cc779f 100644 --- a/tests/docx/numbering_xml_tests.py +++ b/tests/docx/numbering_xml_tests.py @@ -72,6 +72,63 @@ def test_num_referencing_non_existent_abstract_num_is_ignored(): assert_equal(None, numbering.find_level("47", "0")) +def test_given_no_other_levels_with_index_of_0_when_level_is_missing_ilvl_then_level_index_is_0(): + element = xml_element("w:numbering", {}, [ + xml_element("w:abstractNum", {"w:abstractNumId": "42"}, [ + xml_element("w:lvl", {}, [ + xml_element("w:numFmt", {"w:val": "decimal"}), + ]), + ]), + xml_element("w:num", {"w:numId": "47"}, [ + xml_element("w:abstractNumId", {"w:val": "42"}) + ]) + ]) + + numbering = _read_numbering_xml_element(element) + + assert_equal(True, numbering.find_level("47", "0").is_ordered) + + +def test_given_previous_other_level_with_index_of_0_when_level_is_missing_ilvl_then_level_is_ignored(): + element = xml_element("w:numbering", {}, [ + xml_element("w:abstractNum", {"w:abstractNumId": "42"}, [ + xml_element("w:lvl", {"w:ilvl": "0"}, [ + xml_element("w:numFmt", {"w:val": "bullet"}), + ]), + xml_element("w:lvl", {}, [ + xml_element("w:numFmt", {"w:val": "decimal"}), + ]), + ]), + xml_element("w:num", {"w:numId": "47"}, [ + xml_element("w:abstractNumId", {"w:val": "42"}) + ]) + ]) + + numbering = _read_numbering_xml_element(element) + + assert_equal(False, numbering.find_level("47", "0").is_ordered) + + +def test_given_subsequent_other_level_with_index_of_0_when_level_is_missing_ilvl_then_level_is_ignored(): + element = xml_element("w:numbering", {}, [ + xml_element("w:abstractNum", {"w:abstractNumId": "42"}, [ + xml_element("w:lvl", {}, [ + xml_element("w:numFmt", {"w:val": "decimal"}), + ]), + xml_element("w:lvl", {"w:ilvl": "0"}, [ + xml_element("w:numFmt", {"w:val": "bullet"}), + ]), + ]), + xml_element("w:num", {"w:numId": "47"}, [ + xml_element("w:abstractNumId", {"w:val": "42"}) + ]) + ]) + + numbering = _read_numbering_xml_element(element) + + assert_equal(False, numbering.find_level("47", "0").is_ordered) + + def test_when_abstract_num_has_num_style_link_then_style_is_used_to_find_num(): numbering = _read_numbering_xml_element( xml_element("w:numbering", {}, [ |
