Skip to content

Commit 4bd54a2

Browse files
fix: ensure font name propagates correctly to existing styles (#74)
* fix(declarative): remove theme font attrs when setting explicit font on heading styles * version bump
1 parent 7e8ec33 commit 4bd54a2

3 files changed

Lines changed: 46 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cmi-docx"
3-
version = "0.6.5"
3+
version = "0.6.6"
44
description = "Additional tooling for Python-docx."
55
readme = "README.md"
66
requires-python = ">=3.12"

src/cmi_docx/declarative/document.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def _apply_style_definitions(
179179
_apply_table_style_definition(docx_doc, defn)
180180

181181

182-
def _apply_paragraph_style_definition( # noqa: C901, PLR0912
182+
def _apply_paragraph_style_definition( # noqa: C901, PLR0912, PLR0915
183183
docx_doc: docx_document.Document,
184184
defn: styles_mod.ParagraphStyleDefinition,
185185
) -> None:
@@ -205,6 +205,21 @@ def _apply_paragraph_style_definition( # noqa: C901, PLR0912
205205
font = style.font
206206
if defn.font is not None:
207207
font.name = defn.font
208+
# font.element is the <w:style> element; <w:rFonts> lives inside <w:rPr>.
209+
# We must search the nested <w:rPr> to locate it.
210+
rpr = font.element.find(qn("w:rPr"))
211+
r_fonts = rpr.find(qn("w:rFonts")) if rpr is not None else None
212+
if r_fonts is None:
213+
# Built-in styles may already have <w:rPr> but no <w:rFonts>;
214+
# create and insert it as the first child of <w:rPr>.
215+
if rpr is None:
216+
rpr = etree.SubElement(font.element, qn("w:rPr"))
217+
r_fonts = etree.SubElement(rpr, qn("w:rFonts"))
218+
rpr.insert(0, r_fonts)
219+
r_fonts.set(qn("w:ascii"), defn.font)
220+
r_fonts.set(qn("w:hAnsi"), defn.font)
221+
r_fonts.attrib.pop(qn("w:asciiTheme"), None)
222+
r_fonts.attrib.pop(qn("w:hAnsiTheme"), None)
208223
if defn.font_size is not None:
209224
font.size = shared.Pt(defn.font_size)
210225
if defn.bold is not None:

tests/declarative/test_declarative_styles.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,32 @@ async def test_table_style_first_row_xml() -> None:
171171
shd = tc_pr.find(qn("w:shd"))
172172
assert shd is not None
173173
assert shd.get(qn("w:fill")) == "003366"
174+
175+
176+
@pytest.mark.asyncio
177+
async def test_heading_style_font_overrides_theme() -> None:
178+
"""Test that setting font on a built-in heading style removes theme font attributes.
179+
180+
When font="Arial" is applied to the built-in "Heading 1" style via
181+
ParagraphStyleDefinition, the resolved style must:
182+
- Report style.font.name == "Arial" (the explicit font wins).
183+
- Have no w:asciiTheme attribute on the <w:rFonts> element, so the theme
184+
font cannot silently override the explicit font name at render time.
185+
"""
186+
doc = _make_doc(
187+
[declarative.ParagraphStyleDefinition(name="Heading 1", font="Arial")],
188+
sections=[
189+
declarative.Section(
190+
children=[declarative.Paragraph(heading=1, text="Hello")],
191+
)
192+
],
193+
)
194+
195+
docx = await doc.to_docx()
196+
197+
style = docx.styles["Heading 1"]
198+
assert style.font.name == "Arial"
199+
200+
r_fonts = style.font.element.rPr.find(qn("w:rFonts"))
201+
assert r_fonts is not None
202+
assert r_fonts.get(qn("w:asciiTheme")) is None

0 commit comments

Comments
 (0)