Skip to content

Commit aa0fecf

Browse files
authored
Merge pull request #1308 from trycompai/main
[comp] Production Deploy
2 parents 92b09f9 + 2a01cb9 commit aa0fecf

File tree

2 files changed

+80
-14
lines changed

2 files changed

+80
-14
lines changed

packages/ui/src/components/editor/utils/validate-content.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,53 @@ describe('validateAndFixTipTapContent', () => {
181181
groupEndSpy.mockRestore();
182182
});
183183
});
184+
185+
describe('empty text node handling', () => {
186+
const strip = (s: string) => s.replace(/[\u00A0\u200B\u202F]/g, '').trim();
187+
188+
const hasEmptyTextNodes = (node: any): boolean => {
189+
if (!node || typeof node !== 'object') return false;
190+
if (node.type === 'text') {
191+
const txt = typeof node.text === 'string' ? node.text : '';
192+
return strip(txt).length === 0;
193+
}
194+
if (Array.isArray(node.content)) {
195+
return node.content.some((child: any) => hasEmptyTextNodes(child));
196+
}
197+
return false;
198+
};
199+
200+
it('removes empty and whitespace-only (including NBSP/ZWSP) text nodes in paragraphs', () => {
201+
const content = {
202+
type: 'doc',
203+
content: [
204+
{
205+
type: 'paragraph',
206+
content: [
207+
{ type: 'text', text: '' },
208+
{ type: 'text', text: ' ' },
209+
{ type: 'text', text: '\u00A0' },
210+
{ type: 'text', text: '\u200B' },
211+
{ type: 'text', text: 'Hello' },
212+
{ text: 'World' },
213+
],
214+
},
215+
],
216+
};
217+
218+
const fixed = validateAndFixTipTapContent(content);
219+
expect(fixed.type).toBe('doc');
220+
expect(hasEmptyTextNodes(fixed)).toBe(false);
221+
222+
const paragraph = (fixed.content as any[])[0];
223+
const texts = paragraph.content.map((n: any) => n.text);
224+
expect(texts).toEqual(['Hello', 'World']);
225+
});
226+
227+
it('does not introduce empty text nodes when creating empty structures', () => {
228+
const fixed = validateAndFixTipTapContent({});
229+
expect(fixed.type).toBe('doc');
230+
expect(hasEmptyTextNodes(fixed)).toBe(false);
231+
});
232+
});
184233
});

packages/ui/src/components/editor/utils/validate-content.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,14 @@ function fixContentArray(contentArray: any[]): JSONContent[] {
4646
return [createEmptyParagraph()];
4747
}
4848

49-
const fixedContent = contentArray
50-
.map(fixNode)
51-
.filter((node): node is JSONContent => node !== null) as JSONContent[];
49+
const fixedContent = contentArray.map(fixNode).filter((node): node is JSONContent => {
50+
if (!node) return false;
51+
if (node.type === 'text') {
52+
const value = typeof (node as any).text === 'string' ? (node as any).text : '';
53+
return isNonEmptyText(value);
54+
}
55+
return true;
56+
}) as JSONContent[];
5257

5358
// Ensure we have at least one paragraph
5459
if (fixedContent.length === 0) {
@@ -58,6 +63,13 @@ function fixContentArray(contentArray: any[]): JSONContent[] {
5863
return fixedContent;
5964
}
6065

66+
function isNonEmptyText(value: string): boolean {
67+
// Consider normal whitespace, NBSP, zero-width space and narrow no-break space
68+
if (typeof value !== 'string') return false;
69+
const normalized = value.replace(/[\u00A0\u200B\u202F]/g, '');
70+
return normalized.trim().length > 0;
71+
}
72+
6173
/**
6274
* Fixes a single node and its content
6375
*/
@@ -124,12 +136,17 @@ function fixParagraph(node: any): JSONContent {
124136
}
125137
return fixNode(item);
126138
})
127-
.filter(Boolean) as JSONContent[];
139+
.filter((n): n is JSONContent => {
140+
if (!n) return false;
141+
// Drop empty text nodes entirely
142+
if (n.type === 'text') {
143+
const txt = typeof (n as any).text === 'string' ? (n as any).text : '';
144+
return txt.trim().length > 0;
145+
}
146+
return true;
147+
});
128148

129-
// If no valid content, create empty text node
130-
if (fixedContent.length === 0) {
131-
fixedContent.push({ type: 'text', text: '' });
132-
}
149+
// If no valid content, keep an empty paragraph (no empty text nodes)
133150

134151
return {
135152
type: 'paragraph',
@@ -199,9 +216,11 @@ function fixListItem(node: any): JSONContent {
199216
function fixTextNode(node: any): JSONContent {
200217
const { text, marks, ...rest } = node;
201218

219+
const value = typeof text === 'string' ? text : '';
220+
// If the resulting text is empty, return a text node with non-empty check handled by callers.
202221
return {
203222
type: 'text',
204-
text: typeof text === 'string' ? text : '',
223+
text: value,
205224
...(marks && Array.isArray(marks) && { marks: fixMarks(marks) }),
206225
...rest,
207226
};
@@ -219,8 +238,7 @@ function fixHeading(node: any): JSONContent {
219238
return {
220239
type: 'heading',
221240
attrs: { level: validLevel, ...(attrs && typeof attrs === 'object' ? attrs : {}) },
222-
content:
223-
content && Array.isArray(content) ? fixContentArray(content) : [{ type: 'text', text: '' }],
241+
content: content && Array.isArray(content) ? fixContentArray(content) : [],
224242
...rest,
225243
};
226244
}
@@ -248,8 +266,7 @@ function fixCodeBlock(node: any): JSONContent {
248266

249267
return {
250268
type: 'codeBlock',
251-
content:
252-
content && Array.isArray(content) ? fixContentArray(content) : [{ type: 'text', text: '' }],
269+
content: content && Array.isArray(content) ? fixContentArray(content) : [],
253270
...(attrs && typeof attrs === 'object' && { attrs }),
254271
...rest,
255272
};
@@ -287,7 +304,7 @@ function createEmptyDocument(): JSONContent {
287304
function createEmptyParagraph(): JSONContent {
288305
return {
289306
type: 'paragraph',
290-
content: [{ type: 'text', text: '' }],
307+
content: [],
291308
};
292309
}
293310

0 commit comments

Comments
 (0)