From fc02e314c0527a8fcd8cfb814b00eab9b00c80d7 Mon Sep 17 00:00:00 2001 From: Chirica Gheorghe Date: Sat, 4 Mar 2017 15:36:58 +0200 Subject: [PATCH 1/7] Add border for the Paragraph,Run --- pydocx/export/base.py | 5 + pydocx/export/html.py | 132 +++++++++- pydocx/models.py | 24 +- .../wordprocessing/border_properties.py | 239 ++++++++++++++++++ pydocx/openxml/wordprocessing/paragraph.py | 5 + .../wordprocessing/paragraph_properties.py | 2 + .../openxml/wordprocessing/run_properties.py | 2 + pydocx/test/document_builder.py | 26 +- tests/export/test_docx.py | 1 + tests/export/test_xml.py | 79 ++++++ tests/fixtures/paragraphs_with_borders.docx | Bin 0 -> 15617 bytes tests/fixtures/paragraphs_with_borders.html | 63 +++++ tests/templates/p.xml | 5 + tests/templates/r.xml | 1 + tests/templates/rpr.xml | 3 + tests/test_models.py | 26 +- 16 files changed, 604 insertions(+), 9 deletions(-) create mode 100644 pydocx/openxml/wordprocessing/border_properties.py create mode 100644 tests/fixtures/paragraphs_with_borders.docx create mode 100644 tests/fixtures/paragraphs_with_borders.html diff --git a/pydocx/export/base.py b/pydocx/export/base.py index 67b002bc..5cae1125 100644 --- a/pydocx/export/base.py +++ b/pydocx/export/base.py @@ -32,6 +32,8 @@ def __init__(self, path): self.captured_runs = None self.complex_field_runs = [] + self.current_border_item = {} + self.last_paragraph = None self.node_type_to_export_func_map = { wordprocessing.Document: self.export_document, @@ -299,6 +301,9 @@ def yield_body_children(self, body): return self.yield_numbering_spans(body.children) def export_paragraph(self, paragraph): + if self.first_pass: + self.last_paragraph = paragraph + children = self.yield_paragraph_children(paragraph) results = self.yield_nested(children, self.export_node) if paragraph.effective_properties: diff --git a/pydocx/export/html.py b/pydocx/export/html.py index 18e3ea53..8690a55d 100644 --- a/pydocx/export/html.py +++ b/pydocx/export/html.py @@ -274,20 +274,139 @@ def get_heading_tag(self, paragraph): return HtmlTag(tag, id=paragraph.bookmark_name) return HtmlTag(tag) + def export_run(self, run): + results = super(PyDocXHTMLExporter, self).export_run(run) + + for result in self.export_borders(run, results, tag_name='span'): + yield result + def export_paragraph(self, paragraph): results = super(PyDocXHTMLExporter, self).export_paragraph(paragraph) results = is_not_empty_and_not_only_whitespace(results) - if results is None: + + # TODO@botzill In PR#234 we render empty paragraphs properly so + # we don't need this check anymore. Adding for now and to be removed when merging + if results is None and not paragraph.has_border_properties: return tag = self.get_paragraph_tag(paragraph) if tag: results = tag.apply(results) + for tag in self.export_borders(paragraph, results, tag_name='div'): + yield tag + + def export_close_paragraph_border(self): + if self.current_border_item.get('Paragraph'): + yield HtmlTag('div', closed=True) + self.current_border_item['Paragraph'] = None + + def export_borders(self, item, results, tag_name='div'): + if self.first_pass: + for result in results: + yield result + return + + # For now we have here Paragraph and Run + item_name = item.__class__.__name__ + item_is_run = isinstance(item, wordprocessing.Run) + item_is_paragraph = isinstance(item, wordprocessing.Paragraph) + + prev_borders_properties = None + border_properties = None + current_border_item = self.current_border_item.get(item_name) + if current_border_item: + prev_borders_properties = current_border_item.\ + effective_properties.border_properties + + last_item = False + close_border = True + + def current_item_is_last_child(children, child_type): + for p_child in reversed(children): + if isinstance(p_child, child_type): + return p_child == item + return False + + def is_last_item(): + if item_is_paragraph: + if isinstance(item.parent, wordprocessing.TableCell): + return current_item_is_last_child( + item.parent.children, wordprocessing.Paragraph) + elif item == self.last_paragraph: + return True + elif item_is_run: + # Check if current item is the last Run item from paragraph children + return current_item_is_last_child(item.parent.children, wordprocessing.Run) + + return False + + if item.effective_properties: + border_properties = item.effective_properties.border_properties + if border_properties: + last_item = is_last_item() + close_border = False + run_has_different_parent = False + + # If run is from different paragraph then we may need to draw separate border + # even if border properties are the same + if item_is_run and current_border_item: + if current_border_item.parent != item.parent: + run_has_different_parent = True + + if border_properties != prev_borders_properties or run_has_different_parent: + if prev_borders_properties is not None: + # We have a previous border tag opened, so need to close it + yield HtmlTag(tag_name, closed=True) + + # Open a new tag for the new border and include all the properties + border_attrs = self.get_borders_property(border_properties) + yield HtmlTag(tag_name, closed=False, **border_attrs) + + self.current_border_item[item_name] = item + else: + if prev_borders_properties is not None and \ + getattr(border_properties, 'between', None): + # Render border between items + border_attrs = self.get_borders_property( + border_properties, only_between=True) + + yield HtmlTag(tag_name, **border_attrs) + yield HtmlTag(tag_name, closed=True) + + if close_border and prev_borders_properties is not None: + # At this stage we need to make sure that if there is an previously open tag + # about border we need to close it + yield HtmlTag(tag_name, closed=True) + self.current_border_item[item_name] = None + + # All the inner items inside border tag are issued here for result in results: yield result + if border_properties and last_item: + # If the item with border is the last one we need to make sure that we close the + # tag + yield HtmlTag(tag_name, closed=True) + self.current_border_item[item_name] = None + + def get_borders_property(self, border_properties, only_between=False): + attrs = {} + style = {} + + if only_between: + style.update(border_properties.get_between_border_style()) + else: + style.update(border_properties.get_padding_style()) + style.update(border_properties.get_border_style()) + style.update(border_properties.get_shadow_style()) + + if style: + attrs['style'] = convert_dictionary_to_style_fragment(style) + + return attrs + def export_paragraph_property_justification(self, paragraph, results): # TODO these classes could be applied on the paragraph, and not as # inline spans @@ -633,8 +752,17 @@ def export_table(self, table): table_cell_spans = table.calculate_table_cell_spans() self.table_cell_rowspan_tracking[table] = table_cell_spans results = super(PyDocXHTMLExporter, self).export_table(table) + + # Before starting new table new need to make sure that if there is any paragraph + # with border opened before, we need to close it here. + for result in self.export_close_paragraph_border(): + yield result + tag = self.get_table_tag(table) - return tag.apply(results) + results = tag.apply(results) + + for result in results: + yield result def export_table_row(self, table_row): results = super(PyDocXHTMLExporter, self).export_table_row(table_row) diff --git a/pydocx/models.py b/pydocx/models.py index a8b2e2b6..806e370a 100644 --- a/pydocx/models.py +++ b/pydocx/models.py @@ -208,7 +208,7 @@ def __init__( parent=None, **kwargs ): - for field_name, field in self.__class__.__dict__.items(): + for field_name, field in self._get_all_attributes(self.__class__).items(): if isinstance(field, XmlField): # TODO field.default may only refer to the attr, and not if the # field itself is missing @@ -224,6 +224,24 @@ def __init__( self._parent = parent self.container = kwargs.get('container') + @classmethod + def _get_all_attributes(cls, klass): + """Return a set of the accessible attributes of class/type klass. + + This includes all attributes of klass and all of the base classes + recursively. + """ + + attrs = {} + ns = getattr(klass, '__dict__', None) + if ns is not None: + attrs.update(ns) + bases = getattr(klass, '__bases__', []) + for base in bases: + attrs.update(cls._get_all_attributes(base)) + + return attrs + @property def parent(self): return self._parent @@ -263,7 +281,7 @@ def fields(self): model, and yields back only those fields which have been set to a value that isn't the field's default. ''' - for field_name, field in self.__class__.__dict__.items(): + for field_name, field in self._get_all_attributes(self.__class__).items(): if isinstance(field, XmlField): value = getattr(self, field_name, field.default) if value != field.default: @@ -288,7 +306,7 @@ def load(cls, element, **load_kwargs): # Enumerate the defined fields and separate them into attributes and # tags - for field_name, field in cls.__dict__.items(): + for field_name, field in cls._get_all_attributes(cls).items(): if isinstance(field, XmlAttribute): attribute_fields[field_name] = field if isinstance(field, XmlChild): diff --git a/pydocx/openxml/wordprocessing/border_properties.py b/pydocx/openxml/wordprocessing/border_properties.py new file mode 100644 index 00000000..0aac4795 --- /dev/null +++ b/pydocx/openxml/wordprocessing/border_properties.py @@ -0,0 +1,239 @@ +from __future__ import absolute_import, print_function, unicode_literals + +from pydocx.models import XmlModel, XmlChild, XmlAttribute +from pydocx.types import OnOff + +BORDERS_CSS_DEFAULT = 'solid' +BORDERS_CSS_MAPPING = { + 'none': 'none', + 'single': 'solid', + 'double': 'double', + 'triple': 'double', + 'dotted': 'dotted', + 'dashed': 'dashed', + 'dashSmallGap': 'dashed', + 'dotDash': 'dashed', + 'dotDotDash': 'dashed', + 'outset': 'outset' +} + + +class BaseBorder(XmlModel): + style = XmlAttribute(name='val') + width = XmlAttribute(name='sz') + _color = XmlAttribute(name='color') + space = XmlAttribute(name='space') + shadow = XmlAttribute(type=OnOff, name='shadow') + + @property + def color(self): + if self._color in ('auto',): + self._color = '000000' + return self._color + + @property + def css_style(self): + if self.style: + return BORDERS_CSS_MAPPING.get(self.style, BORDERS_CSS_DEFAULT) + return BORDERS_CSS_DEFAULT + + @property + def width_points(self): + """width is of type ST_EighthPointMeasure""" + + if self.width: + width = int(self.width) / 8.0 + if self.style in ('double',): + width *= 3 + elif self.style in ('triple',): + width *= 5 + else: + width = 1 + + return width + + @property + def spacing(self): + if self.space: + return self.space + # If space is not defined we assume 0 by default + return '0' + + def get_css_border_style(self, to_str=True): + border_style = [ + # border-width + '%spt' % self.width_points, + # border-style + self.css_style, + # border-color + '#%s' % self.color + ] + if to_str: + border_style = ' '.join(border_style) + return border_style + + @classmethod + def attributes_list(cls, obj): + if obj: + return (obj.css_style, + obj.width, + obj.color, + obj.spacing, + obj.shadow) + + def __eq__(self, other): + return self.attributes_list(self) == self.attributes_list(other) + + def __ne__(self, other): + return not self == other + + +class TopBorder(BaseBorder): + XML_TAG = 'top' + + +class LeftBorder(BaseBorder): + XML_TAG = 'left' + + +class BottomBorder(BaseBorder): + XML_TAG = 'bottom' + + +class RightBorder(BaseBorder): + XML_TAG = 'right' + + +class BetweenBorder(BaseBorder): + XML_TAG = 'between' + + +class BaseBorderStyle(object): + def get_border_style(self): + raise NotImplemented + + def get_shadow_style(self): + raise NotImplemented + + def get_padding_style(self): + raise NotImplemented + + +class ParagraphBorders(XmlModel, BaseBorderStyle): + XML_TAG = 'pBdr' + top = XmlChild(type=TopBorder) + left = XmlChild(type=LeftBorder) + bottom = XmlChild(type=BottomBorder) + right = XmlChild(type=RightBorder) + between = XmlChild(type=BetweenBorder) + + @property + def borders_name(self): + """Borders are listed by how CSS is expecting them to be""" + + return 'top', 'right', 'bottom', 'left' + + @property + def all_borders(self): + return list(map(lambda brd_name: getattr(self, brd_name), self.borders_name)) + + def get_borders_attribute(self, attr_name, default=None, to_type=None): + attributes = list(map(lambda brd: getattr(brd, attr_name, default), self.all_borders)) + if to_type is not None: + if to_type is set: + attributes = set(attributes) + else: + attributes = list(map(to_type, attributes)) + + return attributes + + def borders_have_same_properties(self): + color = self.get_borders_attribute('color', to_type=set) + style = self.get_borders_attribute('style', to_type=set) + width = self.get_borders_attribute('width', to_type=set) + + if list(set(map(len, (color, style, width)))) == [1]: + return True + + return False + + def get_border_style(self): + border_styles = {} + if self.borders_have_same_properties(): + border_styles['border'] = self.top.get_css_border_style() + else: + for border_name, border in zip(self.borders_name, self.all_borders): + if border: + border_styles['border-%s' % border_name] = border.get_css_border_style() + + return border_styles + + def get_between_border_style(self): + border_styles = {} + if self.between: + border_styles['border-top'] = self.between.get_css_border_style() + # Because there can be padding added by the parent border we need to make sure that + # we adapt margins to not have extra space on left/right + margins = [ + # top + self.between.spacing, + # right + '-%s' % self.right.spacing, + # bottom + self.between.spacing, + # left + '-%s' % self.left.spacing + ] + border_styles['margin'] = ' '.join(map(lambda x: '%spt' % x, margins)) + return border_styles + + def get_padding_style(self): + padding_styles = {} + padding = self.get_borders_attribute('spacing', default=0, to_type=str) + if len(set(padding)) == 1: + padding = list(set(padding)) + padding_styles['padding'] = ' '.join(map(lambda x: '%spt' % x, padding)) + return padding_styles + + def get_shadow_style(self): + shadow_styles = {} + border = self.top + if border and bool(border.shadow): + shadow_styles['box-shadow'] = '{0}pt {0}pt'.format(border.width_points) + return shadow_styles + + @classmethod + def attributes_list(cls, obj): + if obj: + return (obj.top, + obj.left, + obj.bottom, + obj.right, + obj.between) + + def __eq__(self, other): + return self.attributes_list(self) == self.attributes_list(other) + + def __ne__(self, other): + return not self == other + + +class RunBorders(BaseBorder, BaseBorderStyle): + XML_TAG = 'bdr' + + def get_border_style(self): + border_styles = {'border': self.get_css_border_style()} + return border_styles + + def get_shadow_style(self): + shadow_styles = {} + if bool(self.shadow): + shadow_styles['box-shadow'] = '{0}pt {0}pt'.format(self.width_points) + return shadow_styles + + def get_padding_style(self): + padding_styles = {} + # if spacing is 0 no need to set the padding + if int(self.spacing): + padding_styles['padding'] = '%spt' % self.spacing + return padding_styles diff --git a/pydocx/openxml/wordprocessing/paragraph.py b/pydocx/openxml/wordprocessing/paragraph.py index bdb6b387..417f0554 100644 --- a/pydocx/openxml/wordprocessing/paragraph.py +++ b/pydocx/openxml/wordprocessing/paragraph.py @@ -190,6 +190,11 @@ def has_numbering_properties(self): def has_numbering_definition(self): return bool(self.numbering_definition) + @property + @memoized + def has_border_properties(self): + return bool(getattr(self.properties, 'border_properties', None)) + def get_indentation(self, indentation, only_level_ind=False): ''' Get specific indentation of the current paragraph. If indentation is diff --git a/pydocx/openxml/wordprocessing/paragraph_properties.py b/pydocx/openxml/wordprocessing/paragraph_properties.py index c6bbc374..56bf26d6 100644 --- a/pydocx/openxml/wordprocessing/paragraph_properties.py +++ b/pydocx/openxml/wordprocessing/paragraph_properties.py @@ -7,6 +7,7 @@ from pydocx.models import XmlModel, XmlChild from pydocx.openxml.wordprocessing.numbering_properties import NumberingProperties # noqa +from pydocx.openxml.wordprocessing.border_properties import ParagraphBorders class ParagraphProperties(XmlModel): @@ -14,6 +15,7 @@ class ParagraphProperties(XmlModel): parent_style = XmlChild(name='pStyle', attrname='val') numbering_properties = XmlChild(type=NumberingProperties) + border_properties = XmlChild(type=ParagraphBorders) justification = XmlChild(name='jc', attrname='val') # TODO ind can appear multiple times. Need to merge them in document order # This probably means other elements can appear multiple times diff --git a/pydocx/openxml/wordprocessing/run_properties.py b/pydocx/openxml/wordprocessing/run_properties.py index 63587a57..50e3a528 100644 --- a/pydocx/openxml/wordprocessing/run_properties.py +++ b/pydocx/openxml/wordprocessing/run_properties.py @@ -8,6 +8,7 @@ from pydocx.models import XmlModel, XmlChild from pydocx.types import OnOff, Underline from pydocx.openxml.wordprocessing.rfonts import RFonts +from pydocx.openxml.wordprocessing.border_properties import RunBorders class RunProperties(XmlModel): @@ -28,6 +29,7 @@ class RunProperties(XmlModel): sz = XmlChild(name='sz', attrname='val') clr = XmlChild(name='color', attrname='val') r_fonts = XmlChild(type=RFonts) + border_properties = XmlChild(type=RunBorders) @property def color(self): diff --git a/pydocx/test/document_builder.py b/pydocx/test/document_builder.py index 589fec16..d8d37ec4 100644 --- a/pydocx/test/document_builder.py +++ b/pydocx/test/document_builder.py @@ -62,12 +62,29 @@ def xml(self, body): body=body.decode('utf-8'), ) + @classmethod + def xml_borders(cls, border_dict): + borders = [] + if border_dict: + # Create xml for border properties + border_xml = '' + for border_name, border_attributes in border_dict.items(): + border_attrs = ' '.join(('w:%s="%s"' % + (key, val) for key, val in border_attributes.items())) + borders.append( + border_xml.format(border_name=border_name, border_attrs=border_attrs)) + + borders = '\n'.join(borders) + + return borders or None + @classmethod def p_tag( self, text, style='style0', jc=None, + borders=None ): if isinstance(text, string_types): # Use create a single r tag based on the text and the bold @@ -86,6 +103,7 @@ def p_tag( run_tags=run_tags, style=style, jc=jc, + borders=self.xml_borders(borders) ) @classmethod @@ -103,18 +121,19 @@ def r_tag( self, elements, rpr=None, + borders=None ): template = env.get_template(templates['r']) if rpr is None: - rpr = DocxBuilder.rpr_tag() + rpr = DocxBuilder.rpr_tag(borders=self.xml_borders(borders)) return template_render( template, elements=elements, - rpr=rpr, + rpr=rpr ) @classmethod - def rpr_tag(self, inline_styles=None, *args, **kwargs): + def rpr_tag(self, inline_styles=None, borders=None, *args, **kwargs): if inline_styles is None: inline_styles = {} valid_styles = ( @@ -136,6 +155,7 @@ def rpr_tag(self, inline_styles=None, *args, **kwargs): return template_render( template, tags=inline_styles, + borders=borders ) @classmethod diff --git a/tests/export/test_docx.py b/tests/export/test_docx.py index 16fa43d1..28848b7f 100644 --- a/tests/export/test_docx.py +++ b/tests/export/test_docx.py @@ -57,6 +57,7 @@ class ConvertDocxToHtmlTestCase(DocXFixtureTestCaseFactory): 'tables_in_lists', 'textbox', 'track_changes_on', + 'paragraphs_with_borders', ) @raises(MalformedDocxException) diff --git a/tests/export/test_xml.py b/tests/export/test_xml.py index 2ad17a88..e6f724b1 100644 --- a/tests/export/test_xml.py +++ b/tests/export/test_xml.py @@ -1168,3 +1168,82 @@ def get_xml(self): xml = DXB.xml(lis) return xml + + +class ParagraphBordersTestCase(TranslationTestCase): + expected_output = ''' +
+

AAA

+
+
+

BBB

+
+ ''' + + def get_xml(self): + p1_border = { + 'top': { + 'color': 'auto' + } + } + p2_border = { + 'bottom': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + } + } + p_tags = [ + DXB.p_tag('AAA', borders=p1_border), + DXB.p_tag('BBB', borders=p2_border) + ] + body = b'' + for p_tag in p_tags: + body += p_tag + xml = DXB.xml(body) + return xml + + +class RunBordersTestCase(TranslationTestCase): + expected_output = ''' +

+ AAA + BBB +

+ ''' + + def get_xml(self): + r1_border = { + 'bdr': { + 'color': '00FF00', + 'space': '0' + } + } + r2_border = { + 'bdr': { + 'color': '00FF00', + 'space': '1' + } + } + + p_tags = [ + DXB.p_tag( + [ + DXB.r_tag( + [DXB.t_tag('AAA')], + borders=r1_border + ), + DXB.r_tag( + [DXB.t_tag('BBB')], + borders=r2_border + ), + ], + ) + ] + body = b'' + for p_tag in p_tags: + body += p_tag + + xml = DXB.xml(body) + return xml diff --git a/tests/fixtures/paragraphs_with_borders.docx b/tests/fixtures/paragraphs_with_borders.docx new file mode 100644 index 0000000000000000000000000000000000000000..3a2db50b580e73d0051d0773be79af1f24ace1ca GIT binary patch literal 15617 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 +

AAA

+

BBB

+

+ CCC +

+ +
+

DDD

+

EEE

+
+
+

FFF

+
+

GGG

+
+
+

+ HHH III +

+

+ JJJ KKK +

+

+ LLL +

+

MMM

+
+

Simple Paragraph

+
+

NNN

+

OOO

+
+
+

PPP

+
+
+

QQQ

+

RRR

+
+
+

+
+ + + + + + + + + +
+
SSS
+
+
TTT
+
+
+ UUU +
+
+
VVV
+
diff --git a/tests/templates/p.xml b/tests/templates/p.xml index 7a78a060..42144ce8 100644 --- a/tests/templates/p.xml +++ b/tests/templates/p.xml @@ -12,6 +12,11 @@ {% endif %} {% if jc %}{% endif %} + {% if borders %} + + {{ borders }} + + {% endif %} {% for run_tag in run_tags %} {{ run_tag }} diff --git a/tests/templates/r.xml b/tests/templates/r.xml index 2f28a66b..e5cfcb42 100644 --- a/tests/templates/r.xml +++ b/tests/templates/r.xml @@ -3,4 +3,5 @@ {% for element in elements %} {{ element }} {% endfor %} + diff --git a/tests/templates/rpr.xml b/tests/templates/rpr.xml index f49eb08b..cd83d95e 100644 --- a/tests/templates/rpr.xml +++ b/tests/templates/rpr.xml @@ -2,4 +2,7 @@ {% for tag, value in tags.items() %} {% endfor %} + {% if borders %} + {{ borders }} + {% endif %} diff --git a/tests/test_models.py b/tests/test_models.py index 591e055c..359613e4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -15,7 +15,6 @@ XmlModel, XmlRootElementMismatchException, ) - from pydocx.util.xml import parse_xml_from_string @@ -69,6 +68,17 @@ class BucketModel(XmlModel): circle_size = XmlChild(name='circle', attrname='sz') +class BaseItemModel(XmlModel): + val1 = XmlAttribute(name='val1') + val2 = XmlAttribute(name='val2') + + +class ItemModel(BaseItemModel): + XML_TAG = 'item' + + val3 = XmlAttribute(name='val3') + + class BaseTestCase(TestCase): def _get_model_instance_from_xml(self, xml): root = parse_xml_from_string(xml) @@ -298,3 +308,17 @@ def test_apples_and_oranges_models_accessed_through_collection(self): 'four', ] self.assertEqual(types, expected_types) + + +class InheritFromBaseModelTestCase(BaseTestCase): + model = ItemModel + + def test_attributes_available_from_base_class(self): + xml = ''' + + ''' + item = self._get_model_instance_from_xml(xml) + self.assertEqual( + {'val1': 'item_val1', 'val2': 'item_val2', 'val3': 'item_val3'}, + dict(item.fields) + ) From 8934608eabc390bf5771a02ab5f3138e96f01507 Mon Sep 17 00:00:00 2001 From: Chirica Gheorghe Date: Mon, 13 Mar 2017 13:25:27 +0200 Subject: [PATCH 2/7] Add paragraph,run shading color --- pydocx/export/html.py | 102 +++-- pydocx/openxml/wordprocessing/__init__.py | 4 + .../wordprocessing/border_properties.py | 10 +- .../wordprocessing/paragraph_properties.py | 2 + .../openxml/wordprocessing/run_properties.py | 2 + .../wordprocessing/shading_properties.py | 50 +++ pydocx/test/document_builder.py | 31 +- tests/export/test_docx.py | 1 + tests/export/test_xml.py | 373 +++++++++++++++++- tests/fixtures/paragraphs_with_borders.html | 10 +- .../paragraphs_with_borders_and_shading.docx | Bin 0 -> 15447 bytes .../paragraphs_with_borders_and_shading.html | 67 ++++ tests/templates/p.xml | 3 + tests/templates/rpr.xml | 3 + 14 files changed, 622 insertions(+), 36 deletions(-) create mode 100644 pydocx/openxml/wordprocessing/shading_properties.py create mode 100644 tests/fixtures/paragraphs_with_borders_and_shading.docx create mode 100644 tests/fixtures/paragraphs_with_borders_and_shading.html diff --git a/pydocx/export/html.py b/pydocx/export/html.py index 8690a55d..633a8131 100644 --- a/pydocx/export/html.py +++ b/pydocx/export/html.py @@ -282,7 +282,6 @@ def export_run(self, run): def export_paragraph(self, paragraph): results = super(PyDocXHTMLExporter, self).export_paragraph(paragraph) - results = is_not_empty_and_not_only_whitespace(results) # TODO@botzill In PR#234 we render empty paragraphs properly so @@ -314,15 +313,26 @@ def export_borders(self, item, results, tag_name='div'): item_is_paragraph = isinstance(item, wordprocessing.Paragraph) prev_borders_properties = None + prev_shading_properties = None + border_properties = None + shading_properties = None + current_border_item = self.current_border_item.get(item_name) if current_border_item: - prev_borders_properties = current_border_item.\ - effective_properties.border_properties + item_properties = current_border_item.effective_properties + prev_borders_properties = item_properties.border_properties + prev_shading_properties = item_properties.shading_properties last_item = False close_border = True + def prev_properties(): + return prev_borders_properties or prev_shading_properties + + def current_properties(): + return border_properties or shading_properties + def current_item_is_last_child(children, child_type): for p_child in reversed(children): if isinstance(p_child, child_type): @@ -344,7 +354,9 @@ def is_last_item(): if item.effective_properties: border_properties = item.effective_properties.border_properties - if border_properties: + shading_properties = item.effective_properties.shading_properties + + if current_properties(): last_item = is_last_item() close_border = False run_has_different_parent = False @@ -355,27 +367,48 @@ def is_last_item(): if current_border_item.parent != item.parent: run_has_different_parent = True - if border_properties != prev_borders_properties or run_has_different_parent: - if prev_borders_properties is not None: + if border_properties != prev_borders_properties or \ + shading_properties != prev_shading_properties or \ + run_has_different_parent: + if prev_properties() is not None: # We have a previous border tag opened, so need to close it yield HtmlTag(tag_name, closed=True) - # Open a new tag for the new border and include all the properties - border_attrs = self.get_borders_property(border_properties) - yield HtmlTag(tag_name, closed=False, **border_attrs) + # Open a new tag for the new border/shading and include all the properties + attrs = self.get_borders_property( + border_properties, + prev_borders_properties, + shading_properties + ) + yield HtmlTag(tag_name, closed=False, **attrs) self.current_border_item[item_name] = item - else: - if prev_borders_properties is not None and \ - getattr(border_properties, 'between', None): + + if border_properties == prev_borders_properties: + border_between = getattr(border_properties, 'between', None) + add_between_border = bool(border_between) + + if border_between and prev_borders_properties is not None: + if shading_properties: + if shading_properties == prev_shading_properties: + add_between_border = True + else: + + add_between_border = prev_borders_properties.bottom != \ + border_between + + if add_between_border: # Render border between items border_attrs = self.get_borders_property( - border_properties, only_between=True) + border_properties, + prev_borders_properties, + shading_properties, + only_between=True) yield HtmlTag(tag_name, **border_attrs) yield HtmlTag(tag_name, closed=True) - if close_border and prev_borders_properties is not None: + if close_border and prev_properties() is not None: # At this stage we need to make sure that if there is an previously open tag # about border we need to close it yield HtmlTag(tag_name, closed=True) @@ -385,22 +418,47 @@ def is_last_item(): for result in results: yield result - if border_properties and last_item: + if current_properties() and last_item: # If the item with border is the last one we need to make sure that we close the # tag yield HtmlTag(tag_name, closed=True) self.current_border_item[item_name] = None - def get_borders_property(self, border_properties, only_between=False): + def get_borders_property( + self, + border_properties, + prev_border_properties, + shading_properties=None, + only_between=False): attrs = {} style = {} - if only_between: - style.update(border_properties.get_between_border_style()) - else: - style.update(border_properties.get_padding_style()) - style.update(border_properties.get_border_style()) - style.update(border_properties.get_shadow_style()) + if border_properties: + if only_between: + style.update(border_properties.get_between_border_style()) + else: + style.update(border_properties.get_padding_style()) + style.update(border_properties.get_shadow_style()) + + border_style = border_properties.get_border_style() + + if prev_border_properties and \ + isinstance(prev_border_properties, wordprocessing.ParagraphBorders): + + cur_top = border_properties.top + prev_bottom = prev_border_properties.bottom + + all_borders_defined = all([ + border_properties.borders_have_same_properties(), + prev_border_properties.borders_have_same_properties() + ]) + # We need to reset one border if adjacent identical borders are met + if all_borders_defined and cur_top == prev_bottom: + border_style['border-top'] = '0' + style.update(border_style) + + if shading_properties and shading_properties.background_color: + style['background-color'] = '#{0}'.format(shading_properties.background_color) if style: attrs['style'] = convert_dictionary_to_style_fragment(style) diff --git a/pydocx/openxml/wordprocessing/__init__.py b/pydocx/openxml/wordprocessing/__init__.py index 4fce72a2..45f8dca6 100644 --- a/pydocx/openxml/wordprocessing/__init__.py +++ b/pydocx/openxml/wordprocessing/__init__.py @@ -24,6 +24,8 @@ from pydocx.openxml.wordprocessing.numbering_properties import NumberingProperties # noqa from pydocx.openxml.wordprocessing.paragraph import Paragraph from pydocx.openxml.wordprocessing.paragraph_properties import ParagraphProperties # noqa +from pydocx.openxml.wordprocessing.border_properties import ParagraphBorders +from pydocx.openxml.wordprocessing.border_properties import RunBorders from pydocx.openxml.wordprocessing.picture import Picture from pydocx.openxml.wordprocessing.run import Run from pydocx.openxml.wordprocessing.run_properties import RunProperties # noqa @@ -71,9 +73,11 @@ 'NumberingProperties', 'Paragraph', 'ParagraphProperties', + 'ParagraphBorders', 'Picture', 'Run', 'RunProperties', + 'RunBorders', 'RFonts', 'SdtBlock', 'SdtContentBlock', diff --git a/pydocx/openxml/wordprocessing/border_properties.py b/pydocx/openxml/wordprocessing/border_properties.py index 0aac4795..4b5c4b67 100644 --- a/pydocx/openxml/wordprocessing/border_properties.py +++ b/pydocx/openxml/wordprocessing/border_properties.py @@ -47,6 +47,7 @@ def width_points(self): width *= 3 elif self.style in ('triple',): width *= 5 + width = float('%.1f' % width) else: width = 1 @@ -148,6 +149,9 @@ def get_borders_attribute(self, attr_name, default=None, to_type=None): return attributes def borders_have_same_properties(self): + if not any(self.all_borders): + return False + color = self.get_borders_attribute('color', to_type=set) style = self.get_borders_attribute('style', to_type=set) width = self.get_borders_attribute('width', to_type=set) @@ -172,18 +176,20 @@ def get_between_border_style(self): border_styles = {} if self.between: border_styles['border-top'] = self.between.get_css_border_style() + # Because there can be padding added by the parent border we need to make sure that # we adapt margins to not have extra space on left/right margins = [ # top self.between.spacing, # right - '-%s' % self.right.spacing, + '%s' % -int(getattr(self.right, 'spacing', 0)), # bottom self.between.spacing, # left - '-%s' % self.left.spacing + '%s' % -int(getattr(self.left, 'spacing', 0)) ] + border_styles['margin'] = ' '.join(map(lambda x: '%spt' % x, margins)) return border_styles diff --git a/pydocx/openxml/wordprocessing/paragraph_properties.py b/pydocx/openxml/wordprocessing/paragraph_properties.py index 56bf26d6..6903a3db 100644 --- a/pydocx/openxml/wordprocessing/paragraph_properties.py +++ b/pydocx/openxml/wordprocessing/paragraph_properties.py @@ -8,6 +8,7 @@ from pydocx.models import XmlModel, XmlChild from pydocx.openxml.wordprocessing.numbering_properties import NumberingProperties # noqa from pydocx.openxml.wordprocessing.border_properties import ParagraphBorders +from pydocx.openxml.wordprocessing.shading_properties import ParagraphShading class ParagraphProperties(XmlModel): @@ -16,6 +17,7 @@ class ParagraphProperties(XmlModel): parent_style = XmlChild(name='pStyle', attrname='val') numbering_properties = XmlChild(type=NumberingProperties) border_properties = XmlChild(type=ParagraphBorders) + shading_properties = XmlChild(type=ParagraphShading) justification = XmlChild(name='jc', attrname='val') # TODO ind can appear multiple times. Need to merge them in document order # This probably means other elements can appear multiple times diff --git a/pydocx/openxml/wordprocessing/run_properties.py b/pydocx/openxml/wordprocessing/run_properties.py index 50e3a528..81bba0e2 100644 --- a/pydocx/openxml/wordprocessing/run_properties.py +++ b/pydocx/openxml/wordprocessing/run_properties.py @@ -9,6 +9,7 @@ from pydocx.types import OnOff, Underline from pydocx.openxml.wordprocessing.rfonts import RFonts from pydocx.openxml.wordprocessing.border_properties import RunBorders +from pydocx.openxml.wordprocessing.shading_properties import RunShading class RunProperties(XmlModel): @@ -30,6 +31,7 @@ class RunProperties(XmlModel): clr = XmlChild(name='color', attrname='val') r_fonts = XmlChild(type=RFonts) border_properties = XmlChild(type=RunBorders) + shading_properties = XmlChild(type=RunShading) @property def color(self): diff --git a/pydocx/openxml/wordprocessing/shading_properties.py b/pydocx/openxml/wordprocessing/shading_properties.py new file mode 100644 index 00000000..d14cadd0 --- /dev/null +++ b/pydocx/openxml/wordprocessing/shading_properties.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, print_function, unicode_literals + +from pydocx.models import XmlModel, XmlAttribute + + +class BaseShading(XmlModel): + pattern = XmlAttribute(name='val') + color = XmlAttribute(name='color') + fill = XmlAttribute(name='fill') + + @property + def background_color(self): + """Get the background color for shading. Note that this is a simple + implementation as we can't translate all the shadings into CSS""" + + color = None + + if self.pattern in ('solid',) and self.color: + if self.color in ('auto',): + # By default we set the color to black if auto is specified + color = '000000' + else: + color = self.color + elif self.fill and self.fill not in ('auto',): + color = self.fill + elif self.color not in ('auto',): + color = self.color + + return color + + @classmethod + def attributes_list(cls, obj): + if obj: + return (obj.pattern, + obj.color, + obj.fill) + + def __eq__(self, other): + return self.attributes_list(self) == self.attributes_list(other) + + def __ne__(self, other): + return not self == other + + +class ParagraphShading(BaseShading): + XML_TAG = 'shd' + + +class RunShading(BaseShading): + XML_TAG = 'shd' diff --git a/pydocx/test/document_builder.py b/pydocx/test/document_builder.py index d8d37ec4..5e25dd9d 100644 --- a/pydocx/test/document_builder.py +++ b/pydocx/test/document_builder.py @@ -78,13 +78,26 @@ def xml_borders(cls, border_dict): return borders or None + @classmethod + def xml_shading(cls, shading_dict): + shading = '' + if shading_dict is not None: + shading = '' + shading_attrs = ' '.join(('w:%s="%s"' % + (key, val) for key, val in shading_dict.items())) + + shading = shading.format(shading_attrs=shading_attrs) + + return shading + @classmethod def p_tag( self, text, style='style0', jc=None, - borders=None + borders=None, + shading=None ): if isinstance(text, string_types): # Use create a single r tag based on the text and the bold @@ -103,7 +116,8 @@ def p_tag( run_tags=run_tags, style=style, jc=jc, - borders=self.xml_borders(borders) + borders=self.xml_borders(borders), + shading=self.xml_shading(shading) ) @classmethod @@ -121,11 +135,15 @@ def r_tag( self, elements, rpr=None, - borders=None + borders=None, + shading=None ): template = env.get_template(templates['r']) if rpr is None: - rpr = DocxBuilder.rpr_tag(borders=self.xml_borders(borders)) + rpr = DocxBuilder.rpr_tag( + borders=self.xml_borders(borders), + shading=self.xml_shading(shading), + ) return template_render( template, elements=elements, @@ -133,7 +151,7 @@ def r_tag( ) @classmethod - def rpr_tag(self, inline_styles=None, borders=None, *args, **kwargs): + def rpr_tag(self, inline_styles=None, borders=None, shading=None, *args, **kwargs): if inline_styles is None: inline_styles = {} valid_styles = ( @@ -155,7 +173,8 @@ def rpr_tag(self, inline_styles=None, borders=None, *args, **kwargs): return template_render( template, tags=inline_styles, - borders=borders + borders=borders, + shading=shading ) @classmethod diff --git a/tests/export/test_docx.py b/tests/export/test_docx.py index 28848b7f..6735a672 100644 --- a/tests/export/test_docx.py +++ b/tests/export/test_docx.py @@ -58,6 +58,7 @@ class ConvertDocxToHtmlTestCase(DocXFixtureTestCaseFactory): 'textbox', 'track_changes_on', 'paragraphs_with_borders', + 'paragraphs_with_borders_and_shading', ) @raises(MalformedDocxException) diff --git a/tests/export/test_xml.py b/tests/export/test_xml.py index e6f724b1..ed89ed7d 100644 --- a/tests/export/test_xml.py +++ b/tests/export/test_xml.py @@ -1175,7 +1175,7 @@ class ParagraphBordersTestCase(TranslationTestCase):

AAA

-
+

BBB

''' @@ -1205,6 +1205,241 @@ def get_xml(self): return xml +class ParagraphBordersSameTopBottomTestCase(TranslationTestCase): + expected_output = ''' +
+

AAA

+
+
+

BBB

+
+ ''' + + def get_xml(self): + p1_border = { + 'top': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + }, + 'bottom': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + }, + 'left': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + }, + 'right': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + }, + } + p2_border = { + 'top': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + } + } + + p_tags = [ + DXB.p_tag('AAA', borders=p1_border), + DXB.p_tag('BBB', borders=p2_border), + ] + body = b'' + for p_tag in p_tags: + body += p_tag + xml = DXB.xml(body) + return xml + + +class ParagraphBordersSameTopBottomWithShadingTestCase(TranslationTestCase): + expected_output = ''' +
+

AAA

+
+
+

BBB

+
+''' + + def get_xml(self): + p1_border = { + 'top': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + }, + 'bottom': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + }, + 'left': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + }, + 'right': { + 'val': 'single', + 'sz': '5', + 'space': '2', + 'color': '0000FF' + }, + } + p1_shading = { + 'fill': '0000FF', + } + p2_border = p1_border.copy() + p2_shading = { + 'fill': 'FF00FF', + } + + p_tags = [ + DXB.p_tag('AAA', borders=p1_border, shading=p1_shading), + DXB.p_tag('BBB', borders=p2_border, shading=p2_shading), + ] + body = b'' + for p_tag in p_tags: + body += p_tag + xml = DXB.xml(body) + return xml + + +class ParagraphShadingTestCase(TranslationTestCase): + expected_output = ''' +
+

AAA

+

BBB

+
+
+

CCC

+
+
+

DDD

+
+ ''' + + def get_xml(self): + # + p1_shading = { + 'fill': '0000FF', + } + p2_shading = { + 'fill': '0000FF', + } + p3_shading = { + 'val': 'solid', + 'color': 'auto' + } + p4_shading = { + 'val': 'solid', + 'color': 'FF00FF' + } + + p_tags = [ + DXB.p_tag('AAA', shading=p1_shading), + DXB.p_tag('BBB', shading=p2_shading), + DXB.p_tag('CCC', shading=p3_shading), + DXB.p_tag('DDD', shading=p4_shading), + ] + body = b'' + for p_tag in p_tags: + body += p_tag + xml = DXB.xml(body) + return xml + + +class ParagraphBorderAndShadingTestCase(TranslationTestCase): + expected_output = ''' +
+

AAA

+

BBB

+
+
+

CCC

+
+
+

DDD

+
+ ''' + + def get_xml(self): + p1_border = { + 'top': { + 'val': 'single', + 'sz': '5', + 'space': '0', + 'color': '0000FF' + } + } + p1_shading = { + 'fill': '0000FF', + } + p2_border = { + 'top': { + 'val': 'single', + 'sz': '5', + 'space': '0', + 'color': '0000FF' + } + } + p2_shading = { + 'fill': '0000FF', + } + p3_border = { + 'bottom': { + 'val': 'single', + 'sz': '5', + 'space': '0', + 'color': '0000FF' + } + } + p3_shading = { + 'val': 'solid', + 'color': 'FF00FF', + 'fill': '000000', + } + + p4_border = { + 'bottom': { + 'val': 'single', + 'sz': '5', + 'space': '0', + 'color': '0000FF' + } + } + p4_shading = { + 'val': 'solid', + 'color': 'FFFFFF', + 'fill': '000000', + } + + p_tags = [ + DXB.p_tag('AAA', borders=p1_border, shading=p1_shading), + DXB.p_tag('BBB', borders=p2_border, shading=p2_shading), + DXB.p_tag('CCC', borders=p3_border, shading=p3_shading), + DXB.p_tag('DDD', borders=p4_border, shading=p4_shading), + ] + body = b'' + for p_tag in p_tags: + body += p_tag + xml = DXB.xml(body) + return xml + + class RunBordersTestCase(TranslationTestCase): expected_output = '''

@@ -1247,3 +1482,139 @@ def get_xml(self): xml = DXB.xml(body) return xml + + +class RunShadingTestCase(TranslationTestCase): + expected_output = ''' +

+ AAABBB + CCC + DDD +

+ ''' + + def get_xml(self): + r1_shading = { + 'fill': '0000FF', + } + r2_shading = { + 'fill': '0000FF', + } + r3_shading = { + 'val': 'solid', + 'color': 'auto' + } + r4_shading = { + 'val': 'solid', + 'color': 'FF00FF' + } + + p_tags = [ + DXB.p_tag( + [ + DXB.r_tag( + [DXB.t_tag('AAA')], + shading=r1_shading + ), + DXB.r_tag( + [DXB.t_tag('BBB')], + shading=r2_shading + ), + DXB.r_tag( + [DXB.t_tag('CCC')], + shading=r3_shading + ), + DXB.r_tag( + [DXB.t_tag('DDD')], + shading=r4_shading + ), + ], + ) + ] + body = b'' + for p_tag in p_tags: + body += p_tag + xml = DXB.xml(body) + return xml + + +class RunBordersAndShadingTestCase(TranslationTestCase): + expected_output = ''' +

+ AAABBB + CCC + DDD +

+''' + + def get_xml(self): + r1_border = { + 'bdr': { + 'color': '00FF00', + 'space': '0' + } + } + r1_shading = { + 'fill': '0000FF', + } + r2_border = { + 'bdr': { + 'color': '00FF00', + 'space': '0' + } + } + r2_shading = { + 'fill': '0000FF', + } + r3_border = { + 'bdr': { + 'color': 'FFFF00', + 'space': '1' + } + } + r3_shading = { + 'val': 'solid', + 'color': 'auto' + } + r4_border = { + 'bdr': { + 'color': 'FFFF00', + 'space': '1' + } + } + r4_shading = { + 'val': 'solid', + 'color': 'FF00FF' + } + + p_tags = [ + DXB.p_tag( + [ + DXB.r_tag( + [DXB.t_tag('AAA')], + borders=r1_border, + shading=r1_shading + ), + DXB.r_tag( + [DXB.t_tag('BBB')], + borders=r2_border, + shading=r2_shading + ), + DXB.r_tag( + [DXB.t_tag('CCC')], + borders=r3_border, + shading=r3_shading + ), + DXB.r_tag( + [DXB.t_tag('DDD')], + borders=r4_border, + shading=r4_shading + ), + ], + ) + ] + body = b'' + for p_tag in p_tags: + body += p_tag + xml = DXB.xml(body) + return xml diff --git a/tests/fixtures/paragraphs_with_borders.html b/tests/fixtures/paragraphs_with_borders.html index 35661eb4..49e83158 100644 --- a/tests/fixtures/paragraphs_with_borders.html +++ b/tests/fixtures/paragraphs_with_borders.html @@ -5,7 +5,7 @@ CCC

-
+

DDD

EEE

@@ -16,10 +16,10 @@

- HHH III + HHH III

- JJJ KKK + JJJ KKK

LLL @@ -31,7 +31,7 @@

NNN

OOO

-
+

PPP

@@ -52,7 +52,7 @@ -
+
UUU
diff --git a/tests/fixtures/paragraphs_with_borders_and_shading.docx b/tests/fixtures/paragraphs_with_borders_and_shading.docx new file mode 100644 index 0000000000000000000000000000000000000000..186e405d6b94aaeab591dfaeb7b08d3f614abe70 GIT binary patch literal 15447 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/paragraphs_with_borders_and_shading.html b/tests/fixtures/paragraphs_with_borders_and_shading.html new file mode 100644 index 00000000..89cad802 --- /dev/null +++ b/tests/fixtures/paragraphs_with_borders_and_shading.html @@ -0,0 +1,67 @@ +
+

AAA

+

BBB

+
+
+

CCC

+

DDD

+
+

EEE FFF GGG +

+
+

HHH

+

III

+
+
+

JJJ

+
+
+

KKK

+

LLL

+
+
+

MMM

+
+

NNN

+
+
+

OOO

+
+

PPP

+
+
+

QQQ

+

RRR

+
+
+

SSS

+
+ + + + + + + + + +
+
+ TTT +
+ UUU +
+
+
+ VVV +
+ WWW +
+
+ X + X + X + +
YYY ZZZ
+
\ No newline at end of file diff --git a/tests/templates/p.xml b/tests/templates/p.xml index 42144ce8..4c098e18 100644 --- a/tests/templates/p.xml +++ b/tests/templates/p.xml @@ -17,6 +17,9 @@ {{ borders }} {% endif %} + {% if shading %} + {{ shading }} + {% endif %} {% for run_tag in run_tags %} {{ run_tag }} diff --git a/tests/templates/rpr.xml b/tests/templates/rpr.xml index cd83d95e..cd439b7b 100644 --- a/tests/templates/rpr.xml +++ b/tests/templates/rpr.xml @@ -5,4 +5,7 @@ {% if borders %} {{ borders }} {% endif %} + {% if shading %} + {{ shading }} + {% endif %} From c9ca5f53c4d369d09e0856a024a65b2693304cac Mon Sep 17 00:00:00 2001 From: Chirica Gheorghe Date: Wed, 15 Mar 2017 16:14:56 +0200 Subject: [PATCH 3/7] Fix border for list items --- pydocx/export/base.py | 9 +- pydocx/export/border_and_shading.py | 279 +++++++++++++++++ pydocx/export/html.py | 285 +----------------- pydocx/export/html_tag.py | 111 +++++++ .../wordprocessing/numbering_properties.py | 11 + pydocx/openxml/wordprocessing/paragraph.py | 13 +- tests/fixtures/paragraphs_with_borders.docx | Bin 15617 -> 16132 bytes tests/fixtures/paragraphs_with_borders.html | 22 ++ .../paragraphs_with_borders_and_shading.docx | Bin 15447 -> 16121 bytes .../paragraphs_with_borders_and_shading.html | 93 +++--- 10 files changed, 496 insertions(+), 327 deletions(-) create mode 100644 pydocx/export/border_and_shading.py create mode 100644 pydocx/export/html_tag.py diff --git a/pydocx/export/base.py b/pydocx/export/base.py index 5cae1125..460259f4 100644 --- a/pydocx/export/base.py +++ b/pydocx/export/base.py @@ -15,12 +15,14 @@ NumberingSpan, NumberingSpanBuilder, ) +from pydocx.export.border_and_shading import BorderAndShadingBuilder from pydocx.openxml import markup_compatibility, vml, wordprocessing from pydocx.openxml.packaging import WordprocessingDocument class PyDocXExporter(object): numbering_span_builder_class = NumberingSpanBuilder + border_and_shading_builder_class = BorderAndShadingBuilder def __init__(self, path): self.path = path @@ -32,8 +34,9 @@ def __init__(self, path): self.captured_runs = None self.complex_field_runs = [] - self.current_border_item = {} - self.last_paragraph = None + self.paragraphs = [] + self.border_and_shading_builder = self.border_and_shading_builder_class( + self.paragraphs) self.node_type_to_export_func_map = { wordprocessing.Document: self.export_document, @@ -302,7 +305,7 @@ def yield_body_children(self, body): def export_paragraph(self, paragraph): if self.first_pass: - self.last_paragraph = paragraph + self.paragraphs.append(paragraph) children = self.yield_paragraph_children(paragraph) results = self.yield_nested(children, self.export_node) diff --git a/pydocx/export/border_and_shading.py b/pydocx/export/border_and_shading.py new file mode 100644 index 00000000..dc81ae73 --- /dev/null +++ b/pydocx/export/border_and_shading.py @@ -0,0 +1,279 @@ +# coding: utf-8 +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +from pydocx.export.html_tag import HtmlTag +from pydocx.export.numbering_span import NumberingItem +from pydocx.openxml.wordprocessing import Run, Paragraph, TableCell +from pydocx.util.xml import convert_dictionary_to_style_fragment + + +class NoBorderAndShadingBuilder(object): + TAGS = { + 'Paragraph': 'div', + 'Run': 'span' + } + + def __init__(self, items): + self.items = items + self.current_border_item = {} + self.item = None + self.prev_item = None + + def export_borders(self, item, results, first_pass=True): + if results: + for result in results: + yield result + + +class BorderAndShadingBuilder(NoBorderAndShadingBuilder): + @property + def item_is_paragraph(self): + return isinstance(self.item, Paragraph) + + @property + def item_is_run(self): + return isinstance(self.item, Run) + + @property + def item_name(self): + return self.item.__class__.__name__ + + @property + def tag_name(self): + return self.TAGS[self.item_name] + + def current_item_is_last_child(self, children, child_type): + for p_child in reversed(children): + if isinstance(p_child, child_type): + return p_child == self.item + return False + + def is_last_item(self): + if self.item_is_paragraph: + if isinstance(self.item.parent, (TableCell, NumberingItem)): + return self.current_item_is_last_child( + self.item.parent.children, Paragraph) + elif self.item == self.items[-1]: + return True + elif self.item_is_run: + # Check if current item is the last Run item from paragraph children + return self.current_item_is_last_child(self.item.parent.children, Run) + + return False + + def close_tag(self): + self.current_border_item[self.item_name] = None + return HtmlTag(self.tag_name, closed=True) + + def get_next_item(self): + next_item = None + + try: + cur_item_idx = self.items.index(self.item) + if cur_item_idx < len(self.items) - 1: + next_item = self.items[cur_item_idx + 1] + except ValueError: + pass + + return next_item + + def is_next_paragraph_listing(self): + """Check if current item is not listing but next one is listing""" + + if not self.item_is_paragraph: + return False + + next_item = self.get_next_item() + if next_item: + if not self.item.has_numbering_properties and next_item.has_numbering_properties: + return True + + return False + + def export_borders(self, item, results, first_pass=True): + if first_pass: + for result in results: + yield result + return + + self.item = item + + prev_borders_properties = None + prev_shading_properties = None + + border_properties = None + shading_properties = None + + current_border_item = self.current_border_item.get(self.item_name) + if current_border_item: + item_properties = current_border_item.effective_properties + prev_borders_properties = item_properties.border_properties + prev_shading_properties = item_properties.shading_properties + + last_item = False + close_border = True + + def prev_properties(): + return prev_borders_properties or prev_shading_properties + + def current_properties(): + return border_properties or shading_properties + + def properties_are_different(): + if border_properties != prev_borders_properties: + return True + elif shading_properties != prev_shading_properties: + return True + + return False + + def pre_close(): + """Check if we should close the tag before yielding other tags""" + self.item = item + + if not close_border: + return + elif prev_properties() is None: + return + + # At this stage we need to make sure that if there is an previously open tag + # about border/shading we need to close it + yield self.close_tag() + + def post_close(): + """Check if we should close the tag once all the inner tags were yielded""" + self.item = item + + if current_properties() and self.is_next_paragraph_listing(): + pass + elif not last_item: + return + elif current_properties() is None: + return + + # If the item with border/shading is the last one + # we need to make sure that we close the tag + yield self.close_tag() + + if item.effective_properties: + border_properties = item.effective_properties.border_properties + shading_properties = item.effective_properties.shading_properties + + if current_properties(): + last_item = self.is_last_item() + close_border = False + run_has_different_parent = False + + # If run is from different paragraph then we may need to draw separate border + # even if border properties are the same + if self.item_is_run and current_border_item: + if current_border_item.parent != item.parent: + run_has_different_parent = True + + if properties_are_different() or run_has_different_parent: + if prev_properties() is not None: + # We have a previous border/shading tag opened, so need to close it + yield HtmlTag(self.tag_name, closed=True) + + # Open a new tag for the new border/shading and include all the properties + attrs = self.get_borders_property() + yield HtmlTag(self.tag_name, closed=False, **attrs) + self.current_border_item[self.item_name] = item + + if border_properties == prev_borders_properties: + border_between = getattr(border_properties, 'between', None) + add_between_border = bool(border_between) + + if border_between and prev_borders_properties is not None: + if shading_properties: + if shading_properties == prev_shading_properties: + add_between_border = True + else: + + add_between_border = prev_borders_properties.bottom != \ + border_between + + if add_between_border: + # Render border between items + border_attrs = self.get_borders_property(only_between=True) + yield HtmlTag(self.tag_name, **border_attrs) + yield HtmlTag(self.tag_name, closed=True) + + for close_tag in pre_close(): + yield close_tag + + # All the inner items inside border/shading tag are issued here + if results: + for result in results: + yield result + + for close_tag in post_close(): + yield close_tag + + self.prev_item = item + + def reset_top_border_if_the_same(self): + if not self.prev_item or not self.prev_item.effective_properties: + return False + elif not self.prev_item.effective_properties.border_properties: + return False + elif not isinstance(self.prev_item, Paragraph): + return False + elif not isinstance(self.item, Paragraph): + return False + elif not self.item.have_same_numbering_properties_as(self.prev_item): + return False + + curr_border_properties = self.item.effective_properties.border_properties + prev_border_properties = self.prev_item.effective_properties.border_properties + + cur_top = curr_border_properties.top + prev_bottom = prev_border_properties.bottom + + all_borders_defined = all([ + curr_border_properties.borders_have_same_properties(), + prev_border_properties.borders_have_same_properties() + ]) + + if all_borders_defined and cur_top == prev_bottom: + return True + + return False + + def get_borders_property(self, only_between=False): + attrs = {} + style = {} + + border_properties = self.item.effective_properties.border_properties + shading_properties = self.item.effective_properties.shading_properties + + if border_properties: + if only_between: + style.update(border_properties.get_between_border_style()) + else: + style.update(border_properties.get_padding_style()) + style.update(border_properties.get_shadow_style()) + border_style = border_properties.get_border_style() + + # We need to reset one border if adjacent identical borders are met + if self.reset_top_border_if_the_same(): + border_style['border-top'] = '0' + style.update(border_style) + + if shading_properties and shading_properties.background_color: + style['background-color'] = '#{0}'.format(shading_properties.background_color) + + if style: + attrs['style'] = convert_dictionary_to_style_fragment(style) + + return attrs + + def export_close_paragraph_border(self): + if self.current_border_item.get('Paragraph'): + yield HtmlTag('div', closed=True) + self.current_border_item['Paragraph'] = None diff --git a/pydocx/export/html.py b/pydocx/export/html.py index 633a8131..dab1362f 100644 --- a/pydocx/export/html.py +++ b/pydocx/export/html.py @@ -23,9 +23,11 @@ from pydocx.export.numbering_span import NumberingItem from pydocx.openxml import wordprocessing from pydocx.util.uri import uri_is_external -from pydocx.util.xml import ( - convert_dictionary_to_html_attributes, - convert_dictionary_to_style_fragment, +from pydocx.util.xml import convert_dictionary_to_style_fragment +from pydocx.export.html_tag import ( + HtmlTag, + is_only_whitespace, + is_not_empty_and_not_only_whitespace ) @@ -54,106 +56,6 @@ def get_first_from_sequence(sequence, default=None): return first_result -def is_only_whitespace(obj): - ''' - If the obj has `strip` return True if calling strip on the obj results in - an empty instance. Otherwise, return False. - ''' - if hasattr(obj, 'strip'): - return not obj.strip() - return False - - -def is_not_empty_and_not_only_whitespace(gen): - ''' - Determine if a generator is empty, or consists only of whitespace. - - If the generator is non-empty, return the original generator. Otherwise, - return None - ''' - queue = [] - if gen is None: - return - try: - for item in gen: - queue.append(item) - is_whitespace = True - if isinstance(item, HtmlTag): - # If we encounter a tag that allows whitespace, then we can stop - is_whitespace = not item.allow_whitespace - else: - is_whitespace = is_only_whitespace(item) - - if not is_whitespace: - # This item isn't whitespace, so we're done scanning - return chain(queue, gen) - - except StopIteration: - pass - - -class HtmlTag(object): - closed_tag_format = '' - - def __init__( - self, - tag, - allow_self_closing=False, - closed=False, - allow_whitespace=False, - **attrs - ): - self.tag = tag - self.allow_self_closing = allow_self_closing - self.attrs = attrs - self.closed = closed - self.allow_whitespace = allow_whitespace - - def apply(self, results, allow_empty=True): - if not allow_empty: - results = is_not_empty_and_not_only_whitespace(results) - if results is None: - return - - sequence = [[self]] - if results is not None: - sequence.append(results) - - if not self.allow_self_closing: - sequence.append([self.close()]) - - results = chain(*sequence) - - for result in results: - yield result - - def close(self): - return HtmlTag( - tag=self.tag, - closed=True, - ) - - def to_html(self): - if self.closed is True: - return self.closed_tag_format.format(tag=self.tag) - else: - attrs = self.get_html_attrs() - end_bracket = '>' - if self.allow_self_closing: - end_bracket = ' />' - if attrs: - return '<{tag} {attrs}{end}'.format( - tag=self.tag, - attrs=attrs, - end=end_bracket, - ) - else: - return '<{tag}{end}'.format(tag=self.tag, end=end_bracket) - - def get_html_attrs(self): - return convert_dictionary_to_html_attributes(self.attrs) - - class PyDocXHTMLExporter(PyDocXExporter): def __init__(self, *args, **kwargs): super(PyDocXHTMLExporter, self).__init__(*args, **kwargs) @@ -277,7 +179,8 @@ def get_heading_tag(self, paragraph): def export_run(self, run): results = super(PyDocXHTMLExporter, self).export_run(run) - for result in self.export_borders(run, results, tag_name='span'): + for result in self.border_and_shading_builder.export_borders( + run, results, first_pass=self.first_pass): yield result def export_paragraph(self, paragraph): @@ -293,178 +196,10 @@ def export_paragraph(self, paragraph): if tag: results = tag.apply(results) - for tag in self.export_borders(paragraph, results, tag_name='div'): - yield tag - - def export_close_paragraph_border(self): - if self.current_border_item.get('Paragraph'): - yield HtmlTag('div', closed=True) - self.current_border_item['Paragraph'] = None - - def export_borders(self, item, results, tag_name='div'): - if self.first_pass: - for result in results: - yield result - return - - # For now we have here Paragraph and Run - item_name = item.__class__.__name__ - item_is_run = isinstance(item, wordprocessing.Run) - item_is_paragraph = isinstance(item, wordprocessing.Paragraph) - - prev_borders_properties = None - prev_shading_properties = None - - border_properties = None - shading_properties = None - - current_border_item = self.current_border_item.get(item_name) - if current_border_item: - item_properties = current_border_item.effective_properties - prev_borders_properties = item_properties.border_properties - prev_shading_properties = item_properties.shading_properties - - last_item = False - close_border = True - - def prev_properties(): - return prev_borders_properties or prev_shading_properties - - def current_properties(): - return border_properties or shading_properties - - def current_item_is_last_child(children, child_type): - for p_child in reversed(children): - if isinstance(p_child, child_type): - return p_child == item - return False - - def is_last_item(): - if item_is_paragraph: - if isinstance(item.parent, wordprocessing.TableCell): - return current_item_is_last_child( - item.parent.children, wordprocessing.Paragraph) - elif item == self.last_paragraph: - return True - elif item_is_run: - # Check if current item is the last Run item from paragraph children - return current_item_is_last_child(item.parent.children, wordprocessing.Run) - - return False - - if item.effective_properties: - border_properties = item.effective_properties.border_properties - shading_properties = item.effective_properties.shading_properties - - if current_properties(): - last_item = is_last_item() - close_border = False - run_has_different_parent = False - - # If run is from different paragraph then we may need to draw separate border - # even if border properties are the same - if item_is_run and current_border_item: - if current_border_item.parent != item.parent: - run_has_different_parent = True - - if border_properties != prev_borders_properties or \ - shading_properties != prev_shading_properties or \ - run_has_different_parent: - if prev_properties() is not None: - # We have a previous border tag opened, so need to close it - yield HtmlTag(tag_name, closed=True) - - # Open a new tag for the new border/shading and include all the properties - attrs = self.get_borders_property( - border_properties, - prev_borders_properties, - shading_properties - ) - yield HtmlTag(tag_name, closed=False, **attrs) - - self.current_border_item[item_name] = item - - if border_properties == prev_borders_properties: - border_between = getattr(border_properties, 'between', None) - add_between_border = bool(border_between) - - if border_between and prev_borders_properties is not None: - if shading_properties: - if shading_properties == prev_shading_properties: - add_between_border = True - else: - - add_between_border = prev_borders_properties.bottom != \ - border_between - - if add_between_border: - # Render border between items - border_attrs = self.get_borders_property( - border_properties, - prev_borders_properties, - shading_properties, - only_between=True) - - yield HtmlTag(tag_name, **border_attrs) - yield HtmlTag(tag_name, closed=True) - - if close_border and prev_properties() is not None: - # At this stage we need to make sure that if there is an previously open tag - # about border we need to close it - yield HtmlTag(tag_name, closed=True) - self.current_border_item[item_name] = None - - # All the inner items inside border tag are issued here - for result in results: + for result in self.border_and_shading_builder.export_borders( + paragraph, results, first_pass=self.first_pass): yield result - if current_properties() and last_item: - # If the item with border is the last one we need to make sure that we close the - # tag - yield HtmlTag(tag_name, closed=True) - self.current_border_item[item_name] = None - - def get_borders_property( - self, - border_properties, - prev_border_properties, - shading_properties=None, - only_between=False): - attrs = {} - style = {} - - if border_properties: - if only_between: - style.update(border_properties.get_between_border_style()) - else: - style.update(border_properties.get_padding_style()) - style.update(border_properties.get_shadow_style()) - - border_style = border_properties.get_border_style() - - if prev_border_properties and \ - isinstance(prev_border_properties, wordprocessing.ParagraphBorders): - - cur_top = border_properties.top - prev_bottom = prev_border_properties.bottom - - all_borders_defined = all([ - border_properties.borders_have_same_properties(), - prev_border_properties.borders_have_same_properties() - ]) - # We need to reset one border if adjacent identical borders are met - if all_borders_defined and cur_top == prev_bottom: - border_style['border-top'] = '0' - style.update(border_style) - - if shading_properties and shading_properties.background_color: - style['background-color'] = '#{0}'.format(shading_properties.background_color) - - if style: - attrs['style'] = convert_dictionary_to_style_fragment(style) - - return attrs - def export_paragraph_property_justification(self, paragraph, results): # TODO these classes could be applied on the paragraph, and not as # inline spans @@ -813,7 +548,7 @@ def export_table(self, table): # Before starting new table new need to make sure that if there is any paragraph # with border opened before, we need to close it here. - for result in self.export_close_paragraph_border(): + for result in self.border_and_shading_builder.export_close_paragraph_border(): yield result tag = self.get_table_tag(table) diff --git a/pydocx/export/html_tag.py b/pydocx/export/html_tag.py new file mode 100644 index 00000000..b1bb1c06 --- /dev/null +++ b/pydocx/export/html_tag.py @@ -0,0 +1,111 @@ +# coding: utf-8 +from __future__ import ( + absolute_import, + division, + print_function, + unicode_literals, +) + +from itertools import chain + +from pydocx.util.xml import convert_dictionary_to_html_attributes + + +def is_only_whitespace(obj): + ''' + If the obj has `strip` return True if calling strip on the obj results in + an empty instance. Otherwise, return False. + ''' + if hasattr(obj, 'strip'): + return not obj.strip() + return False + + +def is_not_empty_and_not_only_whitespace(gen): + ''' + Determine if a generator is empty, or consists only of whitespace. + + If the generator is non-empty, return the original generator. Otherwise, + return None + ''' + queue = [] + if gen is None: + return + try: + for item in gen: + queue.append(item) + is_whitespace = True + if isinstance(item, HtmlTag): + # If we encounter a tag that allows whitespace, then we can stop + is_whitespace = not item.allow_whitespace + else: + is_whitespace = is_only_whitespace(item) + + if not is_whitespace: + # This item isn't whitespace, so we're done scanning + return chain(queue, gen) + + except StopIteration: + pass + + +class HtmlTag(object): + closed_tag_format = '' + + def __init__( + self, + tag, + allow_self_closing=False, + closed=False, + allow_whitespace=False, + **attrs + ): + self.tag = tag + self.allow_self_closing = allow_self_closing + self.attrs = attrs + self.closed = closed + self.allow_whitespace = allow_whitespace + + def apply(self, results, allow_empty=True): + if not allow_empty: + results = is_not_empty_and_not_only_whitespace(results) + if results is None: + return + + sequence = [[self]] + if results is not None: + sequence.append(results) + + if not self.allow_self_closing: + sequence.append([self.close()]) + + results = chain(*sequence) + + for result in results: + yield result + + def close(self): + return HtmlTag( + tag=self.tag, + closed=True, + ) + + def to_html(self): + if self.closed is True: + return self.closed_tag_format.format(tag=self.tag) + else: + attrs = self.get_html_attrs() + end_bracket = '>' + if self.allow_self_closing: + end_bracket = ' />' + if attrs: + return '<{tag} {attrs}{end}'.format( + tag=self.tag, + attrs=attrs, + end=end_bracket, + ) + else: + return '<{tag}{end}'.format(tag=self.tag, end=end_bracket) + + def get_html_attrs(self): + return convert_dictionary_to_html_attributes(self.attrs) diff --git a/pydocx/openxml/wordprocessing/numbering_properties.py b/pydocx/openxml/wordprocessing/numbering_properties.py index ff1ebb7f..ab66e5bc 100644 --- a/pydocx/openxml/wordprocessing/numbering_properties.py +++ b/pydocx/openxml/wordprocessing/numbering_properties.py @@ -21,3 +21,14 @@ def is_root_level(self): return False return self.level_id == self.ROOT_LEVEL_ID + + @classmethod + def attributes_list(cls, obj): + if obj: + return obj.level_id, obj.num_id + + def __eq__(self, other): + return self.attributes_list(self) == self.attributes_list(other) + + def __ne__(self, other): + return not self == other diff --git a/pydocx/openxml/wordprocessing/paragraph.py b/pydocx/openxml/wordprocessing/paragraph.py index 417f0554..d2a5a05c 100644 --- a/pydocx/openxml/wordprocessing/paragraph.py +++ b/pydocx/openxml/wordprocessing/paragraph.py @@ -183,7 +183,7 @@ def get_number_of_initial_tabs(self): @property @memoized def has_numbering_properties(self): - return bool(getattr(self.properties, 'numbering_properties', None)) + return bool(getattr(self.effective_properties, 'numbering_properties', None)) @property @memoized @@ -193,7 +193,7 @@ def has_numbering_definition(self): @property @memoized def has_border_properties(self): - return bool(getattr(self.properties, 'border_properties', None)) + return bool(getattr(self.effective_properties, 'border_properties', None)) def get_indentation(self, indentation, only_level_ind=False): ''' @@ -211,3 +211,12 @@ def get_indentation(self, indentation, only_level_ind=False): ind = level.paragraph_properties.to_int(indentation, default=0) return ind + + def have_same_numbering_properties_as(self, paragraph): + prop1 = getattr(self.effective_properties, 'numbering_properties', None) + prop2 = getattr(paragraph.effective_properties, 'numbering_properties', None) + + if prop1 == prop2: + return True + + return False diff --git a/tests/fixtures/paragraphs_with_borders.docx b/tests/fixtures/paragraphs_with_borders.docx index 3a2db50b580e73d0051d0773be79af1f24ace1ca..686f5d35bb8a4701fce7df60725e87eb40093464 100644 GIT binary patch delta 7245 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 delta 6744 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 CG}CDS diff --git a/tests/fixtures/paragraphs_with_borders.html b/tests/fixtures/paragraphs_with_borders.html index 49e83158..ff9539de 100644 --- a/tests/fixtures/paragraphs_with_borders.html +++ b/tests/fixtures/paragraphs_with_borders.html @@ -61,3 +61,25 @@ +

White border

+
    +
  1. +
    XXX
    +
      +
    1. +
      XXX1
      +
    2. +
    3. +
      XXX2
      +
    4. +
    +
  2. +
  3. +
    + YYY +
    +
  4. +
  5. + ZZZ +
  6. +
diff --git a/tests/fixtures/paragraphs_with_borders_and_shading.docx b/tests/fixtures/paragraphs_with_borders_and_shading.docx index 186e405d6b94aaeab591dfaeb7b08d3f614abe70..8d7e382ed6bbba04a91b09f6bbf8484b4b765ee2 100644 GIT binary patch delta 6925 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 AHUIzs delta 6278 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 U2lTfS6@pDkF<{Dw{^#^R0R6e3@c;k- diff --git a/tests/fixtures/paragraphs_with_borders_and_shading.html b/tests/fixtures/paragraphs_with_borders_and_shading.html index 89cad802..47cba891 100644 --- a/tests/fixtures/paragraphs_with_borders_and_shading.html +++ b/tests/fixtures/paragraphs_with_borders_and_shading.html @@ -1,67 +1,66 @@ -
-

AAA

-

BBB

-
-
-

CCC

-

DDD

-
+

AAA

+

BBB

+

CCC

+

DDD

EEE FFF GGG -

-
-

HHH

-

III

-
+ style="background-color:#7030A0">GGG

+

+ HHH

+

III

-

JJJ

-
-
-

KKK

-

LLL

-
-
-

MMM

+

JJJ

+

KKK

+

LLL

+

+ MMM

-

NNN

-
+

NNN

OOO

-

PPP

-
+

PPP

QQQ

-

RRR

-
+

RRR

-

SSS

-
+

SSS

+ - -
-
- TTT -
- UUU -
+
TTT
UUU
-
- VVV -
- WWW -
+
VVV
WWW
XXX - X - X - X - -
YYY ZZZ
+
YYY ZZZ +
\ No newline at end of file + +
+

Alpha

+

Beta

+
+
    +
  1. +
    + Gamma +
    +
      +
    1. +
      + Delta +
      +
    2. +
    +
  2. +
  3. +
    + Epsilon +
    +
  4. +
\ No newline at end of file From 8c856ee75c462fa7d5974e7d70ef996cdbfc9b05 Mon Sep 17 00:00:00 2001 From: Chirica Gheorghe Date: Tue, 18 Apr 2017 18:34:05 +0300 Subject: [PATCH 4/7] Fix empty border/shading definition --- pydocx/openxml/wordprocessing/border_properties.py | 5 ++++- pydocx/openxml/wordprocessing/shading_properties.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pydocx/openxml/wordprocessing/border_properties.py b/pydocx/openxml/wordprocessing/border_properties.py index 4b5c4b67..67849e36 100644 --- a/pydocx/openxml/wordprocessing/border_properties.py +++ b/pydocx/openxml/wordprocessing/border_properties.py @@ -210,7 +210,7 @@ def get_shadow_style(self): @classmethod def attributes_list(cls, obj): - if obj: + if obj is not None: return (obj.top, obj.left, obj.bottom, @@ -223,6 +223,9 @@ def __eq__(self, other): def __ne__(self, other): return not self == other + def __nonzero__(self): + return any(self.attributes_list(self) or [None]) + class RunBorders(BaseBorder, BaseBorderStyle): XML_TAG = 'bdr' diff --git a/pydocx/openxml/wordprocessing/shading_properties.py b/pydocx/openxml/wordprocessing/shading_properties.py index d14cadd0..0dd328d9 100644 --- a/pydocx/openxml/wordprocessing/shading_properties.py +++ b/pydocx/openxml/wordprocessing/shading_properties.py @@ -30,7 +30,7 @@ def background_color(self): @classmethod def attributes_list(cls, obj): - if obj: + if obj is not None: return (obj.pattern, obj.color, obj.fill) @@ -41,6 +41,9 @@ def __eq__(self, other): def __ne__(self, other): return not self == other + def __nonzero__(self): + return any(self.attributes_list(self) or [None]) + class ParagraphShading(BaseShading): XML_TAG = 'shd' From 087fa7ee318017fa499b807c19f0d0eba35305e7 Mon Sep 17 00:00:00 2001 From: Chirica Gheorghe Date: Sat, 27 May 2017 16:02:41 +0300 Subject: [PATCH 5/7] Fixed run border for hyperlinks --- pydocx/export/border_and_shading.py | 7 ++++++- pydocx/export/html.py | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pydocx/export/border_and_shading.py b/pydocx/export/border_and_shading.py index dc81ae73..11ab2d9e 100644 --- a/pydocx/export/border_and_shading.py +++ b/pydocx/export/border_and_shading.py @@ -275,5 +275,10 @@ def get_borders_property(self, only_between=False): def export_close_paragraph_border(self): if self.current_border_item.get('Paragraph'): - yield HtmlTag('div', closed=True) + yield HtmlTag(self.TAGS['Paragraph'], closed=True) self.current_border_item['Paragraph'] = None + + def export_close_run_border(self): + if self.current_border_item.get('Run'): + yield HtmlTag(self.TAGS['Run'], closed=True) + self.current_border_item['Run'] = None diff --git a/pydocx/export/html.py b/pydocx/export/html.py index dab1362f..90815c14 100644 --- a/pydocx/export/html.py +++ b/pydocx/export/html.py @@ -518,6 +518,12 @@ def export_hyperlink(self, hyperlink): # the export underline function. There's got to be a better way. old = self.export_run_property_underline self.export_run_property_underline = lambda run, results: results + + # Before starting new hyperlink we need to make sure that if there is any run + # with border opened before, we need to close it here. + for result in self.border_and_shading_builder.export_close_run_border(): + yield result + for result in results: yield result self.export_run_property_underline = old From 87a7507a0c75e595238c8020a29eceb9509ac645 Mon Sep 17 00:00:00 2001 From: Chirica Gheorghe Date: Sat, 29 Jul 2017 15:43:45 +0300 Subject: [PATCH 6/7] Fixed no border style --- pydocx/openxml/wordprocessing/border_properties.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pydocx/openxml/wordprocessing/border_properties.py b/pydocx/openxml/wordprocessing/border_properties.py index 67849e36..27c2b7a9 100644 --- a/pydocx/openxml/wordprocessing/border_properties.py +++ b/pydocx/openxml/wordprocessing/border_properties.py @@ -88,6 +88,11 @@ def __eq__(self, other): def __ne__(self, other): return not self == other + def __bool__(self): + if self.style and self.style in ['nil', 'none']: + return False + return True + class TopBorder(BaseBorder): XML_TAG = 'top' @@ -223,7 +228,7 @@ def __eq__(self, other): def __ne__(self, other): return not self == other - def __nonzero__(self): + def __bool__(self): return any(self.attributes_list(self) or [None]) From 7f1cf6b6cf07b38209cf09eb12b3d04c1a927800 Mon Sep 17 00:00:00 2001 From: Chirica Gheorghe Date: Sat, 19 May 2018 10:44:29 +0300 Subject: [PATCH 7/7] Fixed unittests warnings --- pydocx/export/border_and_shading.py | 2 +- pydocx/export/numbering_span.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pydocx/export/border_and_shading.py b/pydocx/export/border_and_shading.py index 11ab2d9e..c2f15b75 100644 --- a/pydocx/export/border_and_shading.py +++ b/pydocx/export/border_and_shading.py @@ -196,7 +196,7 @@ def post_close(): else: add_between_border = prev_borders_properties.bottom != \ - border_between + border_between if add_between_border: # Render border between items diff --git a/pydocx/export/numbering_span.py b/pydocx/export/numbering_span.py index 809ff672..a48a65dd 100644 --- a/pydocx/export/numbering_span.py +++ b/pydocx/export/numbering_span.py @@ -295,10 +295,10 @@ def detect_parent_child_map_for_items(self): list_start_stop_index = {} # we are interested only in components that are part of the listing components = [component for component in self.components if - hasattr(component, 'properties') - and hasattr(component.properties, 'numbering_properties') - and component.numbering_definition - and component.get_numbering_level()] + hasattr(component, 'properties') and + hasattr(component.properties, 'numbering_properties') and + component.numbering_definition and + component.get_numbering_level()] if not components: return False