aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2025-09-11 15:21:40 +0100
committerMichael Williamson <mike@zwobble.org>2025-09-11 15:21:40 +0100
commit82bc0fe4769483196ce8cc8147fab16b38c52e8f (patch)
treee53ef888e6d880ce76a3843a479231ba36de550e
parent8216e5ae1770955ad2400cee764c7f8ec56483bf (diff)
Handle numbering levels defined without an index
-rw-r--r--NEWS2
-rw-r--r--mammoth/docx/body_xml.py6
-rw-r--r--mammoth/docx/numbering_xml.py26
-rw-r--r--tests/docx/body_xml_tests.py6
-rw-r--r--tests/docx/numbering_xml_tests.py57
5 files changed, 88 insertions, 9 deletions
diff --git a/NEWS b/NEWS
index b4003c2..3651f9b 100644
--- a/NEWS
+++ b/NEWS
@@ -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", {}, [