Skip to content

Commit fbbd2f4

Browse files
committed
Retain formattings from default styles
1 parent 1a3f777 commit fbbd2f4

6 files changed

Lines changed: 80 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Retain formattings from default styles when preserving styles. [buchi]

docxcompose/composer.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections import OrderedDict
55
from copy import deepcopy
66

7+
from docx.enum.style import WD_STYLE_TYPE
78
from docx.opc.constants import CONTENT_TYPE as CT
89
from docx.opc.constants import RELATIONSHIP_TYPE as RT
910
from docx.opc.oxml import serialize_part_xml
@@ -79,6 +80,7 @@ def insert(self, index, doc, remove_property_fields=True):
7980
cprops.dissolve_fields(name)
8081

8182
self._create_style_id_mapping(doc)
83+
self.retain_formatting_from_default_styles(doc)
8284

8385
for element in doc.element.body:
8486
if isinstance(element, CT_SectPr):
@@ -299,6 +301,73 @@ def add_styles_from_other_parts(self, doc):
299301
else:
300302
self.add_styles(doc, el)
301303

304+
def retain_formatting_from_default_styles(self, doc):
305+
""""""
306+
if not self.preserve_styles:
307+
return
308+
style_id_name_mapping = {s.style_id: s.name for s in doc.styles}
309+
for style_type in WD_STYLE_TYPE:
310+
# Currently we only support retaining paragraph styles
311+
if style_type != WD_STYLE_TYPE.PARAGRAPH:
312+
continue
313+
style = doc.styles.default(style_type)
314+
if style is not None:
315+
our_style = self.doc.styles.default(style_type)
316+
if not xml_elements_equal(our_style.element, style.element):
317+
if style_type == WD_STYLE_TYPE.PARAGRAPH:
318+
# Get formattings from the style's run properties
319+
run_properties = xpath(style.element, ".//w:rPr/*")
320+
if run_properties:
321+
# If the run doesn't have run propertes (<w:rPr>),
322+
# we need to add them
323+
for el in xpath(doc.element, ".//w:r[not(w:rPr)]"):
324+
r_pr_element = parse_xml(
325+
'<w:rPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>'
326+
)
327+
el.insert(0, r_pr_element)
328+
# For every run property without a style, we add the
329+
# formattings from the default style.
330+
for el in xpath(doc.element, ".//w:rPr[not(w:rStyle)]"):
331+
# Figure out formattings defined in a style
332+
# from a parent element as we should not add these.
333+
parent = el.getparent()
334+
while parent is not None:
335+
style_ids = xpath(parent, "*/w:pStyle/@w:val")
336+
if style_ids:
337+
break
338+
parent = parent.getparent()
339+
formattings_from_style = []
340+
for style_id in style_ids:
341+
thestyle = doc.styles[
342+
style_id_name_mapping[style_id]
343+
]
344+
formattings_from_style = xpath(
345+
thestyle.element, "w:rPr/*|w:pPr/*"
346+
)
347+
for run_property in run_properties:
348+
if not any(
349+
[
350+
f.tag == run_property.tag
351+
for f in formattings_from_style
352+
]
353+
):
354+
el.append(deepcopy(run_property))
355+
# Get formattings from the style's paragraph properties
356+
paragraph_properties = xpath(style.element, ".//w:pPr/*")
357+
if paragraph_properties:
358+
# If the paragraph doesn't have paragraph propertes
359+
# (<w:pPr>), we need to add them
360+
for el in xpath(doc.element, ".//w:p[not(w:pPr)]"):
361+
p_pr_element = parse_xml(
362+
'<w:pPr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>'
363+
)
364+
el.insert(0, p_pr_element)
365+
# For every paragraph property without a style, we add the
366+
# formattings from the default style.
367+
for el in xpath(doc.element, ".//w:pPr[not(w:pStyle)]"):
368+
for paragraph_property in paragraph_properties:
369+
el.append(deepcopy(paragraph_property))
370+
302371
def add_styles(self, doc, element):
303372
"""Add styles from the given document used in the given element."""
304373
our_style_ids = [s.style_id for s in self.doc.styles]
12.2 KB
Binary file not shown.

tests/docs/styles_default1.docx

13.9 KB
Binary file not shown.

tests/docs/styles_default2.docx

14 KB
Binary file not shown.

tests/test_styles.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ def test_preserve_styles_does_not_duplicate_identical_styles():
101101
] == ["MyCustomStyle", "MyCustomStyleZchn", "MyCustomStyle_1"]
102102

103103

104+
def test_retain_formatting_from_default_styles():
105+
composer = Composer(
106+
Document(docx_path("styles_default1.docx")), preserve_styles=True
107+
)
108+
composer.append(Document(docx_path("styles_default2.docx")))
109+
composed = ComparableDocument(composer.doc)
110+
expected = FixtureDocument("styles_default.docx")
111+
assert composed == expected
112+
113+
104114
@pytest.fixture
105115
def merged_styles():
106116
composer = Composer(Document(docx_path("styles_en.docx")))

0 commit comments

Comments
 (0)