aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mammoth/conversion.py104
-rw-r--r--mammoth/documents.py3
-rw-r--r--mammoth/lists.py6
-rw-r--r--setup.py2
-rw-r--r--tests/conversion_tests.py29
5 files changed, 109 insertions, 35 deletions
diff --git a/mammoth/conversion.py b/mammoth/conversion.py
index 489c74a..cbe207a 100644
--- a/mammoth/conversion.py
+++ b/mammoth/conversion.py
@@ -4,8 +4,11 @@ from __future__ import unicode_literals
from functools import partial
-from . import documents, results, html_paths, images, writers, html, lists
+import cobble
+
+from . import documents, results, html_paths, images, writers, html
from .docx.files import InvalidFileReferenceError
+from .lists import find_index
def convert_document_element_to_html(element,
@@ -42,14 +45,23 @@ def convert_document_element_to_html(element,
note_references=[],
comments=comments,
)
- nodes = converter.visit(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)
-class _DocumentConverter(documents.ElementVisitor):
+@cobble.data
+class _ConversionContext(object):
+ is_table_header = cobble.field()
+
+ def copy(self, **kwargs):
+ return cobble.copy(self, **kwargs)
+
+
+class _DocumentConverter(documents.element_visitor(args=1)):
def __init__(self, messages, style_map, convert_image, id_prefix, ignore_empty_paragraphs, note_references, comments):
self._messages = messages
self._style_map = style_map
@@ -60,27 +72,31 @@ class _DocumentConverter(documents.ElementVisitor):
self._convert_image = convert_image
self._comments = comments
- def visit_image(self, image):
+ def visit_image(self, image, context):
try:
return self._convert_image(image)
except InvalidFileReferenceError as error:
self._messages.append(results.warning(str(error)))
return []
- def visit_document(self, document):
- nodes = self._visit_all(document.children)
+ def visit_document(self, document, context):
+ nodes = self._visit_all(document.children, context)
notes = [
document.notes.resolve(reference)
for reference in self._note_references
]
- notes_list = html.element("ol", {}, self._visit_all(notes))
- comments = html.element("dl", {}, lists.flat_map(self.visit_comment, self._referenced_comments))
+ notes_list = html.element("ol", {}, self._visit_all(notes, context))
+ comments = html.element("dl", {}, [
+ html_node
+ for referenced_comment in self._referenced_comments
+ for html_node in self.visit_comment(referenced_comment, context)
+ ])
return nodes + [notes_list, comments]
- def visit_paragraph(self, paragraph):
+ def visit_paragraph(self, paragraph, context):
def children():
- content = self._visit_all(paragraph.children)
+ content = self._visit_all(paragraph.children, context)
if self._ignore_empty_paragraphs:
return content
else:
@@ -90,8 +106,8 @@ class _DocumentConverter(documents.ElementVisitor):
return html_path.wrap(children)
- def visit_run(self, run):
- nodes = lambda: self._visit_all(run.children)
+ def visit_run(self, run, context):
+ nodes = lambda: self._visit_all(run.children, context)
paths = []
if run.is_strikethrough:
paths.append(self._find_style_for_run_property("strikethrough", default="s"))
@@ -123,21 +139,21 @@ class _DocumentConverter(documents.ElementVisitor):
return html_paths.empty
- def visit_text(self, text):
+ def visit_text(self, text, context):
return [html.text(text.value)]
- def visit_hyperlink(self, hyperlink):
+ def visit_hyperlink(self, hyperlink, context):
if hyperlink.anchor is None:
href = hyperlink.href
else:
href = "#{0}".format(self._html_id(hyperlink.anchor))
- nodes = self._visit_all(hyperlink.children)
+ nodes = self._visit_all(hyperlink.children, context)
return [html.collapsible_element("a", {"href": href}, nodes)]
- def visit_bookmark(self, bookmark):
+ def visit_bookmark(self, bookmark, context):
element = html.collapsible_element(
"a",
{"id": self._html_id(bookmark.name)},
@@ -145,31 +161,49 @@ class _DocumentConverter(documents.ElementVisitor):
return [element]
- def visit_tab(self, tab):
+ def visit_tab(self, tab, context):
return [html.text("\t")]
- def visit_table(self, table):
- return [html.element("table", {}, self._visit_all(table.children))]
+ def visit_table(self, table, context):
+ body_index = find_index(lambda child: not child.is_header, table.children)
+ 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:
+ head_rows = self._visit_all(table.children[:body_index], context.copy(is_table_header=True))
+ body_rows = self._visit_all(table.children[body_index:], context.copy(is_table_header=False))
+ children = [
+ html.element("thead", {}, head_rows),
+ html.element("tbody", {}, body_rows),
+ ]
+
+ return [html.element("table", {}, children)]
- def visit_table_row(self, table_row):
- return [html.element("tr", {}, self._visit_all(table_row.children))]
+ def visit_table_row(self, table_row, context):
+ return [html.element("tr", {}, self._visit_all(table_row.children, context))]
- def visit_table_cell(self, table_cell):
+ def visit_table_cell(self, table_cell, context):
+ if context.is_table_header:
+ tag_name = "th"
+ else:
+ tag_name = "td"
attributes = {}
if table_cell.colspan != 1:
attributes["colspan"] = str(table_cell.colspan)
if table_cell.rowspan != 1:
attributes["rowspan"] = str(table_cell.rowspan)
- nodes = [html.force_write] + self._visit_all(table_cell.children)
+ nodes = [html.force_write] + self._visit_all(table_cell.children, context)
return [
- html.element("td", attributes, nodes)
+ html.element(tag_name, attributes, nodes)
]
- def visit_break(self, break_):
+ def visit_break(self, break_, context):
return self._find_html_path_for_break(break_).wrap(lambda: [])
@@ -183,7 +217,7 @@ class _DocumentConverter(documents.ElementVisitor):
return html_paths.empty
- def visit_note_reference(self, note_reference):
+ def visit_note_reference(self, note_reference, context):
self._note_references.append(note_reference)
note_number = len(self._note_references)
return [
@@ -196,8 +230,8 @@ class _DocumentConverter(documents.ElementVisitor):
]
- def visit_note(self, note):
- note_body = self._visit_all(note.body) + [
+ def visit_note(self, note, context):
+ note_body = self._visit_all(note.body, context) + [
html.collapsible_element("p", {}, [
html.text(" "),
html.element("a", {"href": "#" + self._note_ref_html_id(note)}, [
@@ -210,7 +244,7 @@ class _DocumentConverter(documents.ElementVisitor):
]
- def visit_comment_reference(self, reference):
+ def visit_comment_reference(self, reference, context):
def nodes():
comment = self._comments[reference.comment_id]
count = len(self._referenced_comments) + 1
@@ -232,10 +266,10 @@ class _DocumentConverter(documents.ElementVisitor):
return html_path.wrap(nodes)
- def visit_comment(self, referenced_comment):
+ def visit_comment(self, referenced_comment, context):
label, comment = referenced_comment
# TODO remove duplication with notes
- body = self._visit_all(comment.body) + [
+ body = self._visit_all(comment.body, context) + [
html.collapsible_element("p", {}, [
html.text(" "),
html.element("a", {"href": "#" + self._reference_html_id("comment", comment.comment_id)}, [
@@ -253,8 +287,12 @@ class _DocumentConverter(documents.ElementVisitor):
]
- def _visit_all(self, elements):
- return lists.flat_map(self.visit, elements)
+ def _visit_all(self, elements, context):
+ return [
+ html_node
+ for element in elements
+ for html_node in self.visit(element, context)
+ ]
def _find_html_path_for_paragraph(self, paragraph):
diff --git a/mammoth/documents.py b/mammoth/documents.py
index dbd78dc..b440846 100644
--- a/mammoth/documents.py
+++ b/mammoth/documents.py
@@ -216,4 +216,5 @@ class CommentReference(Element):
comment_reference = CommentReference
-ElementVisitor = cobble.visitor(Element)
+def element_visitor(args):
+ return cobble.visitor(Element, args=args)
diff --git a/mammoth/lists.py b/mammoth/lists.py
index db1f9ca..28468fe 100644
--- a/mammoth/lists.py
+++ b/mammoth/lists.py
@@ -23,6 +23,12 @@ def flat_map(func, values):
]
+def find_index(predicate, values):
+ for index, value in enumerate(values):
+ if predicate(value):
+ return index
+
+
if sys.version_info[0] == 2:
map = map
filter = filter
diff --git a/setup.py b/setup.py
index a7a166e..d6b0ee4 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@ def read(fname):
_install_requires = [
- "cobble>=0.1.2,<0.2",
+ "cobble>=0.1.3,<0.2",
]
if sys.version_info[:2] <= (2, 6):
diff --git a/tests/conversion_tests.py b/tests/conversion_tests.py
index 3dfb9f3..3fd9e34 100644
--- a/tests/conversion_tests.py
+++ b/tests/conversion_tests.py
@@ -317,6 +317,35 @@ def docx_table_is_converted_to_table_in_html():
@istest
+def header_rows_are_wrapped_in_thead():
+ table = documents.table([
+ documents.table_row([documents.table_cell([])], is_header=True),
+ documents.table_row([documents.table_cell([])], is_header=True),
+ documents.table_row([documents.table_cell([])], is_header=False),
+ ])
+ result = convert_document_element_to_html(table)
+ expected_html = (
+ "<table>" +
+ "<thead><tr><th></th></tr><tr><th></th></tr></thead>" +
+ "<tbody><tr><td></td></tr></tbody>" +
+ "</table>")
+ assert_equal(expected_html, result.value)
+
+
+@istest
+def tbody_is_omitted_if_all_rows_are_headers():
+ table = documents.table([
+ documents.table_row([documents.table_cell([])], is_header=True),
+ ])
+ result = convert_document_element_to_html(table)
+ expected_html = (
+ "<table>" +
+ "<thead><tr><th></th></tr></thead>" +
+ "</table>")
+ assert_equal(expected_html, result.value)
+
+
+@istest
def empty_cells_are_preserved_in_table():
table = documents.table([
documents.table_row([