Conversation
Every public output (save, to_bytes, Flat OPC, package class, signing and encryption) marked an existing comments model dirty before staging, so a save without any comment edit serialized the comments part again through the typed model. An empty self-closed root came back as an open and close pair, and Word's comment attribute order changed. Compare reads the original comments from the package bytes, so it then refused a document against its own save with "comments story root shell changed". The output boundary now marks the model dirty only when it no longer matches a fresh parse of its part, as styles, settings and the comments-extended part already do. An unchanged part keeps its bytes, which also leaves a package signature over it valid. This revises the F-255 canonical comments boundary. The F-255 test now pins the byte-identical output without a comment edit, and keeps its canonical rewrite, custom target and Flat OPC checks behind a comment edit. GitHub issue tensorbee#160.
CT_Comments::to_xml wrote xmlns:w first, skipped the retained xmlns:w and xmlns:w14 declarations, and wrote xmlns:w14 again only when a comment paragraph carried a paragraph id. A rewritten part without one, such as a Word comments part after its last comment is removed, kept mc:Ignorable="w14 ..." without declaring w14, which Markup Compatibility forbids. The root now writes its retained attributes in source order. A retained w or w14 declaration stays where it stands and binds the namespace the serializer writes. The fixed declarations come first only when the source lacks them, so a part rdocx authored serializes as before. GitHub issue tensorbee#160.
CT_Document and CT_HdrFtr captured only the namespace declarations of their root. Any typed rewrite of document.xml, which one replacement is enough to cause, or of a header or footer therefore dropped mc:Ignorable while the prefixes it lists stayed declared and in use, such as w14:paraId on every paragraph Word writes. Both models now keep the other root attributes in source order and write them after the namespace declarations. Every prefix such an attribute lists stays declared, since the rewrite already keeps every producer declaration. CT_Document gains a hidden public root_attributes field, as background_extra_xml was added, so a struct literal outside rdocx-oxml needs the new field. CT_HdrFtr already has a private field and keeps the new one private. GitHub issue tensorbee#160.
rdocx validate checked relationships and content types, plus advisory findings, so it passed the comments part that earlier saves wrote with w14 listed in mc:Ignorable and no w14 declaration. Markup Compatibility requires every listed prefix to be declared, and a consumer may reject such a part. rdocx-oxml gains undeclared_compatibility_prefixes, which reports each prefix that an mc:Ignorable or mc:MustUnderstand attribute lists without a declaration in scope. validate runs it over every XML part in part name order and reports each finding as an error. GitHub issue tensorbee#160.
The fixes on this branch change the packaged sources of rdocx-oxml, rdocx, rdocx-layout and rdocx-cli, and the rdocx-cli README, so their crates.io archive rows are measured again. The four rows are dated 2026-09-27 through ARCHIVE_REMEASUREMENT_DATES, as the latest re-measurements on main are. GitHub issue tensorbee#160.
This was referenced Sep 27, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
prepare_staged_output(rdocx
document.rs) used to mark an existing comments model dirty for everypublic output: ZIP, bytes, Flat OPC, package class, signing and encryption.
It now marks it dirty only when the model no longer matches a fresh parse of
its part, the same test that styles, settings and the comments-extended part
already use. Comment mutations still set the flag themselves, as before.
CT_Comments::to_xml(rdocx-oxmlcomments.rs) now writes the retained rootattributes in source order. It keeps a retained
xmlns:worxmlns:w14inplace, bound to the namespace the serializer writes. It adds the fixed
declarations first only when the source lacks them, so a part rdocx authored
serializes as before.
mc:Ignorableon rewritten document, header and footer roots.CT_DocumentandCT_HdrFtrnow keep the non-namespace attributes of theirroot in source order and write them after the namespace declarations.
CT_HdrFtrkeeps the new field private.mc:Ignorableprefixes inrdocx validate. The newrdocx-oxml function lists each prefix that an
mc:Ignorableormc:MustUnderstandattribute names without a declaration in scope.validateruns it over every XML part, in part name order, and reports eachfinding as an error. The rdocx-cli README says what
validatechecks.rdocx-oxml,rdocx,rdocx-layoutand
rdocx-cli, dated 2026-09-27.Part of #160. This PR covers the parts of section 3 that concern
serialization and validation.
Still open for other PRs: sections 1, 2, 4 and the matrix request of #160,
160-3d (compare story shells semantically, needed once a comments part really
changes), the serializer fidelity of rewritten parts (indentation,
w:val="0", redundantxmlns:w), and the same root retention forstyles.xml, which dropsmc:Ignorableand its declarations when a styleis added (see Notes).
Why
The issue's section 3 script printed this on main:
Three defects combine here. First, F-255 made every public output mark the
comments model dirty, so a no-op save always wrote the comments part again
through the typed model. An empty self-closed root came back as an open and
close pair, and Word's comment attribute order (
w:idfirst) came back inrdocx's order.
compare()reads the original comments from the package bytes,so its story skeleton check then refused a document against its own save, no-op
or edited. This hits every file with a comments part, including Word files with
real comments, not only the empty part of the #158 report fixture (which is an
open and close pair).
Second, the rewritten comments root dropped the retained
xmlns:w14, and wrotexmlns:w14again only when some comment paragraph had aw14:paraId. It keptmc:Ignorable="w14 w15", so the part named an undeclared prefix, which MarkupCompatibility (ECMA-376 Part 3) forbids. That still matters after the first fix
whenever the comments model really changes, for example when the last comment
is removed.
Third,
CT_DocumentandCT_HdrFtrcaptured only the namespace declarations oftheir root. A single
try_replace_textrewritesdocument.xml, and the resultlost
mc:Ignorablewhile everyw14:paraIdstayed in the body (the mirror casenoted in PR #154). A replacement in a header or footer did the same to that
part.
With this branch, the same script prints:
Notes
undeclared
mc:Ignorableprefix is an error invalidate, not a warning,since the part is not conformant and a consumer may reject it.
validatealso checksmc:MustUnderstand, which is the same kind of prefixlist with the same rule. A part whose content type does not end in
xml, orthat does not parse as XML, is outside this check. I did not add a
well-formedness error, which would be a separate verdict. I ran it over the
363
.docxfiles I had locally: python-docx outputs, the Production readiness for editing real docx and pptx files: an acceptance contract, two realistic fixtures and two matrices #158 fixtures, therdocx samples, and rdocx outputs of all of them. It flags exactly the 12
files that main's save wrote from the report fixture (the comments part with
w14dropped), and nothing else.document.xml, header and footer roots keep the order they had on main: thefixed
w,r,mc(document only) andwpdeclarations first, then theproducer declarations in source order, then the non-namespace attributes in
source order. That keeps every prefix
mc:Ignorablelists declared, sincethose rewrites already kept every producer declaration. Writing the fixed
declarations in their source positions too would need a new representation
of the root, and it changes nothing a consumer reads.
to_bytes, Flat OPC, package class, encrypted or signed outputwithout a comment edit keeps the comments part byte for byte. Before, it
was always written in rdocx's canonical form (fixed
w:prefix, rdocxattribute order, XML declaration). A comment edit still writes the
canonical form, to the part's own relationship target.
package signature invalid.
document.xml, headers and footers are nowdecoded when parsed. A malformed value (an undefined entity, say) now
fails the parse. On
document.xmla malformed namespace value alreadyfailed it. Header and footer namespace values are still read raw.
CT_Styles::to_xmlwrites onlyxmlns:wandxmlns:r. So adding a style to a Word document dropsmc:Ignorableand every producer declaration fromstyles.xml, and aretained raw child such as
<w14:ligatures w14:val="standard"/>in a style'srun properties is left with an unbound
w14prefix. That part is then notnamespace-well-formed. The fix is the same kind of root retention. It needs
a new field on
CT_Styles, whose fields are all public, so it is anotherbreak of the same kind as the one above, for a part Producer traits: a matrix over every operation, and what still fails in it and around it #160 does not report.
I left it for a separate PR.
say),
compare()still compares the story skeleton and the owner starttags byte for byte.
w14:paraIdvalues. The ones it loses come from the identity attributes ofrewritten table rows and similar owners (Identity attributes (
w:rsid*,w14:paraId, content-controlw:id/w:tag) still breaktoc rebuild,compare()and editing: a matrix to close the class #159 section 3).are still indented, still rewrite a source
w:val="0"as"false"and still write the redundant local
xmlns:w.document.rschanges onlyprepare_staged_output,right below the save paths that Save documents and presentations atomically, keeping links and modes #178 (atomic save) edits. rdocx-cli
commands.rschanges onlyvalidate, which End the CLIs cleanly on a closed pipe and refuse silent overwrites #174 (CLI overwrite policy andclosed pipe) also touches for its output. The new CLI test sits right after
validate_exit_status_is_a_verdict. The regression tests are a namedmod producer_part_roots_survive_saveplaced right afterno_op_save_preserves_every_unchanged_part. The archive rows ofrdocx,rdocx-oxmlandrdocx-cliare re-measured by other PRs of this batch too,so they need one fresh measurement when the PRs are integrated.
samples' roots carry no producer attributes.
Tests
Added:
regression_test.rs,mod producer_part_roots_survive_save:empty_comments_part_keeps_its_bytes_and_compares_against_its_own_save:the issue's reproduction with its self-closed root, the fixture's open and
close pair, and the fixture's unused default namespace. A no-op save
changes no part, and compare against the no-op save gives no revision. A
one-word edit keeps
comments.xmlbyte for byte, and compare against itgives a deletion and an insertion.
word_comments_keep_their_bytes_through_a_body_edit: the same checks witha Word-style comment (
w:idfirst,w14:paraIdon its paragraph) and itsanchors in the body.
rewritten_comments_root_keeps_every_declaration_in_source_order:removing the last Word comment rewrites the part, which must keep the
source root start tag and declare
w14andw15.edited_main_document_keeps_mc_ignorable_while_its_body_uses_w14: after aone-word edit,
document.xmlkeepsmc:Ignorable="w14 w15"with bothprefixes declared, the untouched paragraph keeps its
w14:paraId, comparegives a deletion and an insertion, and a second edit keeps the same root.
rewritten_header_and_footer_keep_mc_ignorable_and_its_declarations: areplacement that hits a header and a footer keeps
mc:Ignorableand itsdeclarations on both roots.
integration_test.rs,comments_part_uses_its_existing_relationship_target(F-255): the output without a comment edit keeps the custom-target part byte
for byte, leaves the signature valid, and the Flat OPC keeps
<x:comments.The canonical
w:root, the raw children, the custom target, the signatureinvalidation and the ZIP and Flat OPC agreement are now checked after
add_comment, which is when the model is written.comments::tests::rewritten_root_keeps_its_declarations_in_source_order,document::tests::root_attributes_survive_a_rewrite_after_the_namespace_declarations,header_footer::tests::root_attributes_survive_a_rewrite_after_the_namespace_declarationsand
namespace::tests::compatibility_prefixes_must_be_declared_in_scope.tests/integration.rs:validate_reports_an_undeclared_ignorable_prefix,on the comments part that main's save wrote from the report fixture.
On main, the five regression tests and the CLI test fail, and so does the
comments unit test against the old serializer. The document and header unit
tests read the new field, so they do not compile on main.
End to end, with a debug build of this branch:
matrix_producer_traits.pyfrom Production readiness for editing real docx and pptx files: an acceptance contract, two realistic fixtures and two matrices #158: the rowempty comments partnowpasses
cmpfldandself(both FAIL on main). The other rows areunchanged. Their failing cells belong to other PRs.
fixture-report.docx: a no-oprdocx replacechanges no part, and aone-word replacement changes only
document.xml, which keepsmc:Ignorablewith all eight prefixes declared.rdocx compareof thefixture against both outputs succeeds with no diagnostic, and
rdocx validatereports no markup compatibility error on the fixture or oneither output.
Run on macOS arm64, debug build, at the head of the branch:
cargo fmt --all --checkandcargo clippy -p rdocx-oxml -p rdocx -p rdocx-layout -p rdocx-cli --all-targets --all-features -- -D warnings:clean. Each of the four fix commits was also checked on its own with clippy
and its tests.
cargo test -p rdocx-oxml: 547 passed, plus 1 doctest.cargo test -p rdocx --no-fail-fast: regression 566 passed, doctests 2passed. Lib and integration have only the environment failures listed
below.
cargo test -p rdocx-layout: 292 passed, plus 1 doctest.cargo test -p rdocx-cli: 2 unit and 18 integration tests passed.cargo check -p rdocx-py -p rdocx-wasm --all-targetsandcargo check --target wasm32-unknown-unknown -p rdocx-wasm: clean.RUSTDOCFLAGS="-D warnings" cargo doc -p rdocx-oxml -p rdocx --no-deps --all-features: clean.python3 scripts/hash_harness.py --check: 49 entries match.python3 scripts/prose_check.py: 0 violations, also on every commitmessage.
python3 scripts/readme_doctests.py: passes with the re-recorded rows.binding was built only to run the issue's scripts.
Environment failures seen here, all on the known list:
large_word_and_presentation_pdfs_preserve_logical_reading_orderand
word_and_powerpoint_chart_pixels_are_identical(pinned Poppler).odt_reader_matches_pinned_libreoffice_structure,public_authored_theme_and_fonts_match_pinned_word_resolution,section_page_semantics_match_pinned_libreoffice_render,every_conditional_table_region_matches_word, andsanitized_public_authoring_fixture_passes_every_conformance_stage, whichneeds an offline
cargo run.