Skip to content

feat: add w:pPrChange translator for paragraph property tracked changes (SD-2417)#2705

Open
luccas-harbour wants to merge 1 commit intomainfrom
luccas/sd-2417-investigation-structural-tracked-changes-dropped-on-import
Open

feat: add w:pPrChange translator for paragraph property tracked changes (SD-2417)#2705
luccas-harbour wants to merge 1 commit intomainfrom
luccas/sd-2417-investigation-structural-tracked-changes-dropped-on-import

Conversation

@luccas-harbour
Copy link
Copy Markdown
Contributor

Summary

  • Add a new w:pPrChange translator that encodes/decodes tracked changes on paragraph properties (author, date, id) along with the nested w:pPr containing the previous properties
  • Register the translator as a child of w:pPr and in the global handler index
  • Extract base pPr property translators into a shared module to avoid a circular dependency between pPrpPrChange

@linear
Copy link
Copy Markdown

linear bot commented Apr 2, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Status: PASS

The w:pPrChange translator looks correct. All three attributes (w:id, w:author, w:date) match the spec exactly — correct names, correct types (w:id as integer, w:date as ST_DateTime string). The single child element w:pPr is the only valid child, and the translator handles that correctly.

One minor note: w:id is required by the spec (the document is non-conformant if omitted). The current decoder has an interesting behavior — if a change object has only { id, author, date } with no paragraphProperties, the decode returns undefined (as shown in the test at line ~145). That means a pPrChange with no prior paragraph properties (empty <w:pPr/>) won't round-trip cleanly. The spec explicitly allows an empty <w:pPr/> to mean "no prior properties existed." Whether that matters depends on whether this code path is exercised in practice, but it's worth noting for future work.

See https://ooxml.dev/spec?q=pPrChange for full spec details.

@luccas-harbour luccas-harbour force-pushed the luccas/sd-2417-investigation-structural-tracked-changes-dropped-on-import branch from 33e5e5c to 3e946ec Compare April 2, 2026 16:15
@luccas-harbour luccas-harbour self-assigned this Apr 2, 2026
@luccas-harbour luccas-harbour marked this pull request as ready for review April 2, 2026 16:16
@luccas-harbour
Copy link
Copy Markdown
Contributor Author

The current decoder has an interesting behavior — if a change object has only { id, author, date } with no paragraphProperties, the decode returns undefined (as shown in the test at line ~145). That means a pPrChange with no prior paragraph properties (empty <w:pPr/>) won't round-trip cleanly. The spec explicitly allows an empty <w:pPr/> to mean "no prior properties existed." Whether that matters depends on whether this code path is exercised in practice, but it's worth noting for future work.

This issue was fixed.

Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

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

@luccas-harbour clean structure — sharing the base translators between pPr and pPrChange is a nice call.

one issue: when the original <w:pPr/> inside <w:pPrChange> is empty, it gets lost on round-trip. the decode guard you added works, but the encode side never feeds it — the key gets dropped for empty elements. tested with a Word file — the empty <w:pPr/> disappears after export.

pPrChange-empty-pPr.docx

also a minor DX thing and a pre-existing ordering note — details in inline comments.

tests look good. left a few comments.

const changeNode = params.nodes[0];
const pPrNode = changeNode?.elements?.find((el) => el.name === 'w:pPr');

let paragraphProperties = pPrNode ? pPrTranslator.encode({ ...params, nodes: [pPrNode] }) : undefined;
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.

when <w:pPr/> exists but is empty, pPrTranslator.encode() returns undefined here — so the paragraphProperties key never makes it into the result. later, when decoding back to XML, the guard at line 81 (hasParagraphProperties) can't fire because the key is missing. the empty <w:pPr/> is lost.

the spec says <w:pPr> is required inside <w:pPrChange> — even when empty it means "there were no previous properties." tested with a real Word file and confirmed the element disappears after export.

Suggested change
let paragraphProperties = pPrNode ? pPrTranslator.encode({ ...params, nodes: [pPrNode] }) : undefined;
let paragraphProperties = pPrNode ? (pPrTranslator.encode({ ...params, nodes: [pPrNode] }) ?? {}) : undefined;

wWordWrapTranslator,
wRPrTranslator,
];
const propertyTranslators = [...basePropertyTranslators, wPPrChangeTranslator];
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 blocking, just a heads up: the spec expects sectPr before pPrChange inside <w:pPr>, but the exporter always puts sectPr last. so if a paragraph has both a section break and a tracked property change, they'll be in the wrong order. this predates your PR — the pattern was already there, it just didn't matter until pPrChange existed. rare in practice. maybe a follow-up ticket?

attributeHandlers = [],
{ emitWhenAttributesOnly = false } = {},
) {
const propertyTranslatorsByXmlName = {};
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.

emitWhenAttributesOnly isn't used anywhere — the pPrChange translator handles this case with its own logic instead. worth removing so it doesn't sit here unused?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants