Skip to content

fix(docx): make Word accept the exported package - #45

Merged
entelecheia merged 1 commit into
mainfrom
fix/docx-word-compat
Aug 1, 2026
Merged

fix(docx): make Word accept the exported package#45
entelecheia merged 1 commit into
mainfrom
fix/docx-word-compat

Conversation

@entelecheia

Copy link
Copy Markdown
Member

Follow-up to #44. The exported DOCX either failed to open in Word or rendered wrong on real documents.

Root cause

CT_PPr and CT_RPr are strict XSD sequences. The writer emitted children in authoring order and put w:spacing in pPr twice, so Word rejected the document at the first violation. Two further defects broke tables and lists independently.

Fixes

  • pPr order/cardinality — emit pStyle → numPr → spacing → ind → jc; line spacing is merged with before/after into the single w:spacing the schema allows.
  • rPr order — reordered to the CT_RPr sequence (color/spacing before sz; u/shd/vertAlign after). Superscript and subscript are now mutually exclusive.
  • Run containment — hyphen, non-breaking/full-width space, line break and tab were written as bare content directly under w:p. They now open a run like text does.
  • Numbering levels — every abstractNum defines all 9 levels (w:ilvl 0..=8), so a numPr can never reference an undefined level; HWP's 10th level is dropped (w:ilvl > 8 is invalid) and numPr ilvl is clamped. Bullet lvlText is XML-escaped.
  • Table merges — a colspan+rowspan origin emitted one vMerge cell per covered column, each carrying the full gridSpan, so covered rows overflowed the grid (a 3-column table produced 4-column rows). Only the origin column emits a vMerge cell now. w:tcW accounts for the span, and a cell ending in a nested table gets the trailing w:p the schema requires.
  • Text sanitation — C0 control characters that make the XML part unparseable are dropped.

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 against w:gridCol count, and numId/ilvl resolution against numbering.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): green
  • 21 local hwp/hwpx samples converted and validated: 0 problems (the pre-fix outputs of the same documents report undefined numId, pPr order violations, and rows covering 4 of 3 grid columns)

Word-on-real-hardware confirmation is still outstanding.

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.
Copilot AI review requested due to automatic review settings August 1, 2026 21:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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:spacing and making super/subscript mutually exclusive).
  • Ensured all run content (w:t, w:br, w:tab, etc.) is contained inside w:r, and hardened numbering/table emission (9 ilvls, clamping, correct vMerge/gridSpan handling, trailing w:p in w: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();
@entelecheia
entelecheia merged commit 7863e23 into main Aug 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants