Round 2 Word bisection: isolate the field-run co-factor

bisect_03 (fields removed) was the only round-1 file Word opened, and
document.xml becomes byte-identical between the full book and bisect_03
once field runs are removed — but the mini book opens with identical
PAGEREF fields, so the fields interact with a second factor. Add six
candidates that keep the 28 PAGEREF fields while peeling other layers:

- 07 drop deep-copied chapter prose (keep titles/bookmarks/fields)
- 08 drop numbering properties
- 09 inline hyperlink wrappers
- 10 remove the whole TOC section (cleaner no-fields control)
- 11 rewrite all PAGEREF targets to a single bookmark
- 12 keep fields only for the first 3 TOC entries
This commit is contained in:
bm
2026-09-23 07:57:21 -07:00
parent 0031acf59e
commit 284ebad6af

View File

@@ -41,6 +41,12 @@ NAMES = [
'bisect_04_no_styles.docx',
'bisect_05_first8.docx',
'bisect_06_no_images.docx',
'bisect_07_no_prose.docx',
'bisect_08_no_numbering.docx',
'bisect_09_no_hyperlinks.docx',
'bisect_10_no_toc_section.docx',
'bisect_11_same_target_fields.docx',
'bisect_12_first3_toc.docx',
]
@@ -116,6 +122,85 @@ def make_no_images(doc):
p.getparent().remove(p)
def _sections(doc):
"""Return list of [start, end) kid-slices, one per body section,
using the in-paragraph sectPr holder paragraphs as boundaries."""
body = doc.element.body
holders = []
for s in body.iter(qn('w:sectPr')):
par = s.getparent().getparent()
if par is not None and par.getparent() is body:
holders.append(par)
kids = [c for c in body if c.tag == qn('w:p')]
secs, start = [], 0
for h in holders:
hi = kids.index(h)
secs.append((start, hi + 1))
start = hi + 1
if start < len(kids):
secs.append((start, len(kids)))
return kids, secs
def make_no_prose(doc):
"""Drop the deep-copied chapter prose (sections 5..32) while keeping each
chapter's title paragraph (with its bookmark) and section holder."""
kids, secs = _sections(doc)
body = doc.element.body
drop = []
for ci, (a, b) in enumerate(secs):
if 5 <= ci <= 32: # chapters 1..28
drop.extend(kids[a + 1:b - 1]) # keep title [a], holder [b-1]
for k in drop:
body.remove(k)
def make_no_numbering(doc):
"""Remove all list-numbering properties (keeps fields)."""
for np_ in list(doc.element.body.iter(qn('w:numPr'))):
np_.getparent().remove(np_)
def make_no_hyperlinks(doc):
"""Remove w:hyperlink wrappers, inlining their runs (keeps fields)."""
for hl in list(doc.element.body.iter(qn('w:hyperlink'))):
par = hl.getparent()
anchor = hl
for r in list(hl):
par.addnext(r)
par.remove(hl)
def make_no_toc_section(doc):
"""Remove the whole TOC section (heading + entries + its section holder)."""
kids, secs = _sections(doc)
body = doc.element.body
a, b = secs[4] # TOC section
for k in kids[a:b]:
body.remove(k)
def make_same_target_fields(doc):
"""Rewrite every PAGEREF target to _UF_ch_0 (uniform targets, fields kept)."""
for it in doc.element.body.iter(qn('w:instrText')):
if (it.text or '').strip().startswith('PAGEREF'):
it.text = ' PAGEREF _UF_ch_0 '
def make_first3_toc(doc):
"""Keep only the first 3 TOC entries' fields; plain-text the rest."""
seen = 0
for p in doc.element.body.iter(qn('w:p')):
if p.find(".//" + qn('w:instrText')) is None:
continue
if seen < 3:
seen += 1
continue
for r in list(p.findall(qn('w:r'))):
if r.find(qn('w:fldChar')) is not None or r.find(qn('w:instrText')) is not None:
p.remove(r)
MAKERS = [
make_no_running,
make_no_oddpage,
@@ -123,6 +208,12 @@ MAKERS = [
make_no_styles,
make_first8,
make_no_images,
make_no_prose,
make_no_numbering,
make_no_hyperlinks,
make_no_toc_section,
make_same_target_fields,
make_first3_toc,
]