Skip to content

feat(ooxml,cli): DOCX writer — M3 3.6 - #39

Merged
aenawi merged 1 commit into
mainfrom
docx-writer
Sep 5, 2026
Merged

aenawi merged 1 commit into
mainfrom
docx-writer

Conversation

@aenawi

@aenawi aenawi commented Sep 5, 2026

Copy link
Copy Markdown
Owner

mirsam repair report.docx fixed.docx works, and repair stops turning a Word document away as a readable format without a writer — PLAN §3.6, which closes M3. The third OOXML vocabulary on one token-rewrite scaffold, and the first writer that turned out simpler than the one before it.

What lands where

word.rs is WordprocessingML's repair vocabulary — what rewrite.rs is for DrawingML and sheet.rs for SpreadsheetML. Not one Word element name entered token.rs to carry it.

Fix Element
SetDirection w:pPr/w:bidi, and w:tblPr/w:bidiVisual on a table
SetAlignment w:pPr/w:jc
SetLanguage w:rPr/w:lang@w:bidi
SetComplexFont w:rPr/w:rFonts@w:cs
ConvertLiteralBullet w:pPr/w:numPr

Each is placed by rank against the schema sequence its parent is. CT_PPrBase and CT_RPr are xsd:sequence, and a correct element in the wrong position is a document Word offers to repair.

The inheritance pass is absent, not skipped

§1.2's hardest correction was that lowering Start onto DrawingML's physical algn without knowing the paragraph's direction reproduces the defect being repaired. That is why rewrite::Inherited exists, and why PptxDocument::apply re-runs its scanner over every part it touches.

w:jc is evaluated against the paragraph's own w:bidi (MS-OE376 Part 4 §2.3.1.13, note b), so Start is written start whichever way the paragraph runs, and stays the start edge if the direction changes afterwards. There is nothing here to resolve. This adapter has no Inherited type at all.

Two refusals, both the format's

  • A physical edge. Word has no spelling for "the left of the page whatever the direction" — which is the refusal the conformance suite already records for the reading side. supports answers it for the writing side, so SetAlignment(Left) appears in repairs.skipped rather than being lowered onto whichever relative edge happens to land left today. No Word unit ever produces one, so this will not be seen in practice; it is stated so the writer cannot start inventing it.
  • A typed bullet with no list to join. A Word bullet is a w:numPr pointing into the numbering part, so it can only join a list the document already defines. package.rs replaces the entries a package holds, and a numbering part that is not there is a part, a content-type override and a relationship — none of which is an edit to the paragraph the finding named. bullet_list prefers a definition whose first level draws the marker the author typed, falls back to any bulleted one, and refuses a w:num that overrides its abstract at level zero.

Three smaller decisions, each a defect the other way

  • <w:bidi/> is on. A left-to-right repair writes w:val="0" and never an empty element. Creating one to mean off would turn the repair into the defect it was sent to fix.
  • Filling @w:cs removes @w:cstheme. The theme reference is what Word renders and @w:cs the value it caches for consumers that do not implement themes. Writing into the cache and leaving the reference standing is a repair that reports success and changes nothing a reader sees.
  • xml:space="preserve" on the runs this repair rewrote. Stripping a typed • leaves the space behind it, and Word collapses a w:t's edge whitespace unless the run says otherwise — a space nobody asked to delete. Only runs whose text actually changed are marked, so the rest of the paragraph still comes out byte for byte.

What is not this paragraph

token.rs gained the ability to step over a caller-named set of elements, and Word is why.

A w:txbxContent nests whole paragraphs inside a run, and each is a unit with its own ordinal, its own text and its own repairs; an offset the domain computed for the outer paragraph indexes the outer paragraph's runs alone. An mc:Fallback is stepped over for the reader's reason — it spells out the same paragraphs as the mc:Choice beside it, and a rewriter that counted both would put paragraph 2's repair on the fallback's copy of paragraph 1: a report and a document that had come apart while both still looked right.

It also gained remove_attribute, the counterpart set_attribute needed once a repair had to stop a second attribute contradicting the one being written.

The corpus, which is what the recorded refusal was for

Both .docx reports lost their refused line and gained a repair report. quarterly-review.docx now carries a changed_parts diff naming exactly the tags above, and its after audit is empty — every fixable finding in a real Word document cleared.

make validate-fixtures accepts the repaired copy against the ECMA-376 transitional schemas, and a second repair of that output stages nothing and reproduces it byte for byte.

The CLI case that proved the refusal moved to .html, the format that still gives one, rather than being deleted with the adapter that outgrew it.

Checks

make verify clean — version, fmt, clippy -D warnings, tests, docs, release build. crates/mirsam-ooxml/tests/word.rs adds 38 cases: the rewriter asserted on the whole part, as §1.2's are, plus the DocumentWriter port over a real package. make validate-fixtures passes on all nine corpus packages.

Not run: "Word opens the repaired document without offering to repair it" is the M1 application check in its Word form, and it cannot run in CI. Schema validity is the strongest claim a machine can make here and is what the acceptance rests on. It is not the same claim.

`mirsam repair report.docx fixed.docx` works, and `repair` stops turning a
Word document away as a readable format without a writer. M3 is closed.

`word.rs` is WordprocessingML's repair vocabulary — what `rewrite.rs` is
for DrawingML and `sheet.rs` for SpreadsheetML — and it is the third
vocabulary built on `token.rs` without a Word element name entering it.
Every mutation is a scaffold call with a name in it: `w:pPr/w:bidi`,
`w:pPr/w:jc`, `w:rPr/w:lang@w:bidi`, `w:rPr/w:rFonts@w:cs`,
`w:pPr/w:numPr` and `w:tblPr/w:bidiVisual`, each placed by rank against
the schema sequence its parent is, because `CT_PPrBase` and `CT_RPr` are
`xsd:sequence` and a correct element in the wrong position is a document
Word offers to repair.

The writer needs no inherited direction, and that is Word's doing rather
than a shortcut. 1.2's hardest correction was that lowering `Start` onto
DrawingML's physical `algn` without knowing the paragraph's direction
reproduces the defect being repaired — which is why `rewrite::Inherited`
exists and why `PptxDocument::apply` re-runs its scanner over every part
it touches. `w:jc` is evaluated against the paragraph's own `w:bidi`, so
`Start` is written `start` whichever way the paragraph runs and stays the
start edge if the direction changes afterwards. The whole inheritance
pass is absent from this adapter, not skipped in it.

Two refusals, and both are the format's. A physical edge is not something
a Word paragraph can state, which is the refusal the conformance suite
already records for the reading side; `supports` answers it for the
writing side, so `SetAlignment(Left)` is listed in `repairs.skipped`
rather than lowered onto whichever relative edge happens to land left
today. And a typed bullet becomes a `w:numPr` pointing into the numbering
part, so it can only join a list the document already defines —
`package.rs` replaces the entries a package holds, and a numbering part
that is not there is a part, a content-type override and a relationship,
none of which is an edit to the paragraph the finding named.
`bullet_list` prefers a definition whose first level draws the marker the
author typed, falls back to any bulleted one, and refuses a `w:num` that
overrides its abstract at level zero.

Three smaller decisions, each a defect if taken the other way. `<w:bidi/>`
is on, so a left-to-right repair writes `w:val="0"` and never an empty
element: creating one to mean off would turn the repair into the defect
it was sent to fix. Filling `@w:cs` removes `@w:cstheme` beside it,
because the theme reference is what Word renders and `@w:cs` the value it
caches for consumers that do not implement themes — writing into the
cache and leaving the reference standing is a repair that reports success
and changes nothing a reader sees. And a `w:t` whose text this repair
changed is marked `xml:space="preserve"` where the result has an edge of
whitespace, because stripping a typed bullet leaves the space behind it
and Word would otherwise collapse a space nobody asked to delete; only
the runs actually rewritten are marked, so the rest of the paragraph
still comes out byte for byte.

`token.rs` gained the ability to step over a caller-named set of
elements, and Word is why. A `w:txbxContent` nests whole paragraphs
inside a run, and each is a unit with its own ordinal, its own text and
its own repairs — offsets the domain computed for the outer paragraph
index the outer paragraph's runs alone. An `mc:Fallback` is stepped over
for the reader's reason: it spells out the same paragraphs as the
`mc:Choice` beside it, and a rewriter that counted both would put
paragraph 2's repair on the fallback's copy of paragraph 1, a report and
a document that had come apart while both still looked right. It also
gained `remove_attribute`, the counterpart `set_attribute` needed once a
repair had to stop a second attribute contradicting the one being
written.

The corpus is where it shows, which is what the recorded refusal was for.
Both `.docx` reports lost their `refused` line and gained a repair
report; `quarterly-review.docx` now carries a `changed_parts` diff naming
exactly the tags above, and its `after` audit is empty — every fixable
finding in a real Word document cleared. `make validate-fixtures` accepts
the repaired copy against the ECMA-376 transitional schemas, and a second
`repair` of that output stages nothing and reproduces it byte for byte.
The CLI case that proved the refusal moved to `.html`, the format that
still gives one, rather than being deleted with the adapter that outgrew
it.

Not run: "Word opens the repaired document without offering to repair
it", the M1 application check in its Word form, cannot run in CI. Schema
validity is the strongest claim a machine can make here and is what the
acceptance rests on. It is not the same claim.
@aenawi
aenawi merged commit 5c7741f into main Sep 5, 2026
7 checks passed
@aenawi
aenawi deleted the docx-writer branch September 5, 2026 11:58
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.

1 participant