Skip to content

Commit e76f918

Browse files
committed
Add effective properties for paragraph
1 parent 525d1a4 commit e76f918

File tree

11 files changed

+142
-122
lines changed

11 files changed

+142
-122
lines changed

pydocx/export/html.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ def style(self):
188188
'p': {
189189
'margin-top': '0',
190190
'margin-bottom': '0'
191+
},
192+
'ol': {
193+
'margin-top': '0',
194+
'margin-bottom': '0'
195+
},
196+
'ul': {
197+
'margin-top': '0',
198+
'margin-bottom': '0'
191199
}
192200
}
193201

@@ -373,7 +381,7 @@ def get_paragraph_property_spacing(self, paragraph):
373381
next_before = next_paragraph_spacing['before'] or 0
374382

375383
same_style = current_paragraph_spacing['parent_style'] == \
376-
next_paragraph_spacing['parent_style']
384+
next_paragraph_spacing['parent_style']
377385

378386
if same_style:
379387
if not current_paragraph_spacing['contextual_spacing']:
@@ -393,7 +401,7 @@ def get_paragraph_property_spacing(self, paragraph):
393401
prev_after = previous_paragraph_spacing['after'] or 0
394402

395403
same_style = current_paragraph_spacing['parent_style'] == \
396-
previous_paragraph_spacing['parent_style']
404+
previous_paragraph_spacing['parent_style']
397405

398406
if same_style:
399407
if not current_paragraph_spacing['contextual_spacing']:
@@ -490,7 +498,7 @@ def get_paragraph_property_indentation(self, paragraph):
490498
include_text_indent=True
491499
)
492500
if 'text-indent' in listing_style and \
493-
listing_style['text-indent'] != '0.00em':
501+
listing_style['text-indent'] != '0.00em':
494502
style['text-indent'] = listing_style['text-indent']
495503

496504
if indentation_right:

pydocx/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ def load(cls, element, **load_kwargs):
308308
if field.name is not None:
309309
attr_name = field.name
310310
value = element.attrib.get(attr_name, field.default)
311+
if callable(field.type):
312+
value = field.type(value)
311313
kwargs[field_name] = value
312314

313315
# Child tag fields may specify a handler/type, which is responsible for

pydocx/openxml/packaging/style_definitions_part.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -51,28 +51,39 @@ def get_style_chain_stack(self, style_type, style_id):
5151
styleB
5252
styleC
5353
'''
54-
visited_styles = set()
55-
visited_styles.add(style_id)
5654

55+
visited_styles = set()
5756
styles = self.styles.get_styles_by_type(style_type)
58-
base_style = styles.get(style_id)
59-
60-
if base_style:
61-
yield base_style
62-
63-
# Build up the stack of styles to merge together
64-
current_style = base_style
65-
while current_style:
66-
if not current_style.parent_style:
67-
# The current style doesn't have a parent style
68-
break
69-
if current_style.parent_style in visited_styles:
70-
# Loop detected
71-
break
72-
style = styles.get(current_style.parent_style)
73-
if not style:
74-
# Style doesn't exist
75-
break
76-
visited_styles.add(style.style_id)
77-
yield style
78-
current_style = style
57+
styles_to_apply = {}
58+
59+
def yield_styles_parent_stack(base_style):
60+
if base_style:
61+
yield base_style
62+
63+
# Build up the stack of styles to merge together
64+
current_style = base_style
65+
while current_style:
66+
if not current_style.parent_style:
67+
# The current style doesn't have a parent style
68+
break
69+
if current_style.parent_style in visited_styles:
70+
# Loop detected
71+
break
72+
style = styles.get(current_style.parent_style)
73+
if not style:
74+
# Style doesn't exist
75+
break
76+
visited_styles.add(style.style_id)
77+
yield style
78+
current_style = style
79+
80+
if not style_id:
81+
# In this case we need to check the default defined styles
82+
styles_to_apply = self.styles.get_default_styles_by_type(style_type)
83+
else:
84+
styles_to_apply[style_id] = styles.get(style_id)
85+
86+
for style_id, style in styles_to_apply.items():
87+
visited_styles.add(style_id)
88+
for s in yield_styles_parent_stack(style):
89+
yield s

pydocx/openxml/wordprocessing/paragraph.py

Lines changed: 71 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from pydocx.models import XmlModel, XmlCollection, XmlChild
99
from pydocx.openxml.wordprocessing.bookmark import Bookmark
10-
from pydocx.openxml.wordprocessing.br import Break
1110
from pydocx.openxml.wordprocessing.deleted_run import DeletedRun
1211
from pydocx.openxml.wordprocessing.hyperlink import Hyperlink
1312
from pydocx.openxml.wordprocessing.inserted_run import InsertedRun
@@ -18,7 +17,6 @@
1817
from pydocx.openxml.wordprocessing.smart_tag_run import SmartTagRun
1918
from pydocx.openxml.wordprocessing.tab_char import TabChar
2019
from pydocx.openxml.wordprocessing.text import Text
21-
from pydocx.openxml.wordprocessing.table_cell import TableCell
2220
from pydocx.util.memoize import memoized
2321

2422

@@ -38,10 +36,6 @@ class Paragraph(XmlModel):
3836
Bookmark
3937
)
4038

41-
def __init__(self, **kwargs):
42-
super(Paragraph, self).__init__(**kwargs)
43-
self._effective_properties = None
44-
4539
@property
4640
def is_empty(self):
4741
if not self.children:
@@ -52,20 +46,73 @@ def is_empty(self):
5246
if len(self.children) == 1:
5347
first_child = self.children[0]
5448
if isinstance(first_child, Bookmark) and \
55-
first_child.name in ('_GoBack',):
49+
first_child.name in ('_GoBack',):
5650
return True
5751
# We can have cases when only run properties are defined and no text
5852
elif not first_child.children:
5953
return True
6054
return False
6155

56+
def _get_properties_inherited_from_parent_table(self):
57+
from pydocx.openxml.wordprocessing.table import Table
58+
59+
inherited_properties = {}
60+
61+
parent_table = self.get_first_ancestor(Table)
62+
if parent_table:
63+
style_stack = parent_table.get_style_chain_stack()
64+
for style in reversed(list(style_stack)):
65+
if style.paragraph_properties:
66+
inherited_properties.update(
67+
dict(style.paragraph_properties.fields),
68+
)
69+
return inherited_properties
70+
71+
def _get_inherited_properties_from_parent_style(self):
72+
inherited_properties = {}
73+
style_stack = self.get_style_chain_stack()
74+
for style in reversed(list(style_stack)):
75+
if style.paragraph_properties:
76+
inherited_properties.update(
77+
dict(style.paragraph_properties.fields),
78+
)
79+
return inherited_properties
80+
81+
@property
82+
def inherited_properties(self):
83+
properties = {}
84+
85+
if self.default_doc_styles and \
86+
getattr(self.default_doc_styles.paragraph, 'properties'):
87+
properties.update(
88+
dict(self.default_doc_styles.paragraph.properties.fields),
89+
)
90+
properties.update(
91+
self._get_inherited_properties_from_parent_style(),
92+
)
93+
# Tables can also define custom paragraph pr
94+
properties.update(
95+
self._get_properties_inherited_from_parent_table(),
96+
)
97+
98+
# TODO When enable this make sure that you check the paragraph margins logic
99+
# numbering_level = self.get_numbering_level()
100+
# if numbering_level and numbering_level.paragraph_properties:
101+
# properties.update(
102+
# dict(numbering_level.paragraph_properties.fields),
103+
# )
104+
105+
return ParagraphProperties(**properties)
106+
62107
@property
108+
@memoized
63109
def effective_properties(self):
64-
# TODO need to calculate effective properties like Run
65-
if not self._effective_properties:
66-
properties = self.properties
67-
self._effective_properties = properties
68-
return self._effective_properties
110+
inherited_properties = self.inherited_properties
111+
effective_properties = {}
112+
effective_properties.update(dict(inherited_properties.fields))
113+
if self.properties:
114+
effective_properties.update(dict(self.properties.fields))
115+
return ParagraphProperties(**effective_properties)
69116

70117
@property
71118
def numbering_definition(self):
@@ -76,12 +123,9 @@ def has_structured_document_parent(self):
76123
return self.has_ancestor(SdtBlock)
77124

78125
def get_style_chain_stack(self):
79-
if not self.properties:
80-
return
81-
82-
parent_style = self.properties.parent_style
83-
if not parent_style:
84-
return
126+
# Even if parent style is not defined we still need to check the default style
127+
# properties applied
128+
parent_style = getattr(self.properties, 'parent_style', None)
85129

86130
# TODO the getattr is necessary because of footnotes. From the context
87131
# of a footnote, a paragraph's container is the footnote part, which
@@ -117,9 +161,9 @@ def get_numbering_definition(self):
117161
part = getattr(self.container, 'numbering_definitions_part', None)
118162
if not part:
119163
return
120-
if not self.effective_properties:
164+
if not self.properties:
121165
return
122-
numbering_properties = self.effective_properties.numbering_properties
166+
numbering_properties = self.properties.numbering_properties
123167
if not numbering_properties:
124168
return
125169
return part.numbering.get_numbering_definition(
@@ -131,9 +175,9 @@ def get_numbering_level(self):
131175
numbering_definition = self.get_numbering_definition()
132176
if not numbering_definition:
133177
return
134-
if not self.effective_properties:
178+
if not self.properties:
135179
return
136-
numbering_properties = self.effective_properties.numbering_properties
180+
numbering_properties = self.properties.numbering_properties
137181
if not numbering_properties:
138182
return
139183
return numbering_definition.get_level(
@@ -231,70 +275,26 @@ def get_spacing(self):
231275
"""Get paragraph spacing according to:
232276
ECMA-376, 3rd Edition (June, 2011),
233277
Fundamentals and Markup Language Reference § 17.3.1.33.
234-
235-
Note: Partial implementation for now.
236278
"""
237279
results = {
238280
'line': None,
239281
'after': None,
240282
'before': None,
241-
'contextual_spacing': False,
242-
'parent_style': None
283+
'contextual_spacing': bool(self.effective_properties.contextual_spacing),
284+
'parent_style': self.effective_properties.parent_style
243285
}
244286

245-
# Get the paragraph_properties from the parent styles
246-
style_paragraph_properties = None
247-
for style in self.get_style_chain_stack():
248-
if style.paragraph_properties:
249-
style_paragraph_properties = style.paragraph_properties
250-
break
287+
spacing_properties = self.effective_properties.spacing_properties
251288

252-
if style_paragraph_properties:
253-
results['contextual_spacing'] = bool(style_paragraph_properties.contextual_spacing)
254-
255-
default_paragraph_properties = None
256-
if self.default_doc_styles and self.default_doc_styles.paragraph:
257-
default_paragraph_properties = self.default_doc_styles.paragraph.properties
258-
259-
# Spacing properties can be defined in multiple places and we need to get some
260-
# kind of order of check
261-
properties_order = [None, None, None]
262-
if self.properties:
263-
properties_order[0] = self.properties
264-
if isinstance(self.parent, TableCell):
265-
properties_order[1] = self.parent.parent_table.get_paragraph_properties()
266-
if not self.properties or not self.properties.spacing_properties:
267-
properties_order[2] = default_paragraph_properties
268-
269-
spacing_properties = None
270-
contextual_spacing = None
271-
272-
for properties in properties_order:
273-
if spacing_properties is None:
274-
spacing_properties = getattr(properties, 'spacing_properties', None)
275-
if contextual_spacing is None:
276-
contextual_spacing = getattr(properties, 'contextual_spacing', None)
277-
278-
if not spacing_properties:
289+
if spacing_properties is None:
279290
return results
280291

281-
if contextual_spacing is not None:
282-
results['contextual_spacing'] = bool(contextual_spacing)
283-
284-
if self.properties:
285-
results['parent_style'] = self.properties.parent_style
286-
287292
spacing_line = spacing_properties.to_int('line')
288293
spacing_after = spacing_properties.to_int('after')
289294
spacing_before = spacing_properties.to_int('before')
290295

291-
if default_paragraph_properties and spacing_line is None \
292-
and bool(spacing_properties.after_auto_spacing):
293-
# get the spacing_line from the default definition
294-
spacing_line = default_paragraph_properties.spacing_properties.to_int('line')
295-
296296
if spacing_line:
297-
line = spacing_line / 240.0
297+
line = float("%.2f" % (spacing_line / 240.0))
298298
# default line spacing is 1 so no need to add attribute
299299
if line != 1.0:
300300
results['line'] = line

pydocx/openxml/wordprocessing/paragraph_properties.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,4 @@ def no_indentation(self):
6969
self.indentation_hanging,
7070
self.indentation_right,
7171
self.indentation_first_line,
72-
))
72+
))

pydocx/openxml/wordprocessing/run.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,9 @@ class Run(XmlModel):
4646
)
4747

4848
def get_style_chain_stack(self):
49-
if not self.properties:
50-
return
51-
52-
parent_style = self.properties.parent_style
53-
if not parent_style:
54-
return
49+
# Even if parent style is not defined we still need to check the default style
50+
# properties applied
51+
parent_style = getattr(self.properties, 'parent_style', None)
5552

5653
# TODO the getattr is necessary because of footnotes. From the context
5754
# of a footnote, a paragraph's container is the footnote part, which
@@ -90,6 +87,11 @@ def _get_inherited_properties_from_parent_style(self):
9087
@property
9188
def inherited_properties(self):
9289
properties = {}
90+
if self.default_doc_styles and getattr(self.default_doc_styles.run, 'properties'):
91+
properties.update(
92+
dict(self.default_doc_styles.run.properties.fields),
93+
)
94+
9395
properties.update(
9496
self._get_properties_inherited_from_parent_paragraph(),
9597
)

pydocx/openxml/wordprocessing/style.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
unicode_literals,
66
)
77

8+
from pydocx.types import OnOff
89
from pydocx.models import XmlModel, XmlChild, XmlAttribute
910
from pydocx.openxml.wordprocessing.run_properties import RunProperties
1011
from pydocx.openxml.wordprocessing.paragraph_properties import ParagraphProperties
@@ -14,6 +15,7 @@ class Style(XmlModel):
1415
XML_TAG = 'style'
1516

1617
style_type = XmlAttribute(name='type', default='paragraph')
18+
style_default = XmlAttribute(type=OnOff, name='default', default='0')
1719
style_id = XmlAttribute(name='styleId', default='')
1820
name = XmlChild(attrname='val', default='')
1921
run_properties = XmlChild(type=RunProperties)

0 commit comments

Comments
 (0)