From 90dd2e284f3a8b96dbaeeae182eb93c1297061ad Mon Sep 17 00:00:00 2001 From: mpkelly Date: Tue, 13 Dec 2022 15:53:22 +0000 Subject: [PATCH 1/4] Allow pasting tables with list content inside the cells. This content is converted to simple content which is formatted to look like a list using whitespace. --- packages/block-library/src/list/transforms.js | 2 +- .../block-library/src/table/transforms.js | 99 ++++++++++++++++++- test/integration/blocks-raw-handling.test.js | 6 ++ .../google-docs-table-with-lists-in.html | 1 + .../google-docs-table-with-lists-out.html | 3 + 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 test/integration/fixtures/documents/google-docs-table-with-lists-in.html create mode 100644 test/integration/fixtures/documents/google-docs-table-with-lists-out.html diff --git a/packages/block-library/src/list/transforms.js b/packages/block-library/src/list/transforms.js index a6263d7ad639c..b92047239534e 100644 --- a/packages/block-library/src/list/transforms.js +++ b/packages/block-library/src/list/transforms.js @@ -9,7 +9,7 @@ import { create, split, toHTMLString } from '@wordpress/rich-text'; */ import { createListBlockFromDOMElement } from './utils'; -function getListContentSchema( { phrasingContentSchema } ) { +export function getListContentSchema( { phrasingContentSchema } ) { const listContentSchema = { ...phrasingContentSchema, ul: {}, diff --git a/packages/block-library/src/table/transforms.js b/packages/block-library/src/table/transforms.js index 0651c3bc64c41..cdcfc7c3aa375 100644 --- a/packages/block-library/src/table/transforms.js +++ b/packages/block-library/src/table/transforms.js @@ -1,15 +1,26 @@ +/** + * WordPress dependencies + */ +import { createBlock, getBlockAttributes } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { name } from './block.json'; +import { getListContentSchema } from '../list/transforms'; + const tableContentPasteSchema = ( { phrasingContentSchema } ) => ( { tr: { allowEmpty: true, children: { th: { allowEmpty: true, - children: phrasingContentSchema, + children: getListContentSchema( { phrasingContentSchema } ), attributes: [ 'scope', 'colspan' ], }, td: { allowEmpty: true, - children: phrasingContentSchema, + children: getListContentSchema( { phrasingContentSchema } ), attributes: [ 'colspan' ], }, }, @@ -35,12 +46,96 @@ const tablePasteSchema = ( args ) => ( { }, } ); +const indent = ( depth ) => { + return Array.from( { length: depth * 4 } ) + .map( () => ' ' ) + .join( '' ); +}; + +const isList = ( node ) => { + return node.tagName === 'UL' || node.tagName === 'OL'; +}; + +const transformContent = ( cell ) => { + // not available by default in node (jest) env so eslint complains. + // eslint-disable-next-line no-undef + const parser = new DOMParser(); + const document = parser.parseFromString( cell.content, 'text/html' ); + const result = []; + const processDOMTree = ( node, listDepth = 0 ) => { + if ( isList( node ) ) { + if ( listDepth === 0 ) { + result.push( '
' ); + } + Array.from( node.childNodes ).forEach( + ( listItem, listItemIndex ) => { + const children = Array.from( listItem.childNodes ); + let simple = []; + children.forEach( ( child ) => { + if ( isList( child ) ) { + const bullet = + node.tagName === 'UL' + ? '-' + : `${ listItemIndex + 1 }.`; + if ( simple.length ) { + result.push( + `${ indent( + listDepth + ) } ${ bullet } ${ simple.join( '' ) }
` + ); + simple = []; + } + processDOMTree( child, listDepth + 1 ); + } else { + simple.push( child.outerHTML || child.textContent ); + } + } ); + if ( simple.length ) { + const bullet = + node.tagName === 'UL' + ? '-' + : `${ listItemIndex + 1 }.`; + + result.push( + `${ indent( + listDepth + ) } ${ bullet } ${ simple.join( '' ) }
` + ); + simple = []; + } + } + ); + } else if ( node.nodeName === '#text' ) { + result.push( node.textContent ); + } else { + result.push( node.outerHTML ); + } + }; + + Array.from( document.body.childNodes ).forEach( ( node ) => + processDOMTree( node ) + ); + + cell.content = result.join( '' ); +}; + +const transformTableNode = ( node ) => { + const attributes = getBlockAttributes( name, node.outerHTML ); + attributes.body.forEach( ( row ) => { + row.cells.forEach( ( cell ) => { + transformContent( cell ); + } ); + } ); + return createBlock( name, attributes ); +}; + const transforms = { from: [ { type: 'raw', selector: 'table', schema: tablePasteSchema, + transform: transformTableNode, }, ], }; diff --git a/test/integration/blocks-raw-handling.test.js b/test/integration/blocks-raw-handling.test.js index eeab475567d27..5a9df2dff4634 100644 --- a/test/integration/blocks-raw-handling.test.js +++ b/test/integration/blocks-raw-handling.test.js @@ -1,3 +1,8 @@ +// eslint-disable-next-line jsdoc/check-tag-names +/** + * @jest-environment jsdom + */ + /** * External dependencies */ @@ -379,6 +384,7 @@ describe( 'Blocks raw handling', () => { 'google-docs-list-only', 'google-docs-table', 'google-docs-table-with-comments', + 'google-docs-table-with-lists', 'google-docs-with-comments', 'ms-word', 'ms-word-styled', diff --git a/test/integration/fixtures/documents/google-docs-table-with-lists-in.html b/test/integration/fixtures/documents/google-docs-table-with-lists-in.html new file mode 100644 index 0000000000000..7fca80ba3a972 --- /dev/null +++ b/test/integration/fixtures/documents/google-docs-table-with-lists-in.html @@ -0,0 +1 @@ +

1. Owner

Daniel

Notes:

  1. “Issues” are topics and agenda items anyone can bring to the meeting.

    • Nested

  2. IDS stands for “Identify, Discuss, Solve”.
    Testing new line

    • This is nested

      1. This is even more nested!

More stuff


Other ideas:

  1. A new list

    1. Nested again


diff --git a/test/integration/fixtures/documents/google-docs-table-with-lists-out.html b/test/integration/fixtures/documents/google-docs-table-with-lists-out.html new file mode 100644 index 0000000000000..5e50ce09ffde1 --- /dev/null +++ b/test/integration/fixtures/documents/google-docs-table-with-lists-out.html @@ -0,0 +1,3 @@ + +
1. OwnerDaniel
Notes:
1. “Issues” are topics and agenda items anyone can bring to the meeting.
- Nested
2. IDS stands for “Identify, Discuss, Solve”.
Testing new line
- This is nested
1. This is even more nested!
More stuff
Other ideas:
1. A new list
1. Nested again
+ From c8cc1c325953b5b9fdb80c0de5923828cd7007ec Mon Sep 17 00:00:00 2001 From: mpkelly Date: Tue, 13 Dec 2022 15:53:22 +0000 Subject: [PATCH 2/4] Allow pasting tables with list content inside the cells. This content is converted to simple content which is formatted to look like a list using whitespace. --- packages/block-library/src/list/transforms.js | 2 +- .../block-library/src/table/transforms.js | 99 ++++++++++++++++++- test/integration/blocks-raw-handling.test.js | 6 ++ .../google-docs-table-with-lists-in.html | 1 + .../google-docs-table-with-lists-out.html | 3 + 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 test/integration/fixtures/documents/google-docs-table-with-lists-in.html create mode 100644 test/integration/fixtures/documents/google-docs-table-with-lists-out.html diff --git a/packages/block-library/src/list/transforms.js b/packages/block-library/src/list/transforms.js index a6263d7ad639c..b92047239534e 100644 --- a/packages/block-library/src/list/transforms.js +++ b/packages/block-library/src/list/transforms.js @@ -9,7 +9,7 @@ import { create, split, toHTMLString } from '@wordpress/rich-text'; */ import { createListBlockFromDOMElement } from './utils'; -function getListContentSchema( { phrasingContentSchema } ) { +export function getListContentSchema( { phrasingContentSchema } ) { const listContentSchema = { ...phrasingContentSchema, ul: {}, diff --git a/packages/block-library/src/table/transforms.js b/packages/block-library/src/table/transforms.js index 0651c3bc64c41..cdcfc7c3aa375 100644 --- a/packages/block-library/src/table/transforms.js +++ b/packages/block-library/src/table/transforms.js @@ -1,15 +1,26 @@ +/** + * WordPress dependencies + */ +import { createBlock, getBlockAttributes } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { name } from './block.json'; +import { getListContentSchema } from '../list/transforms'; + const tableContentPasteSchema = ( { phrasingContentSchema } ) => ( { tr: { allowEmpty: true, children: { th: { allowEmpty: true, - children: phrasingContentSchema, + children: getListContentSchema( { phrasingContentSchema } ), attributes: [ 'scope', 'colspan' ], }, td: { allowEmpty: true, - children: phrasingContentSchema, + children: getListContentSchema( { phrasingContentSchema } ), attributes: [ 'colspan' ], }, }, @@ -35,12 +46,96 @@ const tablePasteSchema = ( args ) => ( { }, } ); +const indent = ( depth ) => { + return Array.from( { length: depth * 4 } ) + .map( () => ' ' ) + .join( '' ); +}; + +const isList = ( node ) => { + return node.tagName === 'UL' || node.tagName === 'OL'; +}; + +const transformContent = ( cell ) => { + // not available by default in node (jest) env so eslint complains. + // eslint-disable-next-line no-undef + const parser = new DOMParser(); + const document = parser.parseFromString( cell.content, 'text/html' ); + const result = []; + const processDOMTree = ( node, listDepth = 0 ) => { + if ( isList( node ) ) { + if ( listDepth === 0 ) { + result.push( '
' ); + } + Array.from( node.childNodes ).forEach( + ( listItem, listItemIndex ) => { + const children = Array.from( listItem.childNodes ); + let simple = []; + children.forEach( ( child ) => { + if ( isList( child ) ) { + const bullet = + node.tagName === 'UL' + ? '-' + : `${ listItemIndex + 1 }.`; + if ( simple.length ) { + result.push( + `${ indent( + listDepth + ) } ${ bullet } ${ simple.join( '' ) }
` + ); + simple = []; + } + processDOMTree( child, listDepth + 1 ); + } else { + simple.push( child.outerHTML || child.textContent ); + } + } ); + if ( simple.length ) { + const bullet = + node.tagName === 'UL' + ? '-' + : `${ listItemIndex + 1 }.`; + + result.push( + `${ indent( + listDepth + ) } ${ bullet } ${ simple.join( '' ) }
` + ); + simple = []; + } + } + ); + } else if ( node.nodeName === '#text' ) { + result.push( node.textContent ); + } else { + result.push( node.outerHTML ); + } + }; + + Array.from( document.body.childNodes ).forEach( ( node ) => + processDOMTree( node ) + ); + + cell.content = result.join( '' ); +}; + +const transformTableNode = ( node ) => { + const attributes = getBlockAttributes( name, node.outerHTML ); + attributes.body.forEach( ( row ) => { + row.cells.forEach( ( cell ) => { + transformContent( cell ); + } ); + } ); + return createBlock( name, attributes ); +}; + const transforms = { from: [ { type: 'raw', selector: 'table', schema: tablePasteSchema, + transform: transformTableNode, }, ], }; diff --git a/test/integration/blocks-raw-handling.test.js b/test/integration/blocks-raw-handling.test.js index eeab475567d27..5a9df2dff4634 100644 --- a/test/integration/blocks-raw-handling.test.js +++ b/test/integration/blocks-raw-handling.test.js @@ -1,3 +1,8 @@ +// eslint-disable-next-line jsdoc/check-tag-names +/** + * @jest-environment jsdom + */ + /** * External dependencies */ @@ -379,6 +384,7 @@ describe( 'Blocks raw handling', () => { 'google-docs-list-only', 'google-docs-table', 'google-docs-table-with-comments', + 'google-docs-table-with-lists', 'google-docs-with-comments', 'ms-word', 'ms-word-styled', diff --git a/test/integration/fixtures/documents/google-docs-table-with-lists-in.html b/test/integration/fixtures/documents/google-docs-table-with-lists-in.html new file mode 100644 index 0000000000000..7fca80ba3a972 --- /dev/null +++ b/test/integration/fixtures/documents/google-docs-table-with-lists-in.html @@ -0,0 +1 @@ +

1. Owner

Daniel

Notes:

  1. “Issues” are topics and agenda items anyone can bring to the meeting.

    • Nested

  2. IDS stands for “Identify, Discuss, Solve”.
    Testing new line

    • This is nested

      1. This is even more nested!

More stuff


Other ideas:

  1. A new list

    1. Nested again


diff --git a/test/integration/fixtures/documents/google-docs-table-with-lists-out.html b/test/integration/fixtures/documents/google-docs-table-with-lists-out.html new file mode 100644 index 0000000000000..5e50ce09ffde1 --- /dev/null +++ b/test/integration/fixtures/documents/google-docs-table-with-lists-out.html @@ -0,0 +1,3 @@ + +
1. OwnerDaniel
Notes:
1. “Issues” are topics and agenda items anyone can bring to the meeting.
- Nested
2. IDS stands for “Identify, Discuss, Solve”.
Testing new line
- This is nested
1. This is even more nested!
More stuff
Other ideas:
1. A new list
1. Nested again
+ From 31315c1c9c9abfb162c0863139f3ea82e8e6e2c6 Mon Sep 17 00:00:00 2001 From: mpkelly Date: Thu, 22 Dec 2022 10:53:57 +0000 Subject: [PATCH 3/4] Fix table test by recreating in-file that was not being recognised by the fixture test; add missing attribute to the out-file that got lost somewhere in the merging/rebasing. --- test/integration/__snapshots__/blocks-raw-handling.test.js.snap | 2 ++ ...with-lists-in.html => google-docs-table-with-lists-in.html} | 0 .../fixtures/documents/google-docs-table-with-lists-out.html | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) rename test/integration/fixtures/documents/{google-docs-table-with-lists-in.html => google-docs-table-with-lists-in.html} (100%) diff --git a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap index 19aa5bd262ea3..58969d8e5e79c 100644 --- a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap +++ b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap @@ -14,6 +14,8 @@ exports[`Blocks raw handling pasteHandler google-docs-table 1`] = `"


exports[`Blocks raw handling pasteHandler google-docs-table-with-comments 1`] = `"



One
Two
Three
1
2
3
I
II
III"`; +exports[`Blocks raw handling pasteHandler google-docs-table-with-lists 1`] = `"

1. Owner
Daniel
Notes:
“Issues” are topics and agenda items anyone can bring to the meeting.
Nested
IDS stands for “Identify, Discuss, Solve”.
Testing new line
This is nested
This is even more nested!
More stuff

Other ideas:
A new list
Nested again
"`; + exports[`Blocks raw handling pasteHandler google-docs-with-comments 1`] = `"This is a title

This is a heading

Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.

A
Bulleted
Indented
List

One
Two
Three




One
Two
Three
1
2
3
I
II
III





An image:



"`; exports[`Blocks raw handling pasteHandler gutenberg 1`] = `"Test"`; diff --git a/test/integration/fixtures/documents/google-docs-table-with-lists-in.html b/test/integration/fixtures/documents/google-docs-table-with-lists-in.html similarity index 100% rename from test/integration/fixtures/documents/google-docs-table-with-lists-in.html rename to test/integration/fixtures/documents/google-docs-table-with-lists-in.html diff --git a/test/integration/fixtures/documents/google-docs-table-with-lists-out.html b/test/integration/fixtures/documents/google-docs-table-with-lists-out.html index 5e50ce09ffde1..76cbe532e305f 100644 --- a/test/integration/fixtures/documents/google-docs-table-with-lists-out.html +++ b/test/integration/fixtures/documents/google-docs-table-with-lists-out.html @@ -1,3 +1,3 @@ -
1. OwnerDaniel
Notes:
1. “Issues” are topics and agenda items anyone can bring to the meeting.
- Nested
2. IDS stands for “Identify, Discuss, Solve”.
Testing new line
- This is nested
1. This is even more nested!
More stuff
Other ideas:
1. A new list
1. Nested again
+
1. OwnerDaniel
Notes:
1. “Issues” are topics and agenda items anyone can bring to the meeting.
- Nested
2. IDS stands for “Identify, Discuss, Solve”.
Testing new line
- This is nested
1. This is even more nested!
More stuff
Other ideas:
1. A new list
1. Nested again
From 30ce2cc71d227a65abe179dae2b79a055e99a2e7 Mon Sep 17 00:00:00 2001 From: mpkelly Date: Thu, 22 Dec 2022 10:53:57 +0000 Subject: [PATCH 4/4] Fix table test by recreating in-file that was not being recognised by the fixture test; add missing attribute to the out-file that got lost somewhere in the merging/rebasing. --- test/integration/__snapshots__/blocks-raw-handling.test.js.snap | 2 ++ ...with-lists-in.html => google-docs-table-with-lists-in.html} | 0 .../fixtures/documents/google-docs-table-with-lists-out.html | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) rename test/integration/fixtures/documents/{google-docs-table-with-lists-in.html => google-docs-table-with-lists-in.html} (100%) diff --git a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap index 19aa5bd262ea3..58969d8e5e79c 100644 --- a/test/integration/__snapshots__/blocks-raw-handling.test.js.snap +++ b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap @@ -14,6 +14,8 @@ exports[`Blocks raw handling pasteHandler google-docs-table 1`] = `"


exports[`Blocks raw handling pasteHandler google-docs-table-with-comments 1`] = `"



One
Two
Three
1
2
3
I
II
III"`; +exports[`Blocks raw handling pasteHandler google-docs-table-with-lists 1`] = `"

1. Owner
Daniel
Notes:
“Issues” are topics and agenda items anyone can bring to the meeting.
Nested
IDS stands for “Identify, Discuss, Solve”.
Testing new line
This is nested
This is even more nested!
More stuff

Other ideas:
A new list
Nested again
"`; + exports[`Blocks raw handling pasteHandler google-docs-with-comments 1`] = `"This is a title

This is a heading

Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.

A
Bulleted
Indented
List

One
Two
Three




One
Two
Three
1
2
3
I
II
III





An image:



"`; exports[`Blocks raw handling pasteHandler gutenberg 1`] = `"Test"`; diff --git a/test/integration/fixtures/documents/google-docs-table-with-lists-in.html b/test/integration/fixtures/documents/google-docs-table-with-lists-in.html similarity index 100% rename from test/integration/fixtures/documents/google-docs-table-with-lists-in.html rename to test/integration/fixtures/documents/google-docs-table-with-lists-in.html diff --git a/test/integration/fixtures/documents/google-docs-table-with-lists-out.html b/test/integration/fixtures/documents/google-docs-table-with-lists-out.html index 5e50ce09ffde1..76cbe532e305f 100644 --- a/test/integration/fixtures/documents/google-docs-table-with-lists-out.html +++ b/test/integration/fixtures/documents/google-docs-table-with-lists-out.html @@ -1,3 +1,3 @@ -
1. OwnerDaniel
Notes:
1. “Issues” are topics and agenda items anyone can bring to the meeting.
- Nested
2. IDS stands for “Identify, Discuss, Solve”.
Testing new line
- This is nested
1. This is even more nested!
More stuff
Other ideas:
1. A new list
1. Nested again
+
1. OwnerDaniel
Notes:
1. “Issues” are topics and agenda items anyone can bring to the meeting.
- Nested
2. IDS stands for “Identify, Discuss, Solve”.
Testing new line
- This is nested
1. This is even more nested!
More stuff
Other ideas:
1. A new list
1. Nested again