Problem
ooxml-swift v0.27.0 round-trip on a docx with multiple <w:sectPr> (one per section break, e.g. front-matter / body / title-page) collapses all sections into a single generic sectPr at the end of document.xml. The collapsed sectPr drops:
<w:headerReference w:type="default|even|first" r:id="..."/> — every section's header invocations
<w:footerReference w:type="default|even|first|first" r:id="..."/> — every section's footer invocations
- Section-break positions (the section structure is flattened — original break locations gone)
- Section-level page properties (
<w:pgSz>, <w:pgMar>, <w:pgNumType>, <w:titlePg/>)
Even when header/footer XML files exist with rich content, none of them are activated because no section references them. Word opens the docx without errors but renders no watermark, no page numbers, no chapter-aware page numbering format (lowerRoman vs decimal).
Discovered while rescuing the NTPU master's thesis (kiki830621/collaboration_guo_analysis#20) immediately after fixing #65 (replaceText) and #66 (header/footer VML).
Type
bug
Evidence
Reproduction: DocxReader.read(_raw.docx) then DocxWriter.write(_, to: out.docx) with no edits in between.
_raw.docx — 3 sections with full structure
Section 1 (front matter, lowerRoman pages):
<w:sectPr w:rsidR="008A36FA" w:rsidRPr="000128A9" w:rsidSect="00F25889">
<w:headerReference w:type="even" r:id="rId8"/>
<w:headerReference w:type="default" r:id="rId9"/>
<w:footerReference w:type="default" r:id="rId10"/>
<w:headerReference w:type="first" r:id="rId11"/>
<w:footerReference w:type="first" r:id="rId12"/>
<w:pgSz w:w="11906" w:h="16838"/>
<w:pgMar w:top="1440" w:right="1797" w:bottom="1440" w:left="1797"
w:header="1474" w:footer="397" w:gutter="0"/>
<w:pgNumType w:fmt="lowerRoman" w:start="1"/>
<w:cols w:space="425"/>
<w:docGrid w:type="lines" w:linePitch="381"/>
</w:sectPr>
Section 2 (body, different headers/footers):
<w:sectPr w:rsidR="0020589C" w:rsidRPr="005611B2" w:rsidSect="0089106F">
<w:headerReference w:type="even" r:id="rId13"/>
<w:headerReference w:type="default" r:id="rId14"/>
<w:footerReference w:type="default" r:id="rId15"/>
<w:headerReference w:type="first" r:id="rId16"/>
<w:footerReference w:type="first" r:id="rId17"/>
…same pgSz/pgMar/pgNumType/etc…
</w:sectPr>
Section 3 (cover page, no headers/footers):
<w:sectPr …>
<w:pgSz w:w="11906" w:h="16838"/>
…
<w:titlePg/>
</w:sectPr>
v0.27.0 output — single generic sectPr, no references
Total 228 chars:
<w:sectPr>
<w:pgSz w:w="12240" w:h="15840"/>
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"
w:header="720" w:footer="720" w:gutter="0"/>
<w:cols w:space="720" w:num="1"/>
<w:docGrid w:linePitch="360"/>
</w:sectPr>
Diff:
- 3
<w:sectPr> → 1
- 5
<w:headerReference> × 2 sections → 0
- 4
<w:footerReference> × 2 sections → 0
<w:titlePg/> (cover page) → lost
<w:pgNumType w:fmt="lowerRoman"/> → lost (defaults to decimal)
- Page size: A4 (11906 × 16838) → Letter (12240 × 15840) — likely default fallback because original pgSz dropped
- Page margins: thesis margins (1797/1474/397) → Word defaults (1440/720/720)
document.xml.rels correctly maps rId8..rId17 → header1-6.xml + footer1-4.xml, all files exist in the zip. They are orphaned because the body never invokes them via <w:sectPr>.
Expected
Round-trip preserves:
- Section break positions (
<w:p><w:pPr><w:sectPr>…</w:sectPr></w:pPr></w:p> mid-document and final-paragraph-with-sectPr at end)
- Each
<w:sectPr> byte-equivalent (or at least: all <w:headerReference> / <w:footerReference> / <w:titlePg/> / <w:pgSz> / <w:pgMar> / <w:pgNumType> retained)
- Multi-section structure → multi-section output
Actual
All sections collapse to one generic sectPr at the end of body. References lost. Page properties reset to Word defaults.
Impact
Workaround for thesis #20
Surgical Python sectPr swap: replace v0.27.0's single empty sectPr with _raw section 2's sectPr (string-level inject). Single-section result, but watermark + footer references active. Page-number format distinction (lowerRoman in front matter vs decimal in body) is lost — accepted as P3 cosmetic compromise for thesis-submission deadline.
Reproduction recipe
import OOXMLSwift
// _raw.docx has 3 sectPr in document.xml; v0.27.0 output has 1
let inURL = URL(fileURLWithPath: "/path/to/_raw.docx")
let outURL = URL(fileURLWithPath: "/tmp/out.docx")
var doc = try DocxReader.read(from: inURL)
try DocxWriter.write(doc, to: outURL)
// inspect:
// unzip -p _raw.docx word/document.xml | grep -c '<w:sectPr' → 3
// unzip -p out.docx word/document.xml | grep -c '<w:sectPr' → 1
// unzip -p out.docx word/document.xml | grep -c '<w:headerReference' → 0
Long-term fix sketch
DocumentParser (or whoever owns <w:body> parsing) needs to:
- Capture
<w:sectPr> as a typed structure (model class SectionProperties) holding:
headerReferences: [HeaderReference] (with type: even|default|first, relId: String)
footerReferences: [FooterReference]
pageSize: PageSize?
pageMargin: PageMargin?
pageNumberType: PageNumberType?
titlePage: Bool
rawXML: String (for byte-equivalent fallback while typed coverage incomplete)
- Capture both mid-document section breaks (sectPr inside
<w:p><w:pPr>) AND final-section sectPr (direct child of <w:body>).
- Track section break paragraph positions so the writer emits sectPr at the same body position.
DocumentWriter re-emits sectPr blocks at captured positions with all header/footer references intact.
If full typed coverage is too large for a single PR, an interim fix is just:
- Capture rawXML for every
<w:sectPr> and its enclosing paragraph break verbatim → emit verbatim. No typed access, but byte-equivalent for round-trip safety.
Relationship to #62 / #66
- #62 — VML/wps/wpg in body
- #66 — VML in headers/footers (sister of this issue)
- This issue (#NEW) — section structure / sectPr in document.xml
All three are surfaced during the same NTPU thesis rescue. They're orthogonal but compound: #62 fixes body shapes, #66 fixes header/footer VML, this issue activates header/footer references so they render. All three needed for full thesis fidelity.
Source
Surfaced during /idd-issue invocation while rescuing NTPU thesis (kiki830621/collaboration_guo_analysis#20). Confirmed via Python diff of _raw.docx vs ooxml-swift v0.27.0 round-trip output.
Current Status
Phase: implemented
Last updated: 2026-05-05 by idd-implement
Key Decisions
Scope Changes
- (none — Phase A scope held strictly per plan)
Blocking
- (none — ready for /idd-verify)
Tangential (filed during plan drafting Step 2.5)
Commits
Problem
ooxml-swift v0.27.0 round-trip on a docx with multiple
<w:sectPr>(one per section break, e.g. front-matter / body / title-page) collapses all sections into a single generic sectPr at the end ofdocument.xml. The collapsed sectPr drops:<w:headerReference w:type="default|even|first" r:id="..."/>— every section's header invocations<w:footerReference w:type="default|even|first|first" r:id="..."/>— every section's footer invocations<w:pgSz>,<w:pgMar>,<w:pgNumType>,<w:titlePg/>)Even when header/footer XML files exist with rich content, none of them are activated because no section references them. Word opens the docx without errors but renders no watermark, no page numbers, no chapter-aware page numbering format (lowerRoman vs decimal).
Discovered while rescuing the NTPU master's thesis (kiki830621/collaboration_guo_analysis#20) immediately after fixing #65 (replaceText) and #66 (header/footer VML).
Type
bug
Evidence
Reproduction:
DocxReader.read(_raw.docx)thenDocxWriter.write(_, to: out.docx)with no edits in between._raw.docx — 3 sections with full structure
v0.27.0 output — single generic sectPr, no references
Diff:
<w:sectPr>→ 1<w:headerReference>× 2 sections → 0<w:footerReference>× 2 sections → 0<w:titlePg/>(cover page) → lost<w:pgNumType w:fmt="lowerRoman"/>→ lost (defaults to decimal)document.xml.relscorrectly maps rId8..rId17 → header1-6.xml + footer1-4.xml, all files exist in the zip. They are orphaned because the body never invokes them via<w:sectPr>.Expected
Round-trip preserves:
<w:p><w:pPr><w:sectPr>…</w:sectPr></w:pPr></w:p>mid-document and final-paragraph-with-sectPr at end)<w:sectPr>byte-equivalent (or at least: all<w:headerReference>/<w:footerReference>/<w:titlePg/>/<w:pgSz>/<w:pgMar>/<w:pgNumType>retained)Actual
All sections collapse to one generic sectPr at the end of body. References lost. Page properties reset to Word defaults.
Impact
Workaround for thesis #20
Surgical Python sectPr swap: replace v0.27.0's single empty sectPr with
_rawsection 2's sectPr (string-level inject). Single-section result, but watermark + footer references active. Page-number format distinction (lowerRoman in front matter vs decimal in body) is lost — accepted as P3 cosmetic compromise for thesis-submission deadline.Reproduction recipe
Long-term fix sketch
DocumentParser(or whoever owns<w:body>parsing) needs to:<w:sectPr>as a typed structure (model classSectionProperties) holding:headerReferences: [HeaderReference](withtype: even|default|first,relId: String)footerReferences: [FooterReference]pageSize: PageSize?pageMargin: PageMargin?pageNumberType: PageNumberType?titlePage: BoolrawXML: String(for byte-equivalent fallback while typed coverage incomplete)<w:p><w:pPr>) AND final-section sectPr (direct child of<w:body>).DocumentWriterre-emits sectPr blocks at captured positions with all header/footer references intact.If full typed coverage is too large for a single PR, an interim fix is just:
<w:sectPr>and its enclosing paragraph break verbatim → emit verbatim. No typed access, but byte-equivalent for round-trip safety.Relationship to #62 / #66
All three are surfaced during the same NTPU thesis rescue. They're orthogonal but compound: #62 fixes body shapes, #66 fixes header/footer VML, this issue activates header/footer references so they render. All three needed for full thesis fidelity.
Source
Surfaced during /idd-issue invocation while rescuing NTPU thesis (kiki830621/collaboration_guo_analysis#20). Confirmed via Python diff of
_raw.docxvs ooxml-swift v0.27.0 round-trip output.Current Status
Phase: implemented
Last updated: 2026-05-05 by idd-implement
Key Decisions
de83eef—parseSectPrhelper + signature cascade (parseBodyChildren / parseBody / top-level caller / recursive sdtContent discard) all in single coordinated commit per plan.Issue67SectPrRoundTripTestsall GREEN. Full suite: 883 → 888 ✓, 0 regressions, 1 pre-existing skip preserved.rawChildren: [String]field added toSectionPropertiesfor byte-equivalent unknown-children preservation (architectural pattern:Run.rawElementsv0.14.0+/[test] Test 25.4 (endnote) missing modifiedParts assertion (asymmetry vs 25.1-25.3) #52).<w:sectPr>(DocxReader:823). Phase B / Phase C deferred per planned scope.Scope Changes
Blocking
Tangential (filed during plan drafting Step 2.5)
<w:rFonts>(344 occurrences in thesis output)Commits
de83eef— fix(sectPr): Phase A — DocxReader parses body-level <w:sectPr> (Multi-section <w:sectPr> structure collapses to single generic sectPr — headerReference / footerReference lost #67)