diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2db78527..6d3004d5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,8 @@ -**0.9.7** +**dev** - Text colors other than black and white are no longer ignored +- Textboxes have been implemented. We no longer lose the content inside of + them. **0.9.6** diff --git a/pydocx/export/base.py b/pydocx/export/base.py index d5008caa..baae919b 100644 --- a/pydocx/export/base.py +++ b/pydocx/export/base.py @@ -15,7 +15,7 @@ NumberingSpan, NumberingSpanBuilder, ) -from pydocx.openxml import wordprocessing, vml +from pydocx.openxml import markup_compatibility, vml, wordprocessing from pydocx.openxml.packaging import WordprocessingDocument @@ -64,9 +64,13 @@ def __init__(self, path): wordprocessing.SimpleField: self.export_simple_field, vml.Shape: self.export_vml_shape, vml.ImageData: self.export_vml_image_data, + vml.Textbox: self.export_textbox, wordprocessing.EmbeddedObject: self.export_embedded_object, NumberingSpan: self.export_numbering_span, NumberingItem: self.export_numbering_item, + markup_compatibility.AlternateContent: self.export_markup_compatibility_alternate_content, # noqa + markup_compatibility.Fallback: self.export_markup_compatibility_fallback, + wordprocessing.TxBxContent: self.export_textbox_content, } self.field_type_to_export_func_map = { 'HYPERLINK': getattr(self, 'export_field_hyperlink', None), @@ -535,3 +539,15 @@ def export_field_char(self, field_char): def export_field_code(self, field_code): pass + + def export_textbox(self, textbox): + return self.yield_nested(textbox.children, self.export_node) + + def export_textbox_content(self, textbox_content): + return self.yield_nested(textbox_content.children, self.export_node) + + def export_markup_compatibility_alternate_content(self, alternate_content): + return self.yield_nested(alternate_content.children, self.export_node) + + def export_markup_compatibility_fallback(self, fallback): + return self.yield_nested(fallback.children, self.export_node) diff --git a/pydocx/models.py b/pydocx/models.py index e8e70545..960f3963 100644 --- a/pydocx/models.py +++ b/pydocx/models.py @@ -5,6 +5,7 @@ unicode_literals, ) +import importlib import inspect from collections import defaultdict @@ -120,9 +121,24 @@ class ParkingLot(XmlModel): def __init__(self, *types, **kwargs): default = kwargs.pop('default', []) super(XmlCollection, self).__init__(self, default=default) - self.types = set(types) + self._types = types self._name_to_type_map = None + @property + def types(self): + return set(self._set_types(*self._types)) + + def _set_types(self, *types): + base_path = 'pydocx.openxml.{0}' + for _type in types: + try: + path, klass = _type.rsplit('.', 1) + except AttributeError: + yield _type + else: + module = importlib.import_module(base_path.format(path)) + yield getattr(module, klass) + @property def name_to_type_map(self): if self._name_to_type_map is None: diff --git a/pydocx/openxml/markup_compatibility/__init__.py b/pydocx/openxml/markup_compatibility/__init__.py new file mode 100644 index 00000000..14f8fbbe --- /dev/null +++ b/pydocx/openxml/markup_compatibility/__init__.py @@ -0,0 +1,7 @@ +from pydocx.openxml.markup_compatibility.alternate_content import AlternateContent +from pydocx.openxml.markup_compatibility.fallback import Fallback + +__all__ = [ + 'AlternateContent', + 'Fallback', +] diff --git a/pydocx/openxml/markup_compatibility/alternate_content.py b/pydocx/openxml/markup_compatibility/alternate_content.py new file mode 100644 index 00000000..78fb8b4e --- /dev/null +++ b/pydocx/openxml/markup_compatibility/alternate_content.py @@ -0,0 +1,14 @@ +# coding: utf-8 +from __future__ import ( + absolute_import, + print_function, + unicode_literals, +) + +from pydocx.models import XmlModel, XmlCollection +from pydocx.openxml.markup_compatibility.fallback import Fallback + + +class AlternateContent(XmlModel): + XML_TAG = 'AlternateContent' + children = XmlCollection(Fallback) diff --git a/pydocx/openxml/markup_compatibility/fallback.py b/pydocx/openxml/markup_compatibility/fallback.py new file mode 100644 index 00000000..397091e4 --- /dev/null +++ b/pydocx/openxml/markup_compatibility/fallback.py @@ -0,0 +1,15 @@ +# coding: utf-8 +from __future__ import ( + absolute_import, + print_function, + unicode_literals, +) + +from pydocx.models import XmlModel, XmlCollection +from pydocx.openxml.wordprocessing.picture import Picture + + +class Fallback(XmlModel): + XML_TAG = 'Fallback' + # TODO #204: actually include all of the children defined in the spec. + children = XmlCollection(Picture) diff --git a/pydocx/openxml/vml/__init__.py b/pydocx/openxml/vml/__init__.py index f44a969f..a2547821 100644 --- a/pydocx/openxml/vml/__init__.py +++ b/pydocx/openxml/vml/__init__.py @@ -1,8 +1,10 @@ # coding: utf-8 from pydocx.openxml.vml.image_data import ImageData from pydocx.openxml.vml.shape import Shape +from pydocx.openxml.vml.textbox import Textbox __all__ = [ 'ImageData', 'Shape', + 'Textbox', ] diff --git a/pydocx/openxml/vml/shape.py b/pydocx/openxml/vml/shape.py index 457b6c67..c73ceeaa 100644 --- a/pydocx/openxml/vml/shape.py +++ b/pydocx/openxml/vml/shape.py @@ -13,8 +13,7 @@ class Shape(XmlModel): XML_TAG = 'shape' style = XmlAttribute() - children = XmlCollection(ImageData) - + children = XmlCollection(ImageData, 'vml.Textbox') # TODO perhaps we could have a prepare_style, or clean_style convention? def get_style(self): diff --git a/pydocx/openxml/vml/textbox.py b/pydocx/openxml/vml/textbox.py new file mode 100644 index 00000000..8cf196dd --- /dev/null +++ b/pydocx/openxml/vml/textbox.py @@ -0,0 +1,16 @@ +# coding: utf-8 +from __future__ import ( + absolute_import, + print_function, + unicode_literals, +) + +from pydocx.models import XmlModel, XmlCollection + + +class Textbox(XmlModel): + XML_TAG = 'textbox' + + children = XmlCollection( + 'wordprocessing.TxBxContent', + ) diff --git a/pydocx/openxml/wordprocessing/__init__.py b/pydocx/openxml/wordprocessing/__init__.py index 916f2cf8..2430064d 100644 --- a/pydocx/openxml/wordprocessing/__init__.py +++ b/pydocx/openxml/wordprocessing/__init__.py @@ -40,6 +40,7 @@ from pydocx.openxml.wordprocessing.table_cell_properties import TableCellProperties # noqa from pydocx.openxml.wordprocessing.table_row import TableRow from pydocx.openxml.wordprocessing.text import Text +from pydocx.openxml.wordprocessing.textbox_content import TxBxContent __all__ = [ 'AbstractNum', @@ -82,4 +83,5 @@ 'TableCell', 'TableRow', 'Text', + 'TxBxContent', ] diff --git a/pydocx/openxml/wordprocessing/deleted_run.py b/pydocx/openxml/wordprocessing/deleted_run.py index dedf6917..3d55ff69 100644 --- a/pydocx/openxml/wordprocessing/deleted_run.py +++ b/pydocx/openxml/wordprocessing/deleted_run.py @@ -16,7 +16,6 @@ class DeletedRun(XmlModel): children = XmlCollection( Run, SmartTagRun, + 'wordprocessing.DeletedRun', # TODO Needs InsertedRun ) - -DeletedRun.children.types.add(DeletedRun) diff --git a/pydocx/openxml/wordprocessing/inserted_run.py b/pydocx/openxml/wordprocessing/inserted_run.py index cc19ec72..7fc0255c 100644 --- a/pydocx/openxml/wordprocessing/inserted_run.py +++ b/pydocx/openxml/wordprocessing/inserted_run.py @@ -16,7 +16,6 @@ class InsertedRun(XmlModel): children = XmlCollection( Run, SmartTagRun, + 'wordprocessing.InsertedRun', # TODO Needs DeletedRun ) - -InsertedRun.children.types.add(InsertedRun) diff --git a/pydocx/openxml/wordprocessing/run.py b/pydocx/openxml/wordprocessing/run.py index fcd9f6f0..2acdb944 100644 --- a/pydocx/openxml/wordprocessing/run.py +++ b/pydocx/openxml/wordprocessing/run.py @@ -20,6 +20,7 @@ from pydocx.openxml.wordprocessing.footnote_reference import FootnoteReference from pydocx.openxml.wordprocessing.footnote_reference_mark import FootnoteReferenceMark from pydocx.openxml.wordprocessing.embedded_object import EmbeddedObject +from pydocx.openxml.markup_compatibility import AlternateContent from pydocx.util.memoize import memoized @@ -41,6 +42,7 @@ class Run(XmlModel): FootnoteReferenceMark, FieldChar, FieldCode, + AlternateContent, ) def get_style_chain_stack(self): diff --git a/pydocx/openxml/wordprocessing/smart_tag_run.py b/pydocx/openxml/wordprocessing/smart_tag_run.py index e56fd0fe..dfa906f9 100644 --- a/pydocx/openxml/wordprocessing/smart_tag_run.py +++ b/pydocx/openxml/wordprocessing/smart_tag_run.py @@ -14,6 +14,5 @@ class SmartTagRun(XmlModel): children = XmlCollection( Run, + 'wordprocessing.SmartTagRun', ) - -SmartTagRun.children.types.add(SmartTagRun) diff --git a/pydocx/openxml/wordprocessing/table.py b/pydocx/openxml/wordprocessing/table.py index 2fc85907..61314b29 100644 --- a/pydocx/openxml/wordprocessing/table.py +++ b/pydocx/openxml/wordprocessing/table.py @@ -8,7 +8,6 @@ from collections import defaultdict from pydocx.models import XmlModel, XmlCollection -from pydocx.openxml.wordprocessing.table_cell import TableCell from pydocx.openxml.wordprocessing.table_row import TableRow @@ -46,7 +45,3 @@ def calculate_table_cell_spans(self): if active_rowspan_for_column: cell_to_rowspan_count[active_rowspan_for_column] += 1 # noqa return dict(cell_to_rowspan_count) - - -# Python makes defining nested class hierarchies at the global level difficult -TableCell.children.types.add(Table) diff --git a/pydocx/openxml/wordprocessing/table_cell.py b/pydocx/openxml/wordprocessing/table_cell.py index 7df2dc31..8a538e86 100644 --- a/pydocx/openxml/wordprocessing/table_cell.py +++ b/pydocx/openxml/wordprocessing/table_cell.py @@ -17,5 +17,5 @@ class TableCell(XmlModel): children = XmlCollection( Paragraph, - # Table is added in wordprocessing.table + 'wordprocessing.Table', ) diff --git a/pydocx/openxml/wordprocessing/textbox_content.py b/pydocx/openxml/wordprocessing/textbox_content.py new file mode 100644 index 00000000..a6176804 --- /dev/null +++ b/pydocx/openxml/wordprocessing/textbox_content.py @@ -0,0 +1,15 @@ +# coding: utf-8 +from __future__ import ( + absolute_import, + print_function, + unicode_literals, +) + +from pydocx.models import XmlModel, XmlCollection + + +class TxBxContent(XmlModel): + XML_TAG = 'txbxContent' + children = XmlCollection( + 'wordprocessing.Paragraph', + ) diff --git a/tests/export/html/test_markup_compatibility.py b/tests/export/html/test_markup_compatibility.py new file mode 100644 index 00000000..b98c2192 --- /dev/null +++ b/tests/export/html/test_markup_compatibility.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +from __future__ import ( + absolute_import, + print_function, + unicode_literals, +) + +from pydocx.test import DocumentGeneratorTestCase +from pydocx.test.utils import WordprocessingDocumentFactory +from pydocx.openxml.packaging import MainDocumentPart + + +class TableTestCase(DocumentGeneratorTestCase): + def test_textbox_with_content(self): + document_xml = ''' +

+ + + + + + + +

+ + AAA + +

+ + + + + + + +

+ ''' + + document = WordprocessingDocumentFactory() + document.add(MainDocumentPart, document_xml) + + expected_html = ''' +

+

+ AAA +

+

+ ''' + self.assert_document_generates_html(document, expected_html) + + def test_textbox_with_content_outside_of_textbox(self): + document_xml = ''' +

+ AAA + + BBB + + + + + + +

+ + CCC + +

+ + + + + + + DDD + + EEE +

+ ''' + + document = WordprocessingDocumentFactory() + document.add(MainDocumentPart, document_xml) + + expected_html = ''' +

+ AAABBB +

+ CCC +

+ DDDEEE +

+ ''' + self.assert_document_generates_html(document, expected_html) diff --git a/tests/export/html/test_textbox.py b/tests/export/html/test_textbox.py new file mode 100644 index 00000000..6d7cc5da --- /dev/null +++ b/tests/export/html/test_textbox.py @@ -0,0 +1,85 @@ +# coding: utf-8 + +from __future__ import ( + absolute_import, + print_function, + unicode_literals, +) + +from pydocx.test import DocumentGeneratorTestCase +from pydocx.test.utils import WordprocessingDocumentFactory +from pydocx.openxml.packaging import MainDocumentPart + + +class TableTestCase(DocumentGeneratorTestCase): + def test_textbox_with_content(self): + document_xml = ''' +

+ + + + + +

+ + AAA + +

+ + + + + +

+ ''' + + document = WordprocessingDocumentFactory() + document.add(MainDocumentPart, document_xml) + + expected_html = ''' +

+

+ AAA +

+

+ ''' + self.assert_document_generates_html(document, expected_html) + + def test_textbox_with_content_outside_of_textbox(self): + document_xml = ''' +

+ AAA + + BBB + + + + +

+ + CCC + +

+ + + + + DDD + + EEE +

+ ''' + + document = WordprocessingDocumentFactory() + document.add(MainDocumentPart, document_xml) + + expected_html = ''' +

+ AAABBB +

+ CCC +

+ DDDEEE +

+ ''' + self.assert_document_generates_html(document, expected_html) diff --git a/tests/export/test_docx.py b/tests/export/test_docx.py index b0812d5f..8ebe3f47 100644 --- a/tests/export/test_docx.py +++ b/tests/export/test_docx.py @@ -24,16 +24,15 @@ def convert(path, *args, **kwargs): class ConvertDocxToHtmlTestCase(DocXFixtureTestCaseFactory): cases = ( - 'read_same_image_multiple_times', 'all_configured_styles', + 'export_from_googledocs', + 'external_image', + 'has_missing_image', + 'has_missing_image', 'has_title', 'inline_tags', - 'has_missing_image', 'justification', 'list_in_table', - 'external_image', - 'export_from_googledocs', - 'has_missing_image', 'lists_with_styles', 'missing_numbering', 'missing_style', @@ -41,6 +40,8 @@ class ConvertDocxToHtmlTestCase(DocXFixtureTestCaseFactory): 'nested_table_rowspan', 'nested_tables', 'no_break_hyphen', + 'read_same_image_multiple_times', + 'rotate_image', 'shift_enter', 'simple', 'simple_lists', @@ -49,10 +50,10 @@ class ConvertDocxToHtmlTestCase(DocXFixtureTestCaseFactory): 'styled_bolding', 'styled_color', 'table_col_row_span', + 'table_with_multi_rowspan', 'tables_in_lists', + 'textbox', 'track_changes_on', - 'table_with_multi_rowspan', - 'rotate_image' ) @raises(MalformedDocxException) diff --git a/tests/fixtures/textbox.docx b/tests/fixtures/textbox.docx new file mode 100644 index 00000000..5ad956b2 Binary files /dev/null and b/tests/fixtures/textbox.docx differ diff --git a/tests/fixtures/textbox.html b/tests/fixtures/textbox.html new file mode 100644 index 00000000..2640dda2 --- /dev/null +++ b/tests/fixtures/textbox.html @@ -0,0 +1,5 @@ +

+

AAA

+

BBB

+

CCCDDD

+

diff --git a/tox.ini b/tox.ini index 8c79be6f..253c446f 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ commands = deps = -rrequirements/testing.txt defusedxml: defusedxml==0.4.1 + py26: importlib [testenv:docs] commands =