diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/index.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/index.js index da72ff0a59..9076bb723d 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/index.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/index.js @@ -118,6 +118,7 @@ import { translator as w_personalCompose_translator } from './w/personalCompose/ import { translator as w_personalReply_translator } from './w/personalReply/personalReply-translator.js'; import { translator as w_position_translator } from './w/position/position-translator.js'; import { translator as w_pPr_translator } from './w/pPr/pPr-translator.js'; +import { translator as w_pPrChange_translator } from './w/pPrChange/pPrChange-translator.js'; import { translator as w_pStyle_translator } from './w/pStyle/pStyle-translator.js'; import { translator as w_permEnd_translator } from './w/perm-end/perm-end-translator.js'; import { translator as w_permStart_translator } from './w/perm-start/perm-start-translator.js'; @@ -324,6 +325,7 @@ const translatorList = Array.from( w_personalReply_translator, w_position_translator, w_pPr_translator, + w_pPrChange_translator, w_pStyle_translator, w_permStart_translator, w_permEnd_translator, diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/utils.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/utils.js index 0da20bf8d3..ca11beaee8 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/utils.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/utils.js @@ -500,6 +500,8 @@ export function decodePropertiesByKey(xmlName, sdName, translator, params, attrs * @param {import('@translator').NodeTranslator[]} propertyTranslators An array of property translators to handle nested properties. * @param {object} [defaultEncodedAttrs={}] Optional default attributes to include during encoding. * @param {import('@translator').AttrConfig[]} [attributeHandlers=[]] Optional additional attribute handlers for the nested element. + * @param {object} [options={}] Optional configuration. + * @param {boolean} [options.emitWhenAttributesOnly=false] When true, the decode path emits the XML element if it has attributes even when there are no child elements. Useful for tracked-change wrappers (e.g. w:pPrChange) where the attributes carry independent semantic value. * @returns {import('@translator').NodeTranslatorConfig} The nested property handler config with xmlName, sdName, encode, and decode functions. */ export function createNestedPropertiesTranslator( @@ -508,6 +510,7 @@ export function createNestedPropertiesTranslator( propertyTranslators, defaultEncodedAttrs = {}, attributeHandlers = [], + { emitWhenAttributesOnly = false } = {}, ) { const propertyTranslatorsByXmlName = {}; const propertyTranslatorsBySdName = {}; @@ -542,7 +545,9 @@ export function createNestedPropertiesTranslator( // Process property translators const elements = decodeProperties(params, propertyTranslatorsBySdName, currentValue); - if (elements.length === 0) { + const hasAttributes = emitWhenAttributesOnly && Object.keys(decodedAttrs).length > 0; + + if (elements.length === 0 && !hasAttributes) { return undefined; } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPr/pPr-base-translators.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPr/pPr-base-translators.js new file mode 100644 index 0000000000..6da4587d16 --- /dev/null +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPr/pPr-base-translators.js @@ -0,0 +1,75 @@ +// @ts-check +import { translator as mcAlternateContentTranslator } from '../../mc/altermateContent'; +import { translator as wAdjustRightIndTranslator } from '../adjustRightInd'; +import { translator as wAutoSpaceDETranslator } from '../autoSpaceDE'; +import { translator as wAutoSpaceDNTranslator } from '../autoSpaceDN'; +import { translator as wBidiTranslator } from '../bidi'; +import { translator as wCnfStyleTranslator } from '../cnfStyle'; +import { translator as wContextualSpacingTranslator } from '../contextualSpacing'; +import { translator as wDivIdTranslator } from '../divId'; +import { translator as wFramePrTranslator } from '../framePr'; +import { translator as wIndTranslator } from '../ind'; +import { translator as wJcTranslatorTranslator } from '../jc'; +import { translator as wKeepLinesTranslator } from '../keepLines'; +import { translator as wKeepNextTranslator } from '../keepNext'; +import { translator as wKinsokuTranslator } from '../kinsoku'; +import { translator as wMirrorIndentsTranslator } from '../mirrorIndents'; +import { translator as wNumPrTranslator } from '../numPr'; +import { translator as wOutlineLvlTranslator } from '../outlineLvl'; +import { translator as wOverflowPunctTranslator } from '../overflowPunct'; +import { translator as wPBdrTranslator } from '../pBdr'; +import { translator as wPStyleTranslator } from '../pStyle'; +import { translator as wPageBreakBeforeTranslator } from '../pageBreakBefore'; +import { translator as wShdTranslator } from '../shd'; +import { translator as wSnapToGridTranslator } from '../snapToGrid'; +import { translator as wSpacingTranslator } from '../spacing'; +import { translator as wSuppressAutoHyphensTranslator } from '../suppressAutoHyphens'; +import { translator as wSuppressLineNumbersTranslator } from '../suppressLineNumbers'; +import { translator as wSuppressOverlapTranslator } from '../suppressOverlap'; +import { translator as wTabsTranslator } from '../tabs'; +import { translator as wTextAlignmentTranslator } from '../textAlignment'; +import { translator as wTextDirectionTranslator } from '../textDirection'; +import { translator as wTextboxTightWrapTranslator } from '../textboxTightWrap'; +import { translator as wTopLinePunctTranslator } from '../topLinePunct'; +import { translator as wWidowControlTranslator } from '../widowControl'; +import { translator as wWordWrapTranslator } from '../wordWrap'; +import { translator as wRPrTranslator } from '../rpr'; + +/** @type {import('@translator').NodeTranslator[]} */ +export const basePropertyTranslators = [ + mcAlternateContentTranslator, + wAdjustRightIndTranslator, + wAutoSpaceDETranslator, + wAutoSpaceDNTranslator, + wBidiTranslator, + wCnfStyleTranslator, + wContextualSpacingTranslator, + wDivIdTranslator, + wFramePrTranslator, + wIndTranslator, + wJcTranslatorTranslator, + wKeepLinesTranslator, + wKeepNextTranslator, + wKinsokuTranslator, + wMirrorIndentsTranslator, + wNumPrTranslator, + wOutlineLvlTranslator, + wOverflowPunctTranslator, + wPBdrTranslator, + wPStyleTranslator, + wPageBreakBeforeTranslator, + wShdTranslator, + wSnapToGridTranslator, + wSpacingTranslator, + wSuppressAutoHyphensTranslator, + wSuppressLineNumbersTranslator, + wSuppressOverlapTranslator, + wTabsTranslator, + wTextAlignmentTranslator, + wTextDirectionTranslator, + wTextboxTightWrapTranslator, + wTopLinePunctTranslator, + wWidowControlTranslator, + wWordWrapTranslator, + wRPrTranslator, +]; diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPr/pPr-translator.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPr/pPr-translator.js index 212fe10601..d1d87a92fa 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPr/pPr-translator.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPr/pPr-translator.js @@ -1,82 +1,11 @@ // @ts-check import { NodeTranslator } from '@translator'; import { createNestedPropertiesTranslator } from '@converter/v3/handlers/utils.js'; -import { translator as mcAlternateContentTranslator } from '../../mc/altermateContent'; -import { translator as wAdjustRightIndTranslator } from '../adjustRightInd'; -import { translator as wAutoSpaceDETranslator } from '../autoSpaceDE'; -import { translator as wAutoSpaceDNTranslator } from '../autoSpaceDN'; -import { translator as wBidiTranslator } from '../bidi'; -import { translator as wCnfStyleTranslator } from '../cnfStyle'; -import { translator as wContextualSpacingTranslator } from '../contextualSpacing'; -import { translator as wDivIdTranslator } from '../divId'; -import { translator as wFramePrTranslator } from '../framePr'; -import { translator as wIndTranslator } from '../ind'; -import { translator as wJcTranslatorTranslator } from '../jc'; -import { translator as wKeepLinesTranslator } from '../keepLines'; -import { translator as wKeepNextTranslator } from '../keepNext'; -import { translator as wKinsokuTranslator } from '../kinsoku'; -import { translator as wMirrorIndentsTranslator } from '../mirrorIndents'; -import { translator as wNumPrTranslator } from '../numPr'; -import { translator as wOutlineLvlTranslator } from '../outlineLvl'; -import { translator as wOverflowPunctTranslator } from '../overflowPunct'; -import { translator as wPBdrTranslator } from '../pBdr'; -import { translator as wPStyleTranslator } from '../pStyle'; -import { translator as wPageBreakBeforeTranslator } from '../pageBreakBefore'; -import { translator as wShdTranslator } from '../shd'; -import { translator as wSnapToGridTranslator } from '../snapToGrid'; -import { translator as wSpacingTranslator } from '../spacing'; -import { translator as wSuppressAutoHyphensTranslator } from '../suppressAutoHyphens'; -import { translator as wSuppressLineNumbersTranslator } from '../suppressLineNumbers'; -import { translator as wSuppressOverlapTranslator } from '../suppressOverlap'; -import { translator as wTabsTranslator } from '../tabs'; -import { translator as wTextAlignmentTranslator } from '../textAlignment'; -import { translator as wTextDirectionTranslator } from '../textDirection'; -import { translator as wTextboxTightWrapTranslator } from '../textboxTightWrap'; -import { translator as wTopLinePunctTranslator } from '../topLinePunct'; -import { translator as wWidowControlTranslator } from '../widowControl'; -import { translator as wWordWrapTranslator } from '../wordWrap'; -import { translator as wRPrTranslator } from '../rpr'; +import { basePropertyTranslators } from './pPr-base-translators.js'; +import { translator as wPPrChangeTranslator } from '../pPrChange'; -// Property translators for w:pPr child elements -// Each translator handles a specific property of the paragraph properties /** @type {import('@translator').NodeTranslator[]} */ -const propertyTranslators = [ - mcAlternateContentTranslator, - wAdjustRightIndTranslator, - wAutoSpaceDETranslator, - wAutoSpaceDNTranslator, - wBidiTranslator, - wCnfStyleTranslator, - wContextualSpacingTranslator, - wDivIdTranslator, - wFramePrTranslator, - wIndTranslator, - wJcTranslatorTranslator, - wKeepLinesTranslator, - wKeepNextTranslator, - wKinsokuTranslator, - wMirrorIndentsTranslator, - wNumPrTranslator, - wOutlineLvlTranslator, - wOverflowPunctTranslator, - wPBdrTranslator, - wPStyleTranslator, - wPageBreakBeforeTranslator, - wShdTranslator, - wSnapToGridTranslator, - wSpacingTranslator, - wSuppressAutoHyphensTranslator, - wSuppressLineNumbersTranslator, - wSuppressOverlapTranslator, - wTabsTranslator, - wTextAlignmentTranslator, - wTextDirectionTranslator, - wTextboxTightWrapTranslator, - wTopLinePunctTranslator, - wWidowControlTranslator, - wWordWrapTranslator, - wRPrTranslator, -]; +const propertyTranslators = [...basePropertyTranslators, wPPrChangeTranslator]; /** * The NodeTranslator instance for the w:pPr element. diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/index.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/index.js new file mode 100644 index 0000000000..5bfa4e4b24 --- /dev/null +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/index.js @@ -0,0 +1 @@ +export * from './pPrChange-translator.js'; diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/pPrChange-translator.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/pPrChange-translator.js new file mode 100644 index 0000000000..3e91f9ad90 --- /dev/null +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/pPrChange-translator.js @@ -0,0 +1,99 @@ +import { NodeTranslator } from '@translator'; +import { carbonCopy } from '@core/utilities/carbonCopy.js'; +import { createNestedPropertiesTranslator, createAttributeHandler } from '@converter/v3/handlers/utils.js'; +import { basePropertyTranslators } from '../pPr/pPr-base-translators.js'; + +const pPrTranslator = NodeTranslator.from( + createNestedPropertiesTranslator('w:pPr', 'paragraphProperties', basePropertyTranslators), +); + +const ATTRIBUTE_HANDLERS = [ + createAttributeHandler('w:id'), + createAttributeHandler('w:author'), + createAttributeHandler('w:date'), +]; + +function getSectPr(pPrNode) { + const sectPr = pPrNode?.elements?.find((el) => el.name === 'w:sectPr'); + return sectPr ? carbonCopy(sectPr) : undefined; +} + +/** + * The NodeTranslator instance for the w:pPrChange element. + * @type {import('@translator').NodeTranslator} + */ +export const translator = NodeTranslator.from({ + xmlName: 'w:pPrChange', + sdNodeOrKeyName: 'change', + type: NodeTranslator.translatorTypes.NODE, + attributes: ATTRIBUTE_HANDLERS, + encode: (params, encodedAttrs = {}) => { + const changeNode = params.nodes[0]; + const pPrNode = changeNode?.elements?.find((el) => el.name === 'w:pPr'); + + let paragraphProperties = pPrNode ? pPrTranslator.encode({ ...params, nodes: [pPrNode] }) : undefined; + const sectPr = getSectPr(pPrNode); + if (sectPr) { + paragraphProperties = { + ...(paragraphProperties || {}), + sectPr, + }; + } + + const result = { + ...encodedAttrs, + ...(paragraphProperties ? { paragraphProperties } : {}), + }; + + return Object.keys(result).length ? result : undefined; + }, + decode: function (params) { + const change = params.node?.attrs?.change; + if (!change || typeof change !== 'object') return undefined; + + const decodedAttrs = this.decodeAttributes({ + node: { ...params.node, attrs: change }, + }); + const hasParagraphProperties = Object.prototype.hasOwnProperty.call(change, 'paragraphProperties'); + const paragraphProperties = hasParagraphProperties ? change.paragraphProperties : undefined; + + let pPrNode = + paragraphProperties && typeof paragraphProperties === 'object' + ? pPrTranslator.decode({ + ...params, + node: { ...params.node, attrs: { paragraphProperties } }, + }) + : undefined; + + const sectPr = paragraphProperties?.sectPr ? carbonCopy(paragraphProperties.sectPr) : undefined; + if (sectPr) { + if (!pPrNode) { + pPrNode = { + name: 'w:pPr', + type: 'element', + attributes: {}, + elements: [], + }; + } + pPrNode.elements = [...(pPrNode.elements || []), sectPr]; + } + + if (!pPrNode && hasParagraphProperties) { + pPrNode = { + name: 'w:pPr', + type: 'element', + attributes: {}, + elements: [], + }; + } + + if (!pPrNode && !Object.keys(decodedAttrs).length) return undefined; + + return { + name: 'w:pPrChange', + type: 'element', + attributes: decodedAttrs, + elements: pPrNode ? [pPrNode] : [], + }; + }, +}); diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/pPrChange-translator.test.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/pPrChange-translator.test.js new file mode 100644 index 0000000000..472e8403e8 --- /dev/null +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPrChange/pPrChange-translator.test.js @@ -0,0 +1,368 @@ +vi.mock('../../../../exporter.js', () => { + const processOutputMarks = vi.fn((marks) => marks || []); + const generateRunProps = vi.fn((processedMarks) => ({ + name: 'w:rPr', + elements: [], + })); + return { processOutputMarks, generateRunProps }; +}); + +import { describe, it, expect } from 'vitest'; +import { translator } from './pPrChange-translator.js'; +import { NodeTranslator } from '@translator'; + +describe('w:pPrChange translator', () => { + describe('config', () => { + it('should have correct properties', () => { + expect(translator.xmlName).toBe('w:pPrChange'); + expect(translator.sdNodeOrKeyName).toBe('change'); + expect(translator).toBeInstanceOf(NodeTranslator); + }); + }); + + describe('encode', () => { + it('should encode a w:pPrChange element with attributes and nested w:pPr', () => { + const xmlNode = { + name: 'w:pPrChange', + attributes: { + 'w:id': '0', + 'w:author': 'Luccas Correa', + 'w:date': '2026-04-02T11:25:00Z', + }, + elements: [ + { + name: 'w:pPr', + elements: [ + { name: 'w:pStyle', attributes: { 'w:val': 'ListParagraph' } }, + { + name: 'w:numPr', + elements: [{ name: 'w:numId', attributes: { 'w:val': '1' } }], + }, + { name: 'w:ind', attributes: { 'w:hanging': '360' } }, + ], + }, + ], + }; + + const result = translator.encode({ nodes: [xmlNode] }); + + expect(result).toEqual({ + id: '0', + author: 'Luccas Correa', + date: '2026-04-02T11:25:00Z', + paragraphProperties: { + styleId: 'ListParagraph', + numberingProperties: { numId: 1 }, + indent: { hanging: 360 }, + }, + }); + }); + + it('should encode a w:pPrChange with only attributes and empty w:pPr', () => { + const xmlNode = { + name: 'w:pPrChange', + attributes: { + 'w:id': '5', + 'w:author': 'Test Author', + 'w:date': '2026-01-01T00:00:00Z', + }, + elements: [ + { + name: 'w:pPr', + elements: [], + }, + ], + }; + + const result = translator.encode({ nodes: [xmlNode] }); + + expect(result).toEqual({ + id: '5', + author: 'Test Author', + date: '2026-01-01T00:00:00Z', + }); + }); + + it('should encode nested sectPr from the changed paragraph properties', () => { + const sectPr = { + name: 'w:sectPr', + elements: [{ name: 'w:type', attributes: { 'w:val': 'nextPage' } }], + }; + const xmlNode = { + name: 'w:pPrChange', + attributes: { + 'w:id': '6', + 'w:author': 'Section Author', + 'w:date': '2026-01-02T00:00:00Z', + }, + elements: [ + { + name: 'w:pPr', + elements: [sectPr], + }, + ], + }; + + const result = translator.encode({ nodes: [xmlNode] }); + + expect(result).toEqual({ + id: '6', + author: 'Section Author', + date: '2026-01-02T00:00:00Z', + paragraphProperties: { + sectPr, + }, + }); + }); + + it('should encode a w:pPrChange with only attributes and no children', () => { + const xmlNode = { + name: 'w:pPrChange', + attributes: { + 'w:id': '3', + 'w:author': 'Author', + 'w:date': '2026-01-01T00:00:00Z', + }, + elements: [], + }; + + const result = translator.encode({ nodes: [xmlNode] }); + + expect(result).toEqual({ + id: '3', + author: 'Author', + date: '2026-01-01T00:00:00Z', + }); + }); + + it('should return undefined if no attributes or children are present', () => { + const xmlNode = { + name: 'w:pPrChange', + attributes: {}, + elements: [], + }; + + const result = translator.encode({ nodes: [xmlNode] }); + + expect(result).toBeUndefined(); + }); + }); + + describe('decode', () => { + it('should decode a change object with attributes and nested paragraphProperties', () => { + const superDocNode = { + attrs: { + change: { + id: '0', + author: 'Luccas Correa', + date: '2026-04-02T11:25:00Z', + paragraphProperties: { + styleId: 'ListParagraph', + numberingProperties: { numId: 1 }, + indent: { hanging: 360 }, + }, + }, + }, + }; + + const result = translator.decode({ node: superDocNode }); + + expect(result.name).toBe('w:pPrChange'); + expect(result.attributes).toEqual({ + 'w:id': '0', + 'w:author': 'Luccas Correa', + 'w:date': '2026-04-02T11:25:00Z', + }); + expect(result.elements).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'w:pPr', + elements: expect.arrayContaining([ + { name: 'w:pStyle', attributes: { 'w:val': 'ListParagraph' } }, + expect.objectContaining({ + name: 'w:numPr', + elements: [{ name: 'w:numId', attributes: { 'w:val': '1' } }], + }), + { name: 'w:ind', attributes: { 'w:hanging': '360' } }, + ]), + }), + ]), + ); + }); + + it('should decode a change object with only attributes', () => { + const superDocNode = { + attrs: { + change: { + id: '5', + author: 'Test Author', + date: '2026-01-01T00:00:00Z', + }, + }, + }; + + const result = translator.decode({ node: superDocNode }); + + expect(result).toEqual({ + name: 'w:pPrChange', + type: 'element', + attributes: { + 'w:id': '5', + 'w:author': 'Test Author', + 'w:date': '2026-01-01T00:00:00Z', + }, + elements: [], + }); + }); + + it('should return undefined if change is empty', () => { + const superDocNode = { + attrs: { + change: {}, + }, + }; + + const result = translator.decode({ node: superDocNode }); + + expect(result).toBeUndefined(); + }); + + it('should decode a change with an explicit empty paragraphProperties object', () => { + const superDocNode = { + attrs: { + change: { + id: '8', + author: 'Empty Paragraph Props', + date: '2026-01-03T00:00:00Z', + paragraphProperties: {}, + }, + }, + }; + + const result = translator.decode({ node: superDocNode }); + + expect(result).toEqual({ + name: 'w:pPrChange', + type: 'element', + attributes: { + 'w:id': '8', + 'w:author': 'Empty Paragraph Props', + 'w:date': '2026-01-03T00:00:00Z', + }, + elements: [ + { + name: 'w:pPr', + type: 'element', + attributes: {}, + elements: [], + }, + ], + }); + }); + + it('should decode a change with sectPr-only paragraph properties', () => { + const sectPr = { + name: 'w:sectPr', + elements: [{ name: 'w:type', attributes: { 'w:val': 'nextPage' } }], + }; + const superDocNode = { + attrs: { + change: { + id: '7', + author: 'Section Author', + date: '2026-01-02T00:00:00Z', + paragraphProperties: { + sectPr, + }, + }, + }, + }; + + const result = translator.decode({ node: superDocNode }); + + expect(result).toEqual({ + name: 'w:pPrChange', + type: 'element', + attributes: { + 'w:id': '7', + 'w:author': 'Section Author', + 'w:date': '2026-01-02T00:00:00Z', + }, + elements: [ + { + name: 'w:pPr', + type: 'element', + attributes: {}, + elements: [sectPr], + }, + ], + }); + }); + + it('should return undefined if change is missing', () => { + const superDocNode = { + attrs: {}, + }; + + const result = translator.decode({ node: superDocNode }); + + expect(result).toBeUndefined(); + }); + }); + + describe('round-trip', () => { + it('maintains consistency for a pPrChange with nested properties', () => { + const initialChange = { + id: '0', + author: 'Luccas Correa', + date: '2026-04-02T11:25:00Z', + paragraphProperties: { + styleId: 'ListParagraph', + numberingProperties: { numId: 1 }, + indent: { hanging: 360 }, + }, + }; + + const decoded = translator.decode({ node: { attrs: { change: initialChange } } }); + const encoded = translator.encode({ nodes: [decoded] }); + + expect(encoded).toEqual(initialChange); + }); + + it('maintains consistency for a pPrChange with justification', () => { + const initialChange = { + id: '2', + author: 'Another Author', + date: '2026-03-15T10:00:00Z', + paragraphProperties: { + justification: 'center', + spacing: { before: 200, after: 100 }, + }, + }; + + const decoded = translator.decode({ node: { attrs: { change: initialChange } } }); + const encoded = translator.encode({ nodes: [decoded] }); + + expect(encoded).toEqual(initialChange); + }); + + it('maintains consistency for a pPrChange with sectPr-only paragraph properties', () => { + const initialChange = { + id: '9', + author: 'Section Round Trip', + date: '2026-01-04T00:00:00Z', + paragraphProperties: { + sectPr: { + name: 'w:sectPr', + elements: [{ name: 'w:type', attributes: { 'w:val': 'nextPage' } }], + }, + }, + }; + + const decoded = translator.decode({ node: { attrs: { change: initialChange } } }); + const encoded = translator.encode({ nodes: [decoded] }); + + expect(encoded).toEqual(initialChange); + }); + }); +});