diff options
| author | Michael Williamson <mike@zwobble.org> | 2019-01-27 09:25:29 +0000 |
|---|---|---|
| committer | Michael Williamson <mike@zwobble.org> | 2019-01-27 09:25:29 +0000 |
| commit | 2337bf2362d49efb53ba80c4d61e186191ae3d20 (patch) | |
| tree | 03548225de5b283d0725293b8d540c9cf84e4739 | |
| parent | 3f5c9d2893c87fa58be817daf4f5f9d093e2b880 (diff) | |
Preserve empty table rows
| -rw-r--r-- | NEWS | 8 | ||||
| -rw-r--r-- | mammoth/conversion.py | 80 | ||||
| -rw-r--r-- | tests/conversion_tests.py | 46 |
3 files changed, 77 insertions, 57 deletions
@@ -1,3 +1,7 @@ +# 1.4.8 + +* Preserve empty table rows. + # 1.4.7 * Always write files as UTF-8 in the CLI. @@ -95,7 +99,7 @@ * Use alt text title as alt text for images when the alt text description is blank or missing. - + # 1.1.1 * Handle comments without author initials. @@ -298,5 +302,5 @@ * Allow paragraphs and runs to be matched by style name. For instance, to match a paragraph with the style name `Heading 1`: - + p[style-name='Heading 1'] diff --git a/mammoth/conversion.py b/mammoth/conversion.py index eb514ec..d463116 100644 --- a/mammoth/conversion.py +++ b/mammoth/conversion.py @@ -17,16 +17,16 @@ def convert_document_element_to_html(element, id_prefix=None, output_format=None, ignore_empty_paragraphs=True): - + if style_map is None: style_map = [] - + if id_prefix is None: id_prefix = "" - + if convert_image is None: convert_image = images.data_uri - + if isinstance(element, documents.Document): comments = dict( (comment.comment_id, comment) @@ -47,7 +47,7 @@ def convert_document_element_to_html(element, ) context = _ConversionContext(is_table_header=False) nodes = converter.visit(element, context) - + writer = writers.writer(output_format) html.write(writer, html.collapse(html.strip_empty(nodes))) return results.Result(writer.as_string(), messages) @@ -56,7 +56,7 @@ def convert_document_element_to_html(element, @cobble.data class _ConversionContext(object): is_table_header = cobble.field() - + def copy(self, **kwargs): return cobble.copy(self, **kwargs) @@ -71,7 +71,7 @@ class _DocumentConverter(documents.element_visitor(args=1)): self._referenced_comments = [] self._convert_image = convert_image self._comments = comments - + def visit_image(self, image, context): try: return self._convert_image(image) @@ -101,7 +101,7 @@ class _DocumentConverter(documents.element_visitor(args=1)): return content else: return [html.force_write] + content - + html_path = self._find_html_path_for_paragraph(paragraph) return html_path.wrap(children) @@ -129,8 +129,8 @@ class _DocumentConverter(documents.element_visitor(args=1)): nodes = partial(path.wrap, nodes) return nodes() - - + + def _find_style_for_run_property(self, element_type, default=None): style = self._find_style(None, element_type) if style is not None: @@ -143,30 +143,30 @@ class _DocumentConverter(documents.element_visitor(args=1)): def visit_text(self, text, context): return [html.text(text.value)] - - + + def visit_hyperlink(self, hyperlink, context): if hyperlink.anchor is None: href = hyperlink.href else: href = "#{0}".format(self._html_id(hyperlink.anchor)) - + attributes = {"href": href} if hyperlink.target_frame is not None: attributes["target"] = hyperlink.target_frame - + nodes = self._visit_all(hyperlink.children, context) return [html.collapsible_element("a", attributes, nodes)] - - + + def visit_bookmark(self, bookmark, context): element = html.collapsible_element( "a", {"id": self._html_id(bookmark.name)}, [html.force_write]) return [element] - - + + def visit_tab(self, tab, context): return [html.text("\t")] @@ -183,7 +183,7 @@ class _DocumentConverter(documents.element_visitor(args=1)): ) if body_index is None: body_index = len(table.children) - + if body_index == 0: children = self._visit_all(table.children, context.copy(is_table_header=False)) else: @@ -193,14 +193,14 @@ class _DocumentConverter(documents.element_visitor(args=1)): html.element("thead", {}, head_rows), html.element("tbody", {}, body_rows), ] - + return [html.force_write] + children - - + + def visit_table_row(self, table_row, context): - return [html.element("tr", {}, self._visit_all(table_row.children, context))] - - + return [html.element("tr", {}, [html.force_write] + self._visit_all(table_row.children, context))] + + def visit_table_cell(self, table_cell, context): if context.is_table_header: tag_name = "th" @@ -243,7 +243,7 @@ class _DocumentConverter(documents.element_visitor(args=1)): ]) ] - + def visit_note(self, note, context): note_body = self._visit_all(note.body, context) + [ html.collapsible_element("p", {}, [ @@ -271,15 +271,15 @@ class _DocumentConverter(documents.element_visitor(args=1)): "id": self._reference_html_id("comment", reference.comment_id), }, [html.text(label)]) ] - + html_path = self._find_html_path( None, "comment_reference", default=html_paths.ignore, ) - + return html_path.wrap(nodes) - + def visit_comment(self, referenced_comment, context): label, comment = referenced_comment # TODO remove duplication with notes @@ -312,24 +312,24 @@ class _DocumentConverter(documents.element_visitor(args=1)): def _find_html_path_for_paragraph(self, paragraph): default = html_paths.path([html_paths.element("p", fresh=True)]) return self._find_html_path(paragraph, "paragraph", default, warn_unrecognised=True) - + def _find_html_path_for_run(self, run): return self._find_html_path(run, "run", default=html_paths.empty, warn_unrecognised=True) - - + + def _find_html_path(self, element, element_type, default, warn_unrecognised=False): style = self._find_style(element, element_type) if style is not None: return style.html_path - + if warn_unrecognised and getattr(element, "style_id", None) is not None: self._messages.append(results.warning( "Unrecognised {0} style: {1} (Style ID: {2})".format( element_type, element.style_name, element.style_id) )) - + return default - + def _find_style(self, element, element_type): for style in self._style_map: document_matcher = style.document_matcher @@ -338,19 +338,19 @@ class _DocumentConverter(documents.element_visitor(args=1)): def _note_html_id(self, note): return self._referent_html_id(note.note_type, note.note_id) - + def _note_ref_html_id(self, note): return self._reference_html_id(note.note_type, note.note_id) - + def _referent_html_id(self, reference_type, reference_id): return self._html_id("{0}-{1}".format(reference_type, reference_id)) - + def _reference_html_id(self, reference_type, reference_id): return self._html_id("{0}-ref-{1}".format(reference_type, reference_id)) - + def _html_id(self, suffix): return "{0}{1}".format(self._id_prefix, suffix) - + def _document_matcher_matches(matcher, element, element_type): if matcher.element_type in ["underline", "strikethrough", "small_caps", "bold", "italic", "comment_reference"]: diff --git a/tests/conversion_tests.py b/tests/conversion_tests.py index a2b1a0b..234c7ef 100644 --- a/tests/conversion_tests.py +++ b/tests/conversion_tests.py @@ -99,7 +99,7 @@ def default_paragraph_style_is_specified_by_mapping_plain_paragraphs(): ] ) assert_equal('<p class="tip">Tip</p>', result.value) - + @istest def warning_is_emitted_if_paragraph_style_is_unrecognised(): @@ -111,7 +111,7 @@ def warning_is_emitted_if_paragraph_style_is_unrecognised(): ), ) assert_equal([results.warning("Unrecognised paragraph style: Heading 1 (Style ID: Heading1)")], result.messages) - + @istest def no_warning_if_there_is_no_style_for_plain_paragraphs(): @@ -145,7 +145,7 @@ def bulleted_styles_dont_match_plain_paragraph(): ] ) assert_equal('<p>Hello</p>', result.value) - + @istest def bold_runs_are_wrapped_in_strong_tags_by_default(): @@ -153,7 +153,7 @@ def bold_runs_are_wrapped_in_strong_tags_by_default(): documents.run(children=[documents.text("Hello")], is_bold=True), ) assert_equal("<strong>Hello</strong>", result.value) - + @istest def bold_runs_can_be_configured_with_style_mapping(): @@ -162,7 +162,7 @@ def bold_runs_can_be_configured_with_style_mapping(): style_map=[_style_mapping("b => em")] ) assert_equal("<em>Hello</em>", result.value) - + @istest def italic_runs_are_wrapped_in_emphasis_tags_by_default(): @@ -170,7 +170,7 @@ def italic_runs_are_wrapped_in_emphasis_tags_by_default(): documents.run(children=[documents.text("Hello")], is_italic=True), ) assert_equal("<em>Hello</em>", result.value) - + @istest def italic_runs_can_be_configured_with_style_mapping(): @@ -179,7 +179,7 @@ def italic_runs_can_be_configured_with_style_mapping(): style_map=[_style_mapping("i => strong")] ) assert_equal("<strong>Hello</strong>", result.value) - + @istest def underline_runs_are_ignored_by_default(): @@ -187,7 +187,7 @@ def underline_runs_are_ignored_by_default(): documents.run(children=[documents.text("Hello")], is_underline=True), ) assert_equal("Hello", result.value) - + @istest def underline_runs_can_be_mapped_using_style_mapping(): @@ -209,7 +209,7 @@ def style_mapping_for_underline_runs_does_not_close_parent_elements(): ] ) assert_equal("<strong><em>Hello</em></strong>", result.value) - + @istest def strikethrough_runs_are_wrapped_in_s_elements_by_default(): @@ -228,7 +228,7 @@ def strikethrough_runs_can_be_configured_with_style_mapping(): ] ) assert_equal("<del>Hello</del>", result.value) - + @istest def small_caps_runs_are_ignored_by_default(): @@ -236,7 +236,7 @@ def small_caps_runs_are_ignored_by_default(): documents.run(children=[documents.text("Hello")], is_small_caps=True), ) assert_equal("Hello", result.value) - + @istest def small_caps_runs_can_be_mapped_using_style_mapping(): @@ -258,7 +258,7 @@ def superscript_runs_are_wrapped_in_sup_tags(): ), ) assert_equal("<sup>Hello</sup>", result.value) - + @istest def subscript_runs_are_wrapped_in_sub_tags(): @@ -269,8 +269,8 @@ def subscript_runs_are_wrapped_in_sub_tags(): ), ) assert_equal("<sub>Hello</sub>", result.value) - - + + @istest def runs_are_converted_by_satisfying_matching_paths(): result = convert_document_element_to_html( @@ -416,6 +416,22 @@ def empty_cells_are_preserved_in_table(): @istest +def empty_rows_are_preserved_in_table(): + table = documents.table([ + documents.table_row([ + documents.table_cell([_paragraph_with_text("Row 1")]), + ]), + documents.table_row([]), + ]) + result = convert_document_element_to_html(table) + expected_html = ( + "<table>" + + "<tr><td><p>Row 1</p></td></tr><tr></tr>" + + "</table>") + assert_equal(expected_html, result.value) + + +@istest def table_cells_are_written_with_colspan_if_not_equal_to_one(): table = documents.table([ documents.table_row([ @@ -489,7 +505,7 @@ def can_define_custom_conversion_for_images(): def convert_image(image): with image.open() as image_file: return [html.element("img", {"alt": image_file.read().decode("ascii")})] - + image = documents.image(alt_text=None, content_type="image/png", open=lambda: io.BytesIO(b"abc")) result = convert_document_element_to_html(image, convert_image=convert_image) assert_equal('<img alt="abc" />', result.value) |
