diff options
| author | Michael Williamson <mike@zwobble.org> | 2021-01-09 13:52:02 +0000 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2021-01-09 13:52:02 +0000 |
| commit | 801b505840b698858c48761c640d52a5fa5ec3fb (patch) | |
| tree | 00285ea140888c94bde0c17d1cc061b521ca29f0 | |
| parent | d1910bc1e649deb9f0f69114b61023458f3d8d2f (diff) | |
Support numbering defined by paragraph style
| -rw-r--r-- | NEWS | 4 | ||||
| -rw-r--r-- | mammoth/docx/body_xml.py | 13 | ||||
| -rw-r--r-- | mammoth/docx/numbering_xml.py | 34 | ||||
| -rw-r--r-- | tests/docx/body_xml_tests.py | 38 | ||||
| -rw-r--r-- | tests/docx/numbering_xml_tests.py | 24 |
5 files changed, 105 insertions, 8 deletions
@@ -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 |
