fix: drop per-field w:dirty, update fields on open via settings updateFields

Word's per-field dirty-driven field update crashes the full book with a hard
open error even at a single dirty PAGEREF (24-bisect isolation); the settings
level <w:updateFields w:val='true'/> batch path opens the same document with
correct TOC page numbers.
This commit is contained in:
bm
2026-09-23 11:21:32 -07:00
parent d2ad88166a
commit 16504d0b63
2 changed files with 43 additions and 10 deletions

View File

@@ -626,6 +626,32 @@ class RunningHeaderTests(unittest.TestCase):
"w:settings children out of CT_Settings order: "
+ ", ".join(t.split("}")[-1] for t in canonical))
def test_toc_fields_carry_no_dirty_flag(self):
# Per-field w:dirty crashes Word on this book (isolated over 24
# bisection files); PAGEREF begin runs must stay clean and rely on
# the settings-level updateFields batch path instead.
d = self._format()
n_begin = 0
for fc in d.element.body.iter(qn("w:fldChar")):
if fc.get(qn("w:fldCharType")) != "begin":
continue
rpr = fc.getparent().find(qn("w:rPr"))
if rpr is not None:
self.assertIsNone(
rpr.find(qn("w:dirty")),
"PAGEREF begin run must not carry w:dirty")
n_begin += 1
# The book ships two chapters in the test manuscript -> 2 TOC fields.
self.assertGreaterEqual(n_begin, 2)
def test_settings_request_field_update_on_open(self):
# Word recomputes the TOC page numbers through the settings-level
# <w:updateFields> batch path (proved to open the full book).
d = self._format()
uf = d.settings.element.find(qn("w:updateFields"))
self.assertIsNotNone(uf)
self.assertEqual(uf.get(qn("w:val")), "true")
def test_style_references_resolve(self):
# Word rejects documents whose paragraphs reference styles that are
# not defined in styles.xml (LO silently falls back to Normal).

View File

@@ -824,21 +824,21 @@ def _add_bookmark(para, bookmark_id, bookmark_name):
def _add_pageref_field(para, bookmark_name, font_name="Caladea", font_size=Pt(11)):
"""Add a PAGEREF field (page number of bookmark) to the paragraph.
Each field char is in its own run per OOXML spec (Word requires this).
The begin run carries w:dirty so Word knows to recalculate the field."""
The begin run deliberately carries NO w:dirty. Word's per-field dirty
update path crashes with the hard "Word experienced an error" dialog on
this book (proved empirically: 1 dirty field in the full shell fails;
the 28-field mini fails but the 3-field mini opens). Instead the whole
document enables <w:updateFields w:val="true"/> at the settings level
(see pass 2), which makes Word recompute every field on open through a
batch path that survives the same document.
"""
r1 = para.add_run()
r1.font.name = font_name
r1.font.size = font_size
fld1 = OxmlElement("w:fldChar")
fld1.set(qn("w:fldCharType"), "begin")
r1._r.append(fld1)
# Mark field as dirty so Word recalculates it on open
rpr = r1._r.find(qn('w:rPr'))
if rpr is None:
rpr = OxmlElement('w:rPr')
r1._r.insert(0, rpr)
dirty = OxmlElement('w:dirty')
dirty.set(qn('w:val'), '1')
rpr.append(dirty)
r2 = para.add_run()
r2.font.name = font_name
@@ -1888,8 +1888,15 @@ def format_document(filepath, output_path=None, font_name="Caladea", font_size=1
# w:settings per OOXML; Word honors it in modern layout compat mode.
if settings.find(qn('w:autoHyphenation')) is None:
settings.append(OxmlElement('w:autoHyphenation'))
# Fields update on open through the settings-level batch path. Per-field
# w:dirty cannot be used: Word's dirty-driven field update crashes this
# book with a hard open error (empirically isolated over 24 bisections).
if settings.find(qn('w:updateFields')) is None:
uf = OxmlElement('w:updateFields')
uf.set(qn('w:val'), 'true')
settings.append(uf)
# Word requires w:settings children in strict CT_Settings order, so fix up
# the position of the two elements just added (LibreOffice would not care).
# the position of the elements just added (LibreOffice would not care).
_reorder_settings(settings)
for idx, sec in enumerate(secs):