When accepting a tracked change (an OOXML revision marker that records an edit inside a Word file), the final document must keep the accepted content and remove the rejected content. A move (a tracked change that relocates existing content) records both sides of that relocation. In the official OOXML specification (ECMA-376 WordprocessingML)[1], <w:moveFrom> is the element that wraps the source content and <w:moveTo> is the element that wraps the destination content. The two wrappers carry a shared w:id integer that pairs them as one logical relocation; the specific integer value is arbitrary, chosen by the document author.
In this repo, safe-docx, a function named acceptChanges[2] takes parsed OOXML content and rewrites it so accepted revision markup no longer remains. The function also returns a summary with counts for accepted insertions, accepted deletions, resolved moves, and resolved property changes. For move content, accepting the change removes <w:moveFrom> with its source content and unwraps <w:moveTo> so the destination content remains as ordinary document content.
Below is a test scenario of the paired-move behavior of acceptChanges[3]: accept moves by keeping destination and removing source.
The scenario
Given a document with move-from and move-to wrappers,
When moves are resolved into destination-only text,
Then two move wrappers are resolved.
- moveFrom wrappers are removed.
- moveTo wrappers are removed.
- old location text is removed.
- new location text is preserved.
Below is the test fixture code.
const input = [
'<w:p>',
'<w:moveFrom w:id="11"><w:r><w:t>Old location</w:t></w:r></w:moveFrom>',
'<w:moveTo w:id="11"><w:r><w:t>New location</w:t></w:r></w:moveTo>',
'</w:p>',
].join('');
const result = runAcceptChanges(input);
The expected result shape
Below is the result that runAcceptChanges is expected to return for this scenario.
{
xml: '<?xml version="1.0" encoding="UTF-8"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>New location</w:t></w:r></w:p></w:body></w:document>',
summary: {
insertionsAccepted: 0,
deletionsAccepted: 0,
movesResolved: 2,
propertyChangesResolved: 0
}
}
Below is a description of the expected fields:
- The
xmlfield is expected to contain onlyNew locationinside the paragraph, because accepting the move drops the source content and keeps the destination content. - The
summary.movesResolvedfield is expected to be2, because one<w:moveFrom>wrapper is removed and one<w:moveTo>wrapper is unwrapped. - The
summary.insertionsAcceptedfield is expected to be0, because the fixture does not contain any<w:ins>elements. - The
summary.deletionsAcceptedfield is expected to be0, because the fixture does not contain any<w:del>elements. - The
summary.propertyChangesResolvedfield is expected to be0, because the fixture does not contain property change records.
What this scenario does not cover
This scenario is deliberately limited to one paired move inside one paragraph. It does not exercise:
- inserted text wrapped in
<w:ins>elements, covered by accept insertions by unwrapping w:ins wrappers, - deleted text wrapped in
<w:del>elements, covered by accept deletions by removing w:del elements and content, - run property change records, covered by accept property changes by removing change records,
- nested insertion and deletion wrappers, covered by bottom-up processing resolves nested revisions,
- move source wrappers without a matching destination wrapper, covered by orphaned moves handled with safe fallback.
The assertions only test the move summary count, the removal of move wrappers, and the presence or absence of the two fixture texts for this paired move.
A non-obvious detail
The shared w:id="11" value makes the fixture a paired move; this scenario checks the accepted output rather than the pair-validation logic. The important output rule is destination-only content: the source text is removed with <w:moveFrom>, while the destination text is kept after <w:moveTo> is unwrapped.