From bc8c341b37bfac34bbeaccdd5368c2d7edc309ac Mon Sep 17 00:00:00 2001 From: Bruno Herfst Date: Wed, 26 Oct 2016 11:04:09 +1100 Subject: Add support for page and column breaks --- mammoth/conversion.py | 30 +++++++++++++++---- mammoth/document_matchers.py | 10 ++++++- mammoth/documents.py | 11 ++++--- mammoth/docx/body_xml.py | 19 ++++++++---- mammoth/styles/parser/document_matcher_parser.py | 20 +++++++++++++ tests/conversion_tests.py | 20 +++++++++++-- tests/docx/body_xml_tests.py | 35 ++++++++++++++++------ .../styles/parser/document_matcher_parser_tests.py | 27 +++++++++++++++++ 8 files changed, 144 insertions(+), 28 deletions(-) diff --git a/mammoth/conversion.py b/mammoth/conversion.py index 9398c5c..bae503a 100644 --- a/mammoth/conversion.py +++ b/mammoth/conversion.py @@ -132,6 +132,7 @@ class _DocumentConverter(documents.ElementVisitor): else: return html_paths.empty + def visit_text(self, text): return [html.text(text.value)] @@ -176,11 +177,22 @@ class _DocumentConverter(documents.ElementVisitor): return [ html.element("td", attributes, nodes) ] - - - def visit_line_break(self, line_break): - return [html.element("br")] - + + + def visit_break(self, break_): + return self._find_html_path_for_break(break_).wrap(lambda: []) + + + def _find_html_path_for_break(self, break_): + style = self._find_style(break_, "break") + if style is not None: + return style.html_path + elif break_.break_type == "line": + return html_paths.path([html_paths.element("br", fresh=True)]) + else: + return html_paths.empty + + def visit_note_reference(self, note_reference): self._note_references.append(note_reference) note_number = len(self._note_references) @@ -192,6 +204,7 @@ class _DocumentConverter(documents.ElementVisitor): }, [html.text("[{0}]".format(note_number))]) ]) ] + def visit_note(self, note): note_body = self._visit_all(note.body) + [ @@ -300,7 +313,12 @@ class _DocumentConverter(documents.ElementVisitor): def _document_matcher_matches(matcher, element, element_type): if matcher.element_type in ["underline", "strikethrough", "bold", "italic", "comment_reference"]: return matcher.element_type == element_type - else: + elif matcher.element_type == "break": + return ( + matcher.element_type == element_type and + matcher.break_type == element.break_type + ) + else: # matcher.element_type in ["paragraph", "run"]: return ( matcher.element_type == element_type and ( matcher.style_id is None or diff --git a/mammoth/document_matchers.py b/mammoth/document_matchers.py index d1f8e74..faccb50 100644 --- a/mammoth/document_matchers.py +++ b/mammoth/document_matchers.py @@ -39,6 +39,15 @@ class comment_reference(object): element_type = "comment_reference" +BreakMatcher = collections.namedtuple("BreakMatcher", ["break_type"]) +BreakMatcher.element_type = "break" + + +line_break = BreakMatcher("line") +page_break = BreakMatcher("page") +column_break = BreakMatcher("column") + + def equal_to(value): return StringMatcher(_operator_equal_to, value) @@ -50,7 +59,6 @@ def _operator_equal_to(first, second): def starts_with(value): return StringMatcher(_operator_starts_with, value) - def _operator_starts_with(first, second): return second.upper().startswith(first.upper()) diff --git a/mammoth/documents.py b/mammoth/documents.py index 2767f86..327380d 100644 --- a/mammoth/documents.py +++ b/mammoth/documents.py @@ -53,8 +53,13 @@ class TableCell(HasChildren): rowspan = cobble.field() @cobble.data -class LineBreak(Element): - pass +class Break(Element): + break_type = cobble.field() + +line_break = Break("line") +page_break = Break("page") +column_break = Break("column") + @cobble.data class Tab(Element): @@ -123,8 +128,6 @@ def table_cell(children, colspan=None, rowspan=None): return TableCell(children=children, colspan=colspan, rowspan=rowspan) -line_break = LineBreak - def numbering_level(level_index, is_ordered): return _NumberingLevel(str(level_index), bool(is_ordered)) diff --git a/mammoth/docx/body_xml.py b/mammoth/docx/body_xml.py index 22d8440..1fcd567 100644 --- a/mammoth/docx/body_xml.py +++ b/mammoth/docx/body_xml.py @@ -253,7 +253,8 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f return children_result.map(lambda children: documents.hyperlink(anchor=anchor, children=children)) else: return children_result - + + def bookmark_start(element): name = element.attributes.get("w:name") if name == "_GoBack": @@ -262,13 +263,19 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f return _success(documents.bookmark(name)) - def br(element): + def break_(element): break_type = element.attributes.get("w:type") - if break_type: + + if not break_type or break_type == "textWrapping": + return _success(documents.line_break) + elif break_type == "page": + return _success(documents.page_break) + elif break_type == "column": + return _success(documents.column_break) + else: warning = results.warning("Unsupported break type: {0}".format(break_type)) return _empty_result_with_message(warning) - else: - return _success(documents.line_break()) + def inline(element): properties = element.find_child_or_null("wp:docPr").attributes @@ -372,7 +379,7 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file, f "w:pict": pict, "w:hyperlink": hyperlink, "w:bookmarkStart": bookmark_start, - "w:br": br, + "w:br": break_, "wp:inline": inline, "wp:anchor": inline, "v:imagedata": read_imagedata, diff --git a/mammoth/styles/parser/document_matcher_parser.py b/mammoth/styles/parser/document_matcher_parser.py index 7f117b7..36999d0 100644 --- a/mammoth/styles/parser/document_matcher_parser.py +++ b/mammoth/styles/parser/document_matcher_parser.py @@ -40,6 +40,9 @@ def parse_document_matcher(tokens): elif tokens.try_skip(TokenType.IDENTIFIER, "comment-reference"): return document_matchers.comment_reference + elif tokens.try_skip(TokenType.IDENTIFIER, "br"): + return _parse_break(tokens) + else: raise LineParseError("Unrecognised document element: {0}".format(tokens.next_value(TokenType.IDENTIFIER))) @@ -78,3 +81,20 @@ def _parse_list_type(tokens): return False else: raise LineParseError("Unrecognised list type: {0}".format(list_type)) + + +def _parse_break(tokens): + tokens.skip(TokenType.SYMBOL, "[") + tokens.skip(TokenType.IDENTIFIER, "type") + tokens.skip(TokenType.SYMBOL, "=") + type_name = parse_string(tokens) + tokens.skip(TokenType.SYMBOL, "]"); + + if type_name == "line": + return document_matchers.line_break + elif type_name == "page": + return document_matchers.page_break + elif type_name == "column": + return document_matchers.column_break + else: + raise LineParseError("Unrecognised break type: {0}".format(type_name)) diff --git a/tests/conversion_tests.py b/tests/conversion_tests.py index 71fbbc7..3dfb9f3 100644 --- a/tests/conversion_tests.py +++ b/tests/conversion_tests.py @@ -365,11 +365,27 @@ def table_cells_are_written_with_rowspan_if_not_equal_to_one(): @istest def line_break_is_converted_to_br(): - line_break = documents.line_break() - result = convert_document_element_to_html(line_break) + result = convert_document_element_to_html(documents.line_break) assert_equal("
", result.value) +@istest +def breaks_that_are_not_line_breaks_are_ignored(): + result = convert_document_element_to_html(documents.page_break) + assert_equal("", result.value) + + +@istest +def breaks_can_be_mapped_using_style_mappings(): + result = convert_document_element_to_html( + documents.page_break, + style_map=[ + _style_mapping("br[type='page'] => hr") + ], + ) + assert_equal("
", result.value) + + @istest def images_are_converted_to_img_tags_with_data_uri(): image = documents.image(alt_text=None, content_type="image/png", open=lambda: io.BytesIO(b"abc")) diff --git a/tests/docx/body_xml_tests.py b/tests/docx/body_xml_tests.py index ccbd888..02d8b85 100644 --- a/tests/docx/body_xml_tests.py +++ b/tests/docx/body_xml_tests.py @@ -422,22 +422,39 @@ class ReadXmlElementTests(object): documents.bookmark("start"), _read_and_get_document_xml_element(element) ) - + @istest - def br_is_read_as_line_break(self): + def br_without_explicit_type_is_read_as_line_break(self): break_element = xml_element("w:br", {}, []) - result = _read_and_get_document_xml_element(break_element) - assert_equal(result, documents.line_break()) - - + result = _read_document_xml_element(break_element) + assert_equal(documents.line_break, result.value) + + @istest + def br_with_text_wrapping_type_is_read_as_line_break(self): + break_element = xml_element("w:br", {"w:type": "textWrapping"}, []) + result = _read_document_xml_element(break_element) + assert_equal(documents.line_break, result.value) + @istest - def warning_on_breaks_that_arent_line_breaks(self): + def br_with_page_type_is_read_as_page_break(self): break_element = xml_element("w:br", {"w:type": "page"}, []) result = _read_document_xml_element(break_element) - expected_warning = results.warning("Unsupported break type: page") + assert_equal(documents.page_break, result.value) + + @istest + def br_with_column_type_is_read_as_column_break(self): + break_element = xml_element("w:br", {"w:type": "column"}, []) + result = _read_document_xml_element(break_element) + assert_equal(documents.column_break, result.value) + + @istest + def warning_on_break_types_that_arent_recognised(self): + break_element = xml_element("w:br", {"w:type": "unknownBreakType"}, []) + result = _read_document_xml_element(break_element) + expected_warning = results.warning("Unsupported break type: unknownBreakType") assert_equal([expected_warning], result.messages) assert_equal(None, result.value) - + IMAGE_BYTES = b"Not an image at all!" IMAGE_RELATIONSHIP_ID = "rId5" diff --git a/tests/styles/parser/document_matcher_parser_tests.py b/tests/styles/parser/document_matcher_parser_tests.py index 194d6fa..0286e7b 100644 --- a/tests/styles/parser/document_matcher_parser_tests.py +++ b/tests/styles/parser/document_matcher_parser_tests.py @@ -132,6 +132,33 @@ def reads_comment_reference(): read_document_matcher("comment-reference") ) +@istest +def reads_line_breaks(): + assert_equal( + document_matchers.line_break, + read_document_matcher("br[type='line']"), + ) + +@istest +def reads_page_breaks(): + assert_equal( + document_matchers.page_break, + read_document_matcher("br[type='page']"), + ) + +@istest +def reads_column_breaks(): + assert_equal( + document_matchers.column_break, + read_document_matcher("br[type='column']"), + ) + + +@istest +def unrecognised_break_type_raises_error(): + error = assert_raises(LineParseError, lambda: read_document_matcher("br[type='unknownBreakType']")) + assert_equal("Unrecognised break type: unknownBreakType", str(error)) + def read_document_matcher(string): return parse_document_matcher(TokenIterator(tokenise(string))) -- cgit v1.2.3