Skip to content

Commit fd1580e

Browse files
authored
fix(super-converter): normalize inline nodes in non-TOC docPartObj content (#2573)
The generic docPartObj handler was missing the normalizeDocPartContent() call that the TOC handler already had, causing documents with non-TOC gallery types (page numbers, bibliographies, cover pages) containing top-level inline nodes (bookmarks, comments, permissions) to fail with "Invalid content for node type documentPartObject". Also expanded the inline node type set to include commentRangeStart, commentRangeEnd, permStart, and permEnd. SD-2357
1 parent 82e4af2 commit fd1580e

2 files changed

Lines changed: 142 additions & 3 deletions

File tree

packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-doc-part-obj.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export const genericDocPartHandler = (params) => {
9494

9595
const result = {
9696
type: 'documentPartObject',
97-
content: translatedContent,
97+
content: normalizeDocPartContent(translatedContent),
9898
attrs: {
9999
id,
100100
docPartGallery,
@@ -109,7 +109,14 @@ const validGalleryTypeMap = {
109109
'Table of Contents': tableOfContentsHandler,
110110
};
111111

112-
const inlineNodeTypes = new Set(['bookmarkStart', 'bookmarkEnd']);
112+
const inlineNodeTypes = new Set([
113+
'bookmarkStart',
114+
'bookmarkEnd',
115+
'commentRangeStart',
116+
'commentRangeEnd',
117+
'permStart',
118+
'permEnd',
119+
]);
113120
const SD_TOC_XML_NAME = 'sd:tableOfContents';
114121
const PARAGRAPH_XML_NAME = 'w:p';
115122
const PARAGRAPH_PROPERTIES_XML_NAME = 'w:pPr';

packages/super-editor/src/core/super-converter/v3/handlers/w/sdt/helpers/handle-doc-part-obj.test.js

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { describe, it, expect, vi } from 'vitest';
2-
import { normalizeDocPartContent, handleDocPartObj, tableOfContentsHandler } from './handle-doc-part-obj.js';
2+
import {
3+
normalizeDocPartContent,
4+
handleDocPartObj,
5+
tableOfContentsHandler,
6+
genericDocPartHandler,
7+
} from './handle-doc-part-obj.js';
38

49
describe('normalizeDocPartContent', () => {
510
it('wraps inline bookmark nodes in paragraphs', () => {
@@ -28,6 +33,39 @@ describe('normalizeDocPartContent', () => {
2833
const normalized = normalizeDocPartContent(nodes);
2934
expect(normalized).toEqual([nodes[0], { type: 'paragraph', content: [nodes[1]] }]);
3035
});
36+
37+
it('wraps commentRangeStart and commentRangeEnd in paragraphs', () => {
38+
const nodes = [
39+
{ type: 'commentRangeStart', attrs: { id: '1' } },
40+
{ type: 'paragraph', content: [{ type: 'text', text: 'text' }] },
41+
{ type: 'commentRangeEnd', attrs: { id: '1' } },
42+
];
43+
const normalized = normalizeDocPartContent(nodes);
44+
expect(normalized).toEqual([
45+
{ type: 'paragraph', content: [nodes[0]] },
46+
nodes[1],
47+
{ type: 'paragraph', content: [nodes[2]] },
48+
]);
49+
});
50+
51+
it('wraps permStart and permEnd in paragraphs', () => {
52+
const nodes = [
53+
{ type: 'permStart', attrs: { id: '1' } },
54+
{ type: 'paragraph', content: [{ type: 'text', text: 'protected' }] },
55+
{ type: 'permEnd', attrs: { id: '1' } },
56+
];
57+
const normalized = normalizeDocPartContent(nodes);
58+
expect(normalized).toEqual([
59+
{ type: 'paragraph', content: [nodes[0]] },
60+
nodes[1],
61+
{ type: 'paragraph', content: [nodes[2]] },
62+
]);
63+
});
64+
65+
it('handles empty input', () => {
66+
expect(normalizeDocPartContent([])).toEqual([]);
67+
expect(normalizeDocPartContent()).toEqual([]);
68+
});
3169
});
3270

3371
describe('handleDocPartObj', () => {
@@ -122,6 +160,100 @@ describe('handleDocPartObj', () => {
122160
});
123161
});
124162

163+
describe('genericDocPartHandler', () => {
164+
it('normalizes inline nodes in non-TOC docPartObj content', () => {
165+
const handler = vi.fn(() => [
166+
{ type: 'bookmarkStart', attrs: { name: '_GoBack' } },
167+
{ type: 'paragraph', content: [{ type: 'text', text: 'Page Numbers' }] },
168+
{ type: 'bookmarkEnd', attrs: { name: '_GoBack' } },
169+
]);
170+
const sdtPr = {
171+
name: 'w:sdtPr',
172+
elements: [
173+
{ name: 'w:id', attributes: { 'w:val': '100' } },
174+
{
175+
name: 'w:docPartObj',
176+
elements: [{ name: 'w:docPartGallery', attributes: { 'w:val': 'Page Numbers (Bottom of Page)' } }],
177+
},
178+
],
179+
};
180+
const contentNode = { name: 'w:sdtContent', elements: [] };
181+
const params = {
182+
nodes: [contentNode],
183+
nodeListHandler: { handler },
184+
extraParams: { sdtPr, docPartGalleryType: 'Page Numbers (Bottom of Page)' },
185+
path: [],
186+
};
187+
188+
const result = genericDocPartHandler(params);
189+
190+
expect(result.type).toEqual('documentPartObject');
191+
expect(result.content).toEqual([
192+
{ type: 'paragraph', content: [{ type: 'bookmarkStart', attrs: { name: '_GoBack' } }] },
193+
{ type: 'paragraph', content: [{ type: 'text', text: 'Page Numbers' }] },
194+
{ type: 'paragraph', content: [{ type: 'bookmarkEnd', attrs: { name: '_GoBack' } }] },
195+
]);
196+
});
197+
198+
it('normalizes commentRangeStart/End in non-TOC docPartObj content', () => {
199+
const handler = vi.fn(() => [
200+
{ type: 'commentRangeStart', attrs: { id: '5' } },
201+
{ type: 'paragraph', content: [{ type: 'text', text: 'Bibliography' }] },
202+
{ type: 'commentRangeEnd', attrs: { id: '5' } },
203+
]);
204+
const sdtPr = {
205+
name: 'w:sdtPr',
206+
elements: [
207+
{ name: 'w:id', attributes: { 'w:val': '200' } },
208+
{
209+
name: 'w:docPartObj',
210+
elements: [{ name: 'w:docPartGallery', attributes: { 'w:val': 'Bibliographies' } }],
211+
},
212+
],
213+
};
214+
const contentNode = { name: 'w:sdtContent', elements: [] };
215+
const params = {
216+
nodes: [contentNode],
217+
nodeListHandler: { handler },
218+
extraParams: { sdtPr, docPartGalleryType: 'Bibliographies' },
219+
path: [],
220+
};
221+
222+
const result = genericDocPartHandler(params);
223+
224+
expect(result.content).toEqual([
225+
{ type: 'paragraph', content: [{ type: 'commentRangeStart', attrs: { id: '5' } }] },
226+
{ type: 'paragraph', content: [{ type: 'text', text: 'Bibliography' }] },
227+
{ type: 'paragraph', content: [{ type: 'commentRangeEnd', attrs: { id: '5' } }] },
228+
]);
229+
});
230+
231+
it('leaves block-only content unchanged', () => {
232+
const handler = vi.fn(() => [{ type: 'paragraph', content: [{ type: 'text', text: 'Cover Page' }] }]);
233+
const sdtPr = {
234+
name: 'w:sdtPr',
235+
elements: [
236+
{ name: 'w:id', attributes: { 'w:val': '300' } },
237+
{
238+
name: 'w:docPartObj',
239+
elements: [{ name: 'w:docPartGallery', attributes: { 'w:val': 'Cover Pages' } }],
240+
},
241+
],
242+
};
243+
const contentNode = { name: 'w:sdtContent', elements: [] };
244+
const params = {
245+
nodes: [contentNode],
246+
nodeListHandler: { handler },
247+
extraParams: { sdtPr, docPartGalleryType: 'Cover Pages' },
248+
path: [],
249+
};
250+
251+
const result = genericDocPartHandler(params);
252+
253+
expect(result.content).toEqual([{ type: 'paragraph', content: [{ type: 'text', text: 'Cover Page' }] }]);
254+
});
255+
});
256+
125257
describe('tableOfContentsHandler', () => {
126258
const mockNodeListHandler = {
127259
handler: vi.fn(() => [{ type: 'paragraph', content: [{ type: 'text', text: 'TOC Content' }] }]),

0 commit comments

Comments
 (0)