summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2019-01-27 09:25:29 +0000
committerMichael Williamson <mike@zwobble.org>2019-01-27 09:25:29 +0000
commit2337bf2362d49efb53ba80c4d61e186191ae3d20 (patch)
tree03548225de5b283d0725293b8d540c9cf84e4739
parent3f5c9d2893c87fa58be817daf4f5f9d093e2b880 (diff)
Preserve empty table rows
-rw-r--r--NEWS8
-rw-r--r--mammoth/conversion.py80
-rw-r--r--tests/conversion_tests.py46
3 files changed, 77 insertions, 57 deletions
diff --git a/NEWS b/NEWS
index e4ccc9a..31aeb23 100644
--- a/NEWS
+++ b/NEWS
@@ -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)