Skip to content

Commit 21464d8

Browse files
feat: add translator for w:pPrChange
1 parent de2e37e commit 21464d8

File tree

5 files changed

+267
-0
lines changed

5 files changed

+267
-0
lines changed

packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ import { translator as w_personalCompose_translator } from './w/personalCompose/
118118
import { translator as w_personalReply_translator } from './w/personalReply/personalReply-translator.js';
119119
import { translator as w_position_translator } from './w/position/position-translator.js';
120120
import { translator as w_pPr_translator } from './w/pPr/pPr-translator.js';
121+
import { translator as w_pPrChange_translator } from './w/pPrChange/pPrChange-translator.js';
121122
import { translator as w_pStyle_translator } from './w/pStyle/pStyle-translator.js';
122123
import { translator as w_permEnd_translator } from './w/perm-end/perm-end-translator.js';
123124
import { translator as w_permStart_translator } from './w/perm-start/perm-start-translator.js';
@@ -324,6 +325,7 @@ const translatorList = Array.from(
324325
w_personalReply_translator,
325326
w_position_translator,
326327
w_pPr_translator,
328+
w_pPrChange_translator,
327329
w_pStyle_translator,
328330
w_permStart_translator,
329331
w_permEnd_translator,

packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pPr/pPr-translator.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { translator as wTopLinePunctTranslator } from '../topLinePunct';
3636
import { translator as wWidowControlTranslator } from '../widowControl';
3737
import { translator as wWordWrapTranslator } from '../wordWrap';
3838
import { translator as wRPrTranslator } from '../rpr';
39+
import { translator as wPPrChangeTranslator } from '../pPrChange';
3940

4041
// Property translators for w:pPr child elements
4142
// Each translator handles a specific property of the paragraph properties
@@ -76,6 +77,7 @@ const propertyTranslators = [
7677
wWidowControlTranslator,
7778
wWordWrapTranslator,
7879
wRPrTranslator,
80+
wPPrChangeTranslator,
7981
];
8082

8183
/**
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './pPrChange-translator.js';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// @ts-check
2+
import { NodeTranslator } from '@translator';
3+
import {
4+
createNestedPropertiesTranslator,
5+
createIntegerAttributeHandler,
6+
createAttributeHandler,
7+
} from '@converter/v3/handlers/utils.js';
8+
import { translator as wPPrTranslator } from '../pPr';
9+
10+
/** @type {import('@translator').NodeTranslator[]} */
11+
const propertyTranslators = [wPPrTranslator];
12+
13+
/**
14+
* The NodeTranslator instance for the w:pPrChange element.
15+
* @type {import('@translator').NodeTranslator}
16+
*/
17+
export const translator = NodeTranslator.from(
18+
createNestedPropertiesTranslator('w:pPrChange', 'change', propertyTranslators, {}, [
19+
createIntegerAttributeHandler('w:id'),
20+
createAttributeHandler('w:author'),
21+
createAttributeHandler('w:date'),
22+
]),
23+
);
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
vi.mock('../../../../exporter.js', () => {
2+
const processOutputMarks = vi.fn((marks) => marks || []);
3+
const generateRunProps = vi.fn((processedMarks) => ({
4+
name: 'w:rPr',
5+
elements: [],
6+
}));
7+
return { processOutputMarks, generateRunProps };
8+
});
9+
10+
import { describe, it, expect } from 'vitest';
11+
import { translator } from './pPrChange-translator.js';
12+
import { NodeTranslator } from '@translator';
13+
14+
describe('w:pPrChange translator', () => {
15+
describe('config', () => {
16+
it('should have correct properties', () => {
17+
expect(translator.xmlName).toBe('w:pPrChange');
18+
expect(translator.sdNodeOrKeyName).toBe('change');
19+
expect(translator).toBeInstanceOf(NodeTranslator);
20+
});
21+
});
22+
23+
describe('encode', () => {
24+
it('should encode a w:pPrChange element with attributes and nested w:pPr', () => {
25+
const xmlNode = {
26+
name: 'w:pPrChange',
27+
attributes: {
28+
'w:id': '0',
29+
'w:author': 'Luccas Correa',
30+
'w:date': '2026-04-02T11:25:00Z',
31+
},
32+
elements: [
33+
{
34+
name: 'w:pPr',
35+
elements: [
36+
{ name: 'w:pStyle', attributes: { 'w:val': 'ListParagraph' } },
37+
{
38+
name: 'w:numPr',
39+
elements: [{ name: 'w:numId', attributes: { 'w:val': '1' } }],
40+
},
41+
{ name: 'w:ind', attributes: { 'w:hanging': '360' } },
42+
],
43+
},
44+
],
45+
};
46+
47+
const result = translator.encode({ nodes: [xmlNode] });
48+
49+
expect(result).toEqual({
50+
id: 0,
51+
author: 'Luccas Correa',
52+
date: '2026-04-02T11:25:00Z',
53+
paragraphProperties: {
54+
styleId: 'ListParagraph',
55+
numberingProperties: { numId: 1 },
56+
indent: { hanging: 360 },
57+
},
58+
});
59+
});
60+
61+
it('should encode a w:pPrChange with only attributes and empty w:pPr', () => {
62+
const xmlNode = {
63+
name: 'w:pPrChange',
64+
attributes: {
65+
'w:id': '5',
66+
'w:author': 'Test Author',
67+
'w:date': '2026-01-01T00:00:00Z',
68+
},
69+
elements: [
70+
{
71+
name: 'w:pPr',
72+
elements: [],
73+
},
74+
],
75+
};
76+
77+
const result = translator.encode({ nodes: [xmlNode] });
78+
79+
expect(result).toEqual({
80+
id: 5,
81+
author: 'Test Author',
82+
date: '2026-01-01T00:00:00Z',
83+
});
84+
});
85+
86+
it('should encode a w:pPrChange with only attributes and no children', () => {
87+
const xmlNode = {
88+
name: 'w:pPrChange',
89+
attributes: {
90+
'w:id': '3',
91+
'w:author': 'Author',
92+
'w:date': '2026-01-01T00:00:00Z',
93+
},
94+
elements: [],
95+
};
96+
97+
const result = translator.encode({ nodes: [xmlNode] });
98+
99+
expect(result).toEqual({
100+
id: 3,
101+
author: 'Author',
102+
date: '2026-01-01T00:00:00Z',
103+
});
104+
});
105+
106+
it('should return undefined if no attributes or children are present', () => {
107+
const xmlNode = {
108+
name: 'w:pPrChange',
109+
attributes: {},
110+
elements: [],
111+
};
112+
113+
const result = translator.encode({ nodes: [xmlNode] });
114+
115+
expect(result).toBeUndefined();
116+
});
117+
});
118+
119+
describe('decode', () => {
120+
it('should decode a change object with attributes and nested paragraphProperties', () => {
121+
const superDocNode = {
122+
attrs: {
123+
change: {
124+
id: 0,
125+
author: 'Luccas Correa',
126+
date: '2026-04-02T11:25:00Z',
127+
paragraphProperties: {
128+
styleId: 'ListParagraph',
129+
numberingProperties: { numId: 1 },
130+
indent: { hanging: 360 },
131+
},
132+
},
133+
},
134+
};
135+
136+
const result = translator.decode({ node: superDocNode });
137+
138+
expect(result.name).toBe('w:pPrChange');
139+
expect(result.attributes).toEqual({
140+
'w:id': '0',
141+
'w:author': 'Luccas Correa',
142+
'w:date': '2026-04-02T11:25:00Z',
143+
});
144+
expect(result.elements).toEqual(
145+
expect.arrayContaining([
146+
expect.objectContaining({
147+
name: 'w:pPr',
148+
elements: expect.arrayContaining([
149+
{ name: 'w:pStyle', attributes: { 'w:val': 'ListParagraph' } },
150+
expect.objectContaining({
151+
name: 'w:numPr',
152+
elements: [{ name: 'w:numId', attributes: { 'w:val': '1' } }],
153+
}),
154+
{ name: 'w:ind', attributes: { 'w:hanging': '360' } },
155+
]),
156+
}),
157+
]),
158+
);
159+
});
160+
161+
it('should decode a change object with only attributes', () => {
162+
const superDocNode = {
163+
attrs: {
164+
change: {
165+
id: 5,
166+
author: 'Test Author',
167+
date: '2026-01-01T00:00:00Z',
168+
},
169+
},
170+
};
171+
172+
const result = translator.decode({ node: superDocNode });
173+
174+
// No child elements means no w:pPr, so nothing to output
175+
// The translator should still produce the element if attributes exist
176+
// But createNestedPropertiesTranslator returns undefined if no child elements
177+
expect(result).toBeUndefined();
178+
});
179+
180+
it('should return undefined if change is empty', () => {
181+
const superDocNode = {
182+
attrs: {
183+
change: {},
184+
},
185+
};
186+
187+
const result = translator.decode({ node: superDocNode });
188+
189+
expect(result).toBeUndefined();
190+
});
191+
192+
it('should return undefined if change is missing', () => {
193+
const superDocNode = {
194+
attrs: {},
195+
};
196+
197+
const result = translator.decode({ node: superDocNode });
198+
199+
expect(result).toBeUndefined();
200+
});
201+
});
202+
203+
describe('round-trip', () => {
204+
it('maintains consistency for a pPrChange with nested properties', () => {
205+
const initialChange = {
206+
id: 0,
207+
author: 'Luccas Correa',
208+
date: '2026-04-02T11:25:00Z',
209+
paragraphProperties: {
210+
styleId: 'ListParagraph',
211+
numberingProperties: { numId: 1 },
212+
indent: { hanging: 360 },
213+
},
214+
};
215+
216+
const decoded = translator.decode({ node: { attrs: { change: initialChange } } });
217+
const encoded = translator.encode({ nodes: [decoded] });
218+
219+
expect(encoded).toEqual(initialChange);
220+
});
221+
222+
it('maintains consistency for a pPrChange with justification', () => {
223+
const initialChange = {
224+
id: 2,
225+
author: 'Another Author',
226+
date: '2026-03-15T10:00:00Z',
227+
paragraphProperties: {
228+
justification: 'center',
229+
spacing: { before: 200, after: 100 },
230+
},
231+
};
232+
233+
const decoded = translator.decode({ node: { attrs: { change: initialChange } } });
234+
const encoded = translator.encode({ nodes: [decoded] });
235+
236+
expect(encoded).toEqual(initialChange);
237+
});
238+
});
239+
});

0 commit comments

Comments
 (0)