fix(docx): make Word accept the exported package - #45
Conversation
Word validates CT_PPr/CT_RPr as strict sequences and rejects the whole document on the first violation. The GJ-1 writer emitted properties in authoring order, so real documents either failed to open or rendered wrong. - pPr: emit pStyle -> numPr -> spacing -> ind -> jc, and merge line spacing with before/after into the single w:spacing the schema allows - rPr: reorder to the CT_RPr sequence (color/spacing before sz, u/shd/vertAlign after); superscript and subscript are now mutually exclusive - runs: hyphen, non-breaking space, line break and tab were written as bare content under w:p; they now open a run like text does - numbering: every abstractNum defines all 9 levels (w:ilvl 0..=8) so a numPr can never point at an undefined level; HWP's 10th level is dropped and numPr ilvl is clamped - tables: a colspan+rowspan origin emitted one vMerge cell per covered column, each carrying the full gridSpan, so covered rows overflowed the grid; only the origin column emits one now. w:tcW accounts for the span, and a cell ending in a nested table gets the trailing w:p the schema requires - text: drop C0 control characters that make the XML part unparseable Adds validate_docx() to the unit tests: it walks the emitted package checking property order and cardinality, run containment, per-row grid coverage, and numId/ilvl resolution against numbering.xml. The previous substring assertions passed on every one of these defects.
There was a problem hiding this comment.
🟡 Not ready to approve
The new validate_docx() test code likely does not compile due to String::from_utf8_lossy(&t) on quick_xml::events::BytesText and should use xml10_content() (or equivalent) instead.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
This PR is a follow-up fix to the DOCX writer to make emitted OOXML packages schema-valid enough for Microsoft Word to open/render correctly, addressing strict CT_PPr / CT_RPr ordering, run containment rules, numbering level validity, table grid/merge correctness, and XML text sanitization.
Changes:
- Reworked paragraph/run property emission to follow OOXML XSD sequence ordering and cardinality constraints (including merging spacing into a single
w:spacingand making super/subscript mutually exclusive). - Ensured all run content (
w:t,w:br,w:tab, etc.) is contained insidew:r, and hardened numbering/table emission (9ilvls, clamping, correct vMerge/gridSpan handling, trailingw:pinw:tc). - Added
validate_docx()structural validator to tests to catch Word-breaking structural issues beyond substring assertions.
File summaries
| File | Description |
|---|---|
| crates/hwp-convert/src/docx.rs | Fixes OOXML ordering/cardinality, run containment, numbering/table correctness, and adds structural DOCX validation in unit tests. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| match reader.read_event().unwrap() { | ||
| Event::Eof => break, | ||
| Event::Text(t) => { | ||
| let raw = String::from_utf8_lossy(&t).to_string(); |
Follow-up to #44. The exported DOCX either failed to open in Word or rendered wrong on real documents.
Root cause
CT_PPrandCT_RPrare strict XSD sequences. The writer emitted children in authoring order and putw:spacinginpPrtwice, so Word rejected the document at the first violation. Two further defects broke tables and lists independently.Fixes
pStyle → numPr → spacing → ind → jc; line spacing is merged with before/after into the singlew:spacingthe schema allows.CT_RPrsequence (color/spacingbeforesz;u/shd/vertAlignafter). Superscript and subscript are now mutually exclusive.w:p. They now open a run like text does.abstractNumdefines all 9 levels (w:ilvl0..=8), so anumPrcan never reference an undefined level; HWP's 10th level is dropped (w:ilvl> 8 is invalid) andnumPrilvl is clamped. BulletlvlTextis XML-escaped.vMergecell per covered column, each carrying the fullgridSpan, so covered rows overflowed the grid (a 3-column table produced 4-column rows). Only the origin column emits avMergecell now.w:tcWaccounts for the span, and a cell ending in a nested table gets the trailingw:pthe schema requires.Verification
Adds
validate_docx()to the unit tests. It walks the emitted package and checks property order and cardinality, run containment, per-row grid coverage againstw:gridColcount, andnumId/ilvlresolution againstnumbering.xml. The previous substring assertions passed on every one of the defects above; this validator fails on all of them.scripts/check.sh(fmt + clippy + test + structured corpus): greennumId,pProrder violations, and rows covering 4 of 3 grid columns)Word-on-real-hardware confirmation is still outstanding.