@@ -46,14 +46,9 @@ function fixContentArray(contentArray: any[]): JSONContent[] {
4646 return [ createEmptyParagraph ( ) ] ;
4747 }
4848
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 [ ] ;
49+ const fixedContent = contentArray
50+ . map ( fixNode )
51+ . filter ( ( node ) : node is JSONContent => node !== null ) as JSONContent [ ] ;
5752
5853 // Ensure we have at least one paragraph
5954 if ( fixedContent . length === 0 ) {
@@ -63,11 +58,13 @@ function fixContentArray(contentArray: any[]): JSONContent[] {
6358 return fixedContent ;
6459}
6560
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 ;
61+ function ensureNonEmptyText ( value : unknown ) : string {
62+ const text = typeof value === 'string' ? value : '' ;
63+ // Normalize NBSP and narrow no-break space for emptiness checks
64+ const normalized = text . replace ( / [ \u00A0 \u202F ] / g, '' ) ;
65+ if ( normalized . trim ( ) . length > 0 ) return text ;
66+ // Return zero-width space to ensure non-empty text node without visual change
67+ return '\u200B' ;
7168}
7269
7370/**
@@ -94,6 +91,12 @@ function fixNode(node: any): JSONContent | null {
9491 return fixList ( node ) ;
9592 case 'listItem' :
9693 return fixListItem ( node ) ;
94+ case 'table' :
95+ return fixTable ( node ) ;
96+ case 'tableRow' :
97+ return fixTableRow ( node ) ;
98+ case 'tableCell' :
99+ return fixTableCell ( node ) ;
97100 case 'text' :
98101 return fixTextNode ( node ) ;
99102 case 'heading' :
@@ -102,6 +105,8 @@ function fixNode(node: any): JSONContent | null {
102105 return fixBlockquote ( node ) ;
103106 case 'codeBlock' :
104107 return fixCodeBlock ( node ) ;
108+ case 'hardBreak' :
109+ return { type : 'hardBreak' } ;
105110 default :
106111 // For other valid nodes, just fix their content if they have any
107112 return {
@@ -130,21 +135,13 @@ function fixParagraph(node: any): JSONContent {
130135 if ( item . text && ! item . type ) {
131136 return {
132137 type : 'text' ,
133- text : item . text ,
138+ text : ensureNonEmptyText ( item . text ) ,
134139 ...( item . marks && { marks : fixMarks ( item . marks ) } ) ,
135140 } ;
136141 }
137142 return fixNode ( item ) ;
138143 } )
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- } ) ;
144+ . filter ( ( n ) : n is JSONContent => Boolean ( n ) ) ;
148145
149146 // If no valid content, keep an empty paragraph (no empty text nodes)
150147
@@ -216,8 +213,7 @@ function fixListItem(node: any): JSONContent {
216213function fixTextNode ( node : any ) : JSONContent {
217214 const { text, marks, ...rest } = node ;
218215
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.
216+ const value = ensureNonEmptyText ( text ) ;
221217 return {
222218 type : 'text' ,
223219 text : value ,
@@ -272,6 +268,49 @@ function fixCodeBlock(node: any): JSONContent {
272268 } ;
273269}
274270
271+ /**
272+ * Fixes table structures
273+ */
274+ function fixTable ( node : any ) : JSONContent {
275+ const { content, attrs, ...rest } = node ;
276+ let rows : JSONContent [ ] = [ ] ;
277+ if ( Array . isArray ( content ) ) {
278+ rows = content
279+ . map ( ( child : any ) => ( child ?. type === 'tableRow' ? fixTableRow ( child ) : null ) )
280+ . filter ( Boolean ) as JSONContent [ ] ;
281+ }
282+ if ( rows . length === 0 ) {
283+ rows = [ createEmptyTableRow ( ) ] ;
284+ }
285+ return { type : 'table' , content : rows , ...( attrs && { attrs } ) , ...rest } ;
286+ }
287+
288+ function fixTableRow ( node : any ) : JSONContent {
289+ const { content, attrs, ...rest } = node ;
290+ let cells : JSONContent [ ] = [ ] ;
291+ if ( Array . isArray ( content ) ) {
292+ cells = content
293+ . map ( ( child : any ) => ( child ?. type === 'tableCell' ? fixTableCell ( child ) : null ) )
294+ . filter ( Boolean ) as JSONContent [ ] ;
295+ }
296+ if ( cells . length === 0 ) {
297+ cells = [ createEmptyTableCell ( ) ] ;
298+ }
299+ return { type : 'tableRow' , content : cells , ...( attrs && { attrs } ) , ...rest } ;
300+ }
301+
302+ function fixTableCell ( node : any ) : JSONContent {
303+ const { content, attrs, ...rest } = node ;
304+ let blocks : JSONContent [ ] = [ ] ;
305+ if ( Array . isArray ( content ) ) {
306+ blocks = fixContentArray ( content ) ;
307+ }
308+ if ( blocks . length === 0 ) {
309+ blocks = [ createEmptyParagraph ( ) ] ;
310+ }
311+ return { type : 'tableCell' , content : blocks , ...( attrs && { attrs } ) , ...rest } ;
312+ }
313+
275314/**
276315 * Fixes marks array
277316 */
@@ -318,6 +357,20 @@ function createEmptyListItem(): JSONContent {
318357 } ;
319358}
320359
360+ function createEmptyTableCell ( ) : JSONContent {
361+ return {
362+ type : 'tableCell' ,
363+ content : [ createEmptyParagraph ( ) ] ,
364+ } ;
365+ }
366+
367+ function createEmptyTableRow ( ) : JSONContent {
368+ return {
369+ type : 'tableRow' ,
370+ content : [ createEmptyTableCell ( ) ] ,
371+ } ;
372+ }
373+
321374/**
322375 * Validates if content is a valid TipTap document structure
323376 */
0 commit comments