aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS4
-rw-r--r--mammoth/docx/body_xml.py13
-rw-r--r--mammoth/docx/numbering_xml.py34
-rw-r--r--tests/docx/body_xml_tests.py38
-rw-r--r--tests/docx/numbering_xml_tests.py24
5 files changed, 105 insertions, 8 deletions
diff --git a/NEWS b/NEWS
index bbe5300..1dc7e89 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,7 @@
+# 1.4.14
+
+* Support numbering defined by paragraph style.
+
# 1.4.13
* Add style mapping for all caps.
diff --git a/mammoth/docx/body_xml.py b/mammoth/docx/body_xml.py
index 17cf31b..d4fdccf 100644
--- a/mammoth/docx/body_xml.py
+++ b/mammoth/docx/body_xml.py
@@ -131,7 +131,6 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f
def paragraph(element):
properties = element.find_child_or_null("w:pPr")
alignment = properties.find_child_or_null("w:jc").attributes.get("w:val")
- numbering = _read_numbering_properties(properties.find_child_or_null("w:numPr"))
indent = _read_paragraph_indent(properties.find_child_or_null("w:ind"))
return _ReadResult.map_results(
@@ -141,7 +140,10 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f
children=children,
style_id=style[0],
style_name=style[1],
- numbering=numbering,
+ numbering=_read_numbering_properties(
+ paragraph_style_id=style[0],
+ element=properties.find_child_or_null("w:numPr"),
+ ),
alignment=alignment,
indent=indent,
)).append_extra()
@@ -209,7 +211,12 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f
def _undefined_style_warning(style_type, style_id):
return results.warning("{0} style with ID {1} was referenced but not defined in the document".format(style_type, style_id))
- def _read_numbering_properties(element):
+ def _read_numbering_properties(paragraph_style_id, element):
+ if paragraph_style_id is not None:
+ level = numbering.find_level_by_paragraph_style_id(paragraph_style_id)
+ if level is not None:
+ return level
+
num_id = element.find_child_or_null("w:numId").attributes.get("w:val")
level_index = element.find_child_or_null("w:ilvl").attributes.get("w:val")
if num_id is None or level_index is None:
diff --git a/mammoth/docx/numbering_xml.py b/mammoth/docx/numbering_xml.py
index af3d1d5..e1ea0b4 100644
--- a/mammoth/docx/numbering_xml.py
+++ b/mammoth/docx/numbering_xml.py
@@ -28,6 +28,13 @@ class _AbstractNum(object):
num_style_link = cobble.field()
+@cobble.data
+class _AbstractNumLevel(object):
+ level_index = cobble.field()
+ is_ordered = cobble.field()
+ paragraph_style_id = cobble.field()
+
+
def _read_abstract_num_levels(element):
levels = map(_read_abstract_num_level, element.find_children("w:lvl"))
return dict(
@@ -40,7 +47,12 @@ def _read_abstract_num_level(element):
level_index = element.attributes["w:ilvl"]
num_fmt = element.find_child_or_null("w:numFmt").attributes.get("w:val")
is_ordered = num_fmt != "bullet"
- return numbering_level(level_index, is_ordered)
+ paragraph_style_id = element.find_child_or_null("w:pStyle").attributes.get("w:val")
+ return _AbstractNumLevel(
+ level_index=level_index,
+ is_ordered=is_ordered,
+ paragraph_style_id=paragraph_style_id,
+ )
def _read_nums(element):
@@ -65,6 +77,12 @@ class _Num(object):
class Numbering(object):
def __init__(self, abstract_nums, nums, styles):
self._abstract_nums = abstract_nums
+ self._levels_by_paragraph_style_id = dict(
+ (level.paragraph_style_id, self._to_numbering_level(level))
+ for abstract_num in abstract_nums.values()
+ for level in abstract_num.levels.values()
+ if level.paragraph_style_id is not None
+ )
self._nums = nums
self._styles = styles
@@ -75,10 +93,22 @@ class Numbering(object):
else:
abstract_num = self._abstract_nums[num.abstract_num_id]
if abstract_num.num_style_link is None:
- return abstract_num.levels.get(level)
+ 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)
+ def find_level_by_paragraph_style_id(self, style_id):
+ return self._levels_by_paragraph_style_id.get(style_id)
+
+ def _to_numbering_level(self, abstract_num_level):
+ if abstract_num_level is None:
+ return None
+ else:
+ return numbering_level(
+ level_index=abstract_num_level.level_index,
+ is_ordered=abstract_num_level.is_ordered,
+ )
+
Numbering.EMPTY = Numbering(abstract_nums={}, nums={}, styles=Styles.EMPTY)
diff --git a/tests/docx/body_xml_tests.py b/tests/docx/body_xml_tests.py
index 20f89a9..0f47ca2 100644
--- a/tests/docx/body_xml_tests.py
+++ b/tests/docx/body_xml_tests.py
@@ -9,6 +9,7 @@ import funk
from mammoth import documents, results
from mammoth.docx.xmlparser import element as xml_element, text as xml_text
from mammoth.docx import body_xml
+from mammoth.docx.numbering_xml import Numbering
from mammoth.docx.relationships_xml import Relationships, Relationship
from mammoth.docx.styles_xml import Styles, Style
from .document_matchers import (
@@ -114,6 +115,26 @@ class ParagraphTests(object):
assert_equal(True, paragraph.numbering.is_ordered)
@istest
+ def numbering_on_paragraph_style_takes_precedence_over_numpr(self):
+ numbering_properties_xml = xml_element("w:numPr", {}, [
+ xml_element("w:ilvl", {"w:val": "1"}),
+ xml_element("w:numId", {"w:val": "42"}),
+ ])
+ properties_xml = xml_element("w:pPr", {}, [
+ xml_element("w:pStyle", {"w:val": "List"}),
+ numbering_properties_xml,
+ ])
+ paragraph_xml = xml_element("w:p", {}, [properties_xml])
+
+ numbering = _NumberingMap(
+ nums={"42": {"1": documents.numbering_level("1", False)}},
+ levels_by_paragraph_style_id={"List": documents.numbering_level("1", True)}
+ )
+ paragraph = _read_and_get_document_xml_element(paragraph_xml, numbering=numbering)
+
+ assert_equal(True, paragraph.numbering.is_ordered)
+
+ @istest
def numbering_properties_are_ignored_if_lvl_is_missing(self):
paragraph_xml = self._paragraph_with_numbering_properties([
xml_element("w:numId", {"w:val": "42"}),
@@ -1200,8 +1221,9 @@ def _read_and_get_document_xml_elements(*args, **kwargs):
def _read_and_get_document_xml(func, *args, **kwargs):
+ numbering = kwargs.pop("numbering", Numbering.EMPTY)
styles = kwargs.pop("styles", FakeStyles())
- result = _read_document_xml(func, *args, styles=styles, **kwargs)
+ result = _read_document_xml(func, *args, numbering=numbering, styles=styles, **kwargs)
assert_equal([], result.messages)
return result.value
@@ -1221,7 +1243,8 @@ def _read_document_xml_elements(*args, **kwargs):
def _read_document_xml(func, element, *args, **kwargs):
- reader = body_xml.reader(*args, **kwargs)
+ numbering = kwargs.pop("numbering", Numbering.EMPTY)
+ reader = body_xml.reader(*args, numbering=numbering, **kwargs)
return func(reader, element)
@@ -1338,8 +1361,17 @@ def _image_relationship(relationship_id, target):
class _NumberingMap(object):
- def __init__(self, nums):
+ def __init__(self, nums=None, levels_by_paragraph_style_id=None):
+ if nums is None:
+ nums = {}
+ if levels_by_paragraph_style_id is None:
+ levels_by_paragraph_style_id = {}
+
self._nums = nums
+ self._levels_by_paragraph_style_id = levels_by_paragraph_style_id
def find_level(self, num_id, level):
return self._nums[num_id][level]
+
+ 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 b707ba1..45102c7 100644
--- a/tests/docx/numbering_xml_tests.py
+++ b/tests/docx/numbering_xml_tests.py
@@ -74,6 +74,30 @@ def when_abstract_num_has_num_style_link_then_style_is_used_to_find_num():
assert_equal(True, numbering.find_level("201", "0").is_ordered)
+# See: 17.9.23 pStyle (Paragraph Style's Associated Numbering Level) in ECMA-376, 4th Edition
+@istest
+def numbering_level_can_be_found_by_paragraph_style_id():
+ numbering = _read_numbering_xml_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:abstractNum", {"w:abstractNumId": "43"}, [
+ xml_element("w:lvl", {"w:ilvl": "0"}, [
+ xml_element("w:pStyle", {"w:val": "List"}),
+ xml_element("w:numFmt", {"w:val": "decimal"}),
+ ]),
+ ]),
+ ]),
+ styles=Styles.create(numbering_styles={"List1": NumberingStyle(num_id="200")}),
+ )
+
+ assert_equal(True, numbering.find_level_by_paragraph_style_id("List").is_ordered)
+ assert_equal(None, numbering.find_level_by_paragraph_style_id("Paragraph"))
+
+
def _read_numbering_xml_element(element, styles=None):
if styles is None:
styles = Styles.EMPTY