Skip to content

Commit 09e1d3e

Browse files
committed
Add border for the Paragraph,Run
1 parent 98c6aa6 commit 09e1d3e

File tree

16 files changed

+604
-8
lines changed

16 files changed

+604
-8
lines changed

pydocx/export/base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def __init__(self, path):
3232

3333
self.captured_runs = None
3434
self.complex_field_runs = []
35+
self.current_border_item = {}
36+
self.last_paragraph = None
3537

3638
self.node_type_to_export_func_map = {
3739
wordprocessing.Document: self.export_document,
@@ -299,6 +301,9 @@ def yield_body_children(self, body):
299301
return self.yield_numbering_spans(body.children)
300302

301303
def export_paragraph(self, paragraph):
304+
if self.first_pass:
305+
self.last_paragraph = paragraph
306+
302307
children = self.yield_paragraph_children(paragraph)
303308
results = self.yield_nested(children, self.export_node)
304309
if paragraph.effective_properties:

pydocx/export/html.py

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,20 +274,139 @@ def get_heading_tag(self, paragraph):
274274
return HtmlTag(tag, id=paragraph.bookmark_name)
275275
return HtmlTag(tag)
276276

277+
def export_run(self, run):
278+
results = super(PyDocXHTMLExporter, self).export_run(run)
279+
280+
for result in self.export_borders(run, results, tag_name='span'):
281+
yield result
282+
277283
def export_paragraph(self, paragraph):
278284
results = super(PyDocXHTMLExporter, self).export_paragraph(paragraph)
279285

280286
results = is_not_empty_and_not_only_whitespace(results)
281-
if results is None:
287+
288+
# TODO@botzill In PR#234 we render empty paragraphs properly so
289+
# we don't need this check anymore. Adding for now and to be removed when merging
290+
if results is None and not paragraph.has_border_properties:
282291
return
283292

284293
tag = self.get_paragraph_tag(paragraph)
285294
if tag:
286295
results = tag.apply(results)
287296

297+
for tag in self.export_borders(paragraph, results, tag_name='div'):
298+
yield tag
299+
300+
def export_close_paragraph_border(self):
301+
if self.current_border_item.get('Paragraph'):
302+
yield HtmlTag('div', closed=True)
303+
self.current_border_item['Paragraph'] = None
304+
305+
def export_borders(self, item, results, tag_name='div'):
306+
if self.first_pass:
307+
for result in results:
308+
yield result
309+
return
310+
311+
# For now we have here Paragraph and Run
312+
item_name = item.__class__.__name__
313+
item_is_run = isinstance(item, wordprocessing.Run)
314+
item_is_paragraph = isinstance(item, wordprocessing.Paragraph)
315+
316+
prev_borders_properties = None
317+
border_properties = None
318+
current_border_item = self.current_border_item.get(item_name)
319+
if current_border_item:
320+
prev_borders_properties = current_border_item.\
321+
effective_properties.border_properties
322+
323+
last_item = False
324+
close_border = True
325+
326+
def current_item_is_last_child(children, child_type):
327+
for p_child in reversed(children):
328+
if isinstance(p_child, child_type):
329+
return p_child == item
330+
return False
331+
332+
def is_last_item():
333+
if item_is_paragraph:
334+
if isinstance(item.parent, wordprocessing.TableCell):
335+
return current_item_is_last_child(
336+
item.parent.children, wordprocessing.Paragraph)
337+
elif item == self.last_paragraph:
338+
return True
339+
elif item_is_run:
340+
# Check if current item is the last Run item from paragraph children
341+
return current_item_is_last_child(item.parent.children, wordprocessing.Run)
342+
343+
return False
344+
345+
if item.effective_properties:
346+
border_properties = item.effective_properties.border_properties
347+
if border_properties:
348+
last_item = is_last_item()
349+
close_border = False
350+
run_has_different_parent = False
351+
352+
# If run is from different paragraph then we may need to draw separate border
353+
# even if border properties are the same
354+
if item_is_run and current_border_item:
355+
if current_border_item.parent != item.parent:
356+
run_has_different_parent = True
357+
358+
if border_properties != prev_borders_properties or run_has_different_parent:
359+
if prev_borders_properties is not None:
360+
# We have a previous border tag opened, so need to close it
361+
yield HtmlTag(tag_name, closed=True)
362+
363+
# Open a new tag for the new border and include all the properties
364+
border_attrs = self.get_borders_property(border_properties)
365+
yield HtmlTag(tag_name, closed=False, **border_attrs)
366+
367+
self.current_border_item[item_name] = item
368+
else:
369+
if prev_borders_properties is not None and \
370+
getattr(border_properties, 'between', None):
371+
# Render border between items
372+
border_attrs = self.get_borders_property(
373+
border_properties, only_between=True)
374+
375+
yield HtmlTag(tag_name, **border_attrs)
376+
yield HtmlTag(tag_name, closed=True)
377+
378+
if close_border and prev_borders_properties is not None:
379+
# At this stage we need to make sure that if there is an previously open tag
380+
# about border we need to close it
381+
yield HtmlTag(tag_name, closed=True)
382+
self.current_border_item[item_name] = None
383+
384+
# All the inner items inside border tag are issued here
288385
for result in results:
289386
yield result
290387

388+
if border_properties and last_item:
389+
# If the item with border is the last one we need to make sure that we close the
390+
# tag
391+
yield HtmlTag(tag_name, closed=True)
392+
self.current_border_item[item_name] = None
393+
394+
def get_borders_property(self, border_properties, only_between=False):
395+
attrs = {}
396+
style = {}
397+
398+
if only_between:
399+
style.update(border_properties.get_between_border_style())
400+
else:
401+
style.update(border_properties.get_padding_style())
402+
style.update(border_properties.get_border_style())
403+
style.update(border_properties.get_shadow_style())
404+
405+
if style:
406+
attrs['style'] = convert_dictionary_to_style_fragment(style)
407+
408+
return attrs
409+
291410
def export_paragraph_property_justification(self, paragraph, results):
292411
# TODO these classes could be applied on the paragraph, and not as
293412
# inline spans
@@ -633,8 +752,17 @@ def export_table(self, table):
633752
table_cell_spans = table.calculate_table_cell_spans()
634753
self.table_cell_rowspan_tracking[table] = table_cell_spans
635754
results = super(PyDocXHTMLExporter, self).export_table(table)
755+
756+
# Before starting new table new need to make sure that if there is any paragraph
757+
# with border opened before, we need to close it here.
758+
for result in self.export_close_paragraph_border():
759+
yield result
760+
636761
tag = self.get_table_tag(table)
637-
return tag.apply(results)
762+
results = tag.apply(results)
763+
764+
for result in results:
765+
yield result
638766

639767
def export_table_row(self, table_row):
640768
results = super(PyDocXHTMLExporter, self).export_table_row(table_row)

pydocx/models.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def __init__(
208208
parent=None,
209209
**kwargs
210210
):
211-
for field_name, field in self.__class__.__dict__.items():
211+
for field_name, field in self._get_all_attributes(self.__class__).items():
212212
if isinstance(field, XmlField):
213213
# TODO field.default may only refer to the attr, and not if the
214214
# field itself is missing
@@ -224,6 +224,24 @@ def __init__(
224224
self._parent = parent
225225
self.container = kwargs.get('container')
226226

227+
@classmethod
228+
def _get_all_attributes(cls, klass):
229+
"""Return a set of the accessible attributes of class/type klass.
230+
231+
This includes all attributes of klass and all of the base classes
232+
recursively.
233+
"""
234+
235+
attrs = {}
236+
ns = getattr(klass, '__dict__', None)
237+
if ns is not None:
238+
attrs.update(ns)
239+
bases = getattr(klass, '__bases__', [])
240+
for base in bases:
241+
attrs.update(cls._get_all_attributes(base))
242+
243+
return attrs
244+
227245
@property
228246
def parent(self):
229247
return self._parent
@@ -263,7 +281,7 @@ def fields(self):
263281
model, and yields back only those fields which have been set to a value
264282
that isn't the field's default.
265283
'''
266-
for field_name, field in self.__class__.__dict__.items():
284+
for field_name, field in self._get_all_attributes(self.__class__).items():
267285
if isinstance(field, XmlField):
268286
value = getattr(self, field_name, field.default)
269287
if value != field.default:
@@ -288,7 +306,7 @@ def load(cls, element, **load_kwargs):
288306

289307
# Enumerate the defined fields and separate them into attributes and
290308
# tags
291-
for field_name, field in cls.__dict__.items():
309+
for field_name, field in cls._get_all_attributes(cls).items():
292310
if isinstance(field, XmlAttribute):
293311
attribute_fields[field_name] = field
294312
if isinstance(field, XmlChild):

0 commit comments

Comments
 (0)