diff --git a/coverage/emitted-schema-known-failures.json b/coverage/emitted-schema-known-failures.json index 02007b7..ee3e96a 100644 --- a/coverage/emitted-schema-known-failures.json +++ b/coverage/emitted-schema-known-failures.json @@ -1,10 +1,4 @@ [ - { - "id": "duplicate-rprchange-in-rpr", - "issue": "#451", - "match": "}rPrChange': This element is not expected.", - "reason": "comparison stacks duplicate w:rPrChange children in a single w:rPr; CT_RPr allows at most one" - }, { "id": "duplicate-para-mark-ins", "issue": "#452", diff --git a/packages/docx-core/src/baselines/atomizer/inPlaceModifier-wrappers.ts b/packages/docx-core/src/baselines/atomizer/inPlaceModifier-wrappers.ts index 2c52a28..5c81380 100644 --- a/packages/docx-core/src/baselines/atomizer/inPlaceModifier-wrappers.ts +++ b/packages/docx-core/src/baselines/atomizer/inPlaceModifier-wrappers.ts @@ -421,6 +421,15 @@ export function addFormatChange( run.insertBefore(rPr, run.firstChild); } + // CT_RPr permits at most one w:rPrChange. Comparison can visit multiple + // format-changed atoms from the same split run, so keep the latest snapshot + // instead of stacking invalid siblings. + for (const child of childElements(rPr)) { + if (child.tagName === 'w:rPrChange') { + rPr.removeChild(child); + } + } + // Create rPrChange const id = allocateRevisionId(state); const rPrChange = createEl('w:rPrChange', { diff --git a/packages/docx-core/src/baselines/atomizer/inPlaceModifier.test.ts b/packages/docx-core/src/baselines/atomizer/inPlaceModifier.test.ts index 6ca7fe3..0a8e598 100644 --- a/packages/docx-core/src/baselines/atomizer/inPlaceModifier.test.ts +++ b/packages/docx-core/src/baselines/atomizer/inPlaceModifier.test.ts @@ -1653,6 +1653,33 @@ describe('inPlaceModifier', () => { expect(childElements(innerRPr)).toHaveLength(3); }); }); + + test('replaces an existing rPrChange instead of stacking duplicates', async ({ given, when, then }: AllureBddContext) => { + let r: Element; + let firstOldRPr: Element; + let secondOldRPr: Element; + let state: ReturnType; + + await given('a run whose formatting is visited twice by split format-changed atoms', () => { + r = el('w:r', {}, [el('w:rPr', {}, [el('w:b')]), el('w:t', {}, undefined, 'text')]); + firstOldRPr = el('w:rPr', {}, [el('w:i')]); + secondOldRPr = el('w:rPr', {}, [el('w:color', { 'w:val': 'FF0000' })]); + state = createRevisionIdState(); + }); + + await when('addFormatChange is called twice for the same run', () => { + addFormatChange(r, firstOldRPr, author, dateStr, state); + addFormatChange(r, secondOldRPr, author, dateStr, state); + }); + + await then('the current rPr contains one replacement rPrChange snapshot', () => { + const rPr = childElements(r).find((c) => c.tagName === 'w:rPr')!; + const rPrChanges = childElements(rPr).filter((c) => c.tagName === 'w:rPrChange'); + expect(rPrChanges).toHaveLength(1); + const innerRPr = childElements(rPrChanges[0]!).find((c) => c.tagName === 'w:rPr')!; + expect(childElements(innerRPr).map((c) => c.tagName)).toEqual(['w:color']); + }); + }); }); // ── Branch coverage: preSplitMixedStatusRuns ──────────────────────