Skip to content

fix(tracked-changes): preserve format on export and import#2362

Open
palmer-cl wants to merge 3 commits intomainfrom
colep/sd-2023-bug-tracked-formatting-changes-are-dropped-on-export
Open

fix(tracked-changes): preserve format on export and import#2362
palmer-cl wants to merge 3 commits intomainfrom
colep/sd-2023-bug-tracked-formatting-changes-are-dropped-on-export

Conversation

@palmer-cl
Copy link
Collaborator

  • Preserve formatting changes on export
  • Add tests for multiple types of format preservation

@linear
Copy link

linear bot commented Mar 11, 2026

@github-actions
Copy link
Contributor

Status: FAIL

The core structural changes look correct — wrapping <w:rPrChange> children inside <w:rPr> as required by the spec (§17.13.5.31), and preserving <w:rPrChange> nodes during run property replacement. Two issues:


1. w:authorEmail is not a valid attribute on w:rPrChange

helpers.js:40 and asserted in r-translator.test.js:399:

// helpers.js
'w:authorEmail': trackStyleMark.attrs.authorEmail,

// r-translator.test.js
expect(runPropertyChange?.attributes).toMatchObject({
  'w:authorEmail': 'reviewer@example.com',
  ...
});

Per §17.13.5.31, w:rPrChange accepts exactly three attributes: w:id, w:author, and w:date. There is no w:authorEmail attribute defined on any revision/annotation element in the spec. Word will likely silently ignore this attribute, but it's still non-conformant XML. See https://ooxml.dev/spec?q=rPrChange for the full attribute table.


2. w:sz value type mismatch in test

r-translator.test.js:425:

expect(sizeNode?.attributes?.['w:val']).toBe(24);  // number

But the input fixture in markImporter.test.js:232 (and the OOXML spec) stores it as a string:

{ name: 'w:sz', attributes: { 'w:val': '24' } }

w:sz/@w:val is typed as ST_HpsMeasure (an unsigned integer serialized as a string in XML). If the implementation is emitting the attribute value as a JS number 24 rather than string '24', that's a round-trip fidelity problem — though an XML serializer would likely coerce it either way, the test asserting toBe(24) (strict equality, number) suggests the importer and exporter are inconsistently handling the type. Worth aligning these.

Copy link
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.

@colep approach is clean, no major issues.

two small DX suggestions inline — nothing blocking.

on tests: would be good to add coverage for (1) a text node with only a trackFormat mark and no other formatting, and (2) a run that already has a w:rPrChange surviving through replaceRunProps. both cover branches that would silently break without a test catching it. left inline comments.

if (runPropertiesNode) {
if (!Array.isArray(runPropertiesNode.elements)) runPropertiesNode.elements = [];

const existingChangeKeys = new Set(
Copy link
Contributor

Choose a reason for hiding this comment

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

the dedup logic here can't actually trigger — the template never contains w:rPrChange nodes, so the Set is always empty. could just append directly without the key check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Would the preserved changes ever have duplicates we need to account for? or safe to just put them all in?

Copy link
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.

@palmer-cl pipeline switch and expanded comment look good.

two small things still open — left inline comments. no correctness issues, good to go once those are sorted.

if (runPropertiesNode) {
if (!Array.isArray(runPropertiesNode.elements)) runPropertiesNode.elements = [];

const existingChangeKeys = new Set(
Copy link
Contributor

Choose a reason for hiding this comment

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

to answer your question: no, duplicates can't happen. the template never contains w:rPrChange nodes, and the spec only allows one per w:rPr. safe to just push directly without the Set.

return { type, attrs: mark?.attrs || {} };
};

const toRunPropertyElements = (marks = []) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

the IIFE wrapper doesn't do anything here — a regular block body works the same.

Suggested change
const toRunPropertyElements = (marks = []) =>
const toRunPropertyElements = (marks = []) => {
const normalizedMarks = marks.map((mark) => normalizeMark(mark)).filter(Boolean);
const runProperties = decodeRPrFromMarks(normalizedMarks);
const rPrNode = wRPrNodeTranslator.decode({ node: { attrs: { runProperties } } });
return Array.isArray(rPrNode?.elements) ? rPrNode.elements : [];
};

const rPrNode = wRPrNodeTranslator.decode({ node: { attrs: { runProperties: combinedRunProperties } } });
const hasRunProperties = Array.isArray(rPrNode?.elements) && rPrNode.elements.length > 0;
const hasStyleChange = Boolean(trackedStyleChange);
const resolvedRunProperties =
Copy link
Contributor

Choose a reason for hiding this comment

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

the editor side of tracked formatting has good test coverage (display, reject, revert), but the export/import side — which is what this PR fixes — is thin. the r-translator test always pairs trackFormat with bold, so the branch where tracked formatting is the only thing on the run never runs. also createTrackStyleMark has no direct tests (both paths: passthrough of existing XML nodes and building from marks). worth adding at least the standalone trackFormat case and a direct test for createTrackStyleMark?

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