diff --git a/.changeset/lazy-walls-run.md b/.changeset/lazy-walls-run.md new file mode 100644 index 0000000..427bf52 --- /dev/null +++ b/.changeset/lazy-walls-run.md @@ -0,0 +1,5 @@ +--- +'@graphcms/html-to-slate-ast': minor +--- + +Populate empty children array with text node diff --git a/packages/html-to-slate-ast/src/index.ts b/packages/html-to-slate-ast/src/index.ts index df16617..05e925e 100644 --- a/packages/html-to-slate-ast/src/index.ts +++ b/packages/html-to-slate-ast/src/index.ts @@ -86,10 +86,15 @@ function deserialize(el: Node) { ) { parent = el.childNodes[0]; } - const children = Array.from(parent.childNodes) + let children = Array.from(parent.childNodes) .map(deserialize) .flat() as ChildNode[]; + if (children.length === 0) { + if (!['COLGROUP', 'COL', 'CAPTION', 'TFOOT'].includes(nodeName)) + // @ts-expect-error + children = [{ text: '' }]; + } if (el.nodeName === 'BODY') { return jsx('fragment', {}, children); } @@ -137,7 +142,9 @@ function deserialize(el: Node) { return jsx('element', attrs, listItemChildren); } else if ( nodeName === 'TABLE' && - !children.find((node: ChildNode) => node.nodeName === 'THEAD') + !Array.from((el as HTMLTableElement).childNodes).find( + (node: ChildNode) => node.nodeName === 'THEAD' + ) ) { // tables must have thead, otherwise field crashes const thead = { @@ -145,6 +152,41 @@ function deserialize(el: Node) { children: [], }; return jsx('element', attrs, [thead, ...children]); + } else if (nodeName === 'TR') { + // if TR is empty, insert a cell with a paragraph to ensure selection can be placed inside + const modifiedChildren = + (el as HTMLTableRowElement).cells.length === 0 + ? [ + { + type: 'table_cell', + children: [ + { + type: 'paragraph', + children: [{ text: el.textContent ? el.textContent : '' }], + }, + ], + }, + ] + : children; + return jsx('element', attrs, modifiedChildren); + } else if (nodeName === 'TD') { + // if TD is empty, insert a a paragraph to ensure selection can be placed inside + const childNodes = Array.from( + (el as HTMLTableDataCellElement).childNodes + ); + const modifiedChildren = + childNodes.length === 0 + ? [ + { + type: 'paragraph', + children: [{ text: '' }], + }, + ] + : childNodes.map((child) => ({ + type: 'paragraph', + children: [{ text: child.textContent ? child.textContent : '' }], + })); + return jsx('element', attrs, modifiedChildren); } else if (nodeName === 'IMG') { return jsx('element', attrs, [attrs.href]); } @@ -153,12 +195,10 @@ function deserialize(el: Node) { if (nodeName === 'DIV') { const childNodes = Array.from(el.childNodes); - const isParagraph = - childNodes.length && - childNodes.every( - (child) => - (isElementNode(child) && isInlineElement(child)) || isTextNode(child) - ); + const isParagraph = childNodes.every( + (child) => + (isElementNode(child) && isInlineElement(child)) || isTextNode(child) + ); if (isParagraph) { return jsx('element', { type: 'paragraph' }, children); } diff --git a/packages/html-to-slate-ast/test/google-docs_input.html b/packages/html-to-slate-ast/test/google-docs_input.html new file mode 100644 index 0000000..b8022cc --- /dev/null +++ b/packages/html-to-slate-ast/test/google-docs_input.html @@ -0,0 +1 @@ +

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

Link to GH 

Unordered list:

Ordered list:

  1. One

  2. Two

Table:

Cell one

Cell two



\ No newline at end of file diff --git a/packages/html-to-slate-ast/test/index.test.ts b/packages/html-to-slate-ast/test/index.test.ts index 06f6115..f135688 100644 --- a/packages/html-to-slate-ast/test/index.test.ts +++ b/packages/html-to-slate-ast/test/index.test.ts @@ -88,26 +88,11 @@ test('Transforms inner spans wrapped in a div into paragraph', () => { }); test('Transforms Google Docs input', () => { - return htmlToSlateAST( - `

Title

Subtitle

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6

Unordered list:

Ordered list:

  1. One

  2. Two

Table:

Cell one

Cell two



` - ).then((ast) => + const input = fs + .readFileSync(__dirname + '/google-docs_input.html') + .toString(); + return htmlToSlateAST(input).then((ast) => expect(ast).toEqual([ - { - type: 'paragraph', - children: [ - { - text: 'Title', - }, - ], - }, - { - type: 'paragraph', - children: [ - { - text: 'Subtitle', - }, - ], - }, { type: 'heading-one', children: [ @@ -153,7 +138,25 @@ test('Transforms Google Docs input', () => { children: [ { text: 'Heading 6', - italic: true, + }, + ], + }, + { + type: 'paragraph', + children: [ + { + type: 'link', + href: 'https://github.com/GraphCMS/next-webapp/pull/1034', + openInNewTab: false, + children: [ + { + text: 'Link to GH', + underline: true, + }, + ], + }, + { + text: ' ', }, ], }, @@ -304,7 +307,12 @@ test('Transforms Google Docs input', () => { type: 'table_cell', children: [ { - text: '\n', + type: 'paragraph', + children: [ + { + text: '', + }, + ], }, ], }, @@ -312,7 +320,12 @@ test('Transforms Google Docs input', () => { type: 'table_cell', children: [ { - text: '\n', + type: 'paragraph', + children: [ + { + text: '', + }, + ], }, ], }, @@ -444,6 +457,7 @@ test('Converts word documents', () => { ], }, { type: 'paragraph', children: [{ text: 'Table\u00a0' }] }, + { type: 'paragraph', children: [{ text: '' }] }, { type: 'table', children: [ @@ -516,3 +530,62 @@ test('Converts an image pasted from Google Docs into a link node', () => { ]) ); }); + +test('Reshape an incorrectly structured table', () => { + return htmlToSlateAST( + '
' + ).then((ast) => + expect(ast).toStrictEqual([ + { + type: 'table', + children: [ + { + type: 'table_head', + children: [], + }, + { + type: 'table_body', + children: [ + { + type: 'table_row', + children: [ + { + type: 'table_cell', + children: [ + { + type: 'paragraph', + children: [ + { + text: '', + }, + ], + }, + ], + }, + ], + }, + { + type: 'table_row', + children: [ + { + type: 'table_cell', + children: [ + { + type: 'paragraph', + children: [ + { + text: '', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ]) + ); +});