diff --git a/blocks/api/paste/create-unwrapper.js b/blocks/api/paste/create-unwrapper.js index 277576a8cc0e05..3266cbe62e3d5f 100644 --- a/blocks/api/paste/create-unwrapper.js +++ b/blocks/api/paste/create-unwrapper.js @@ -13,7 +13,7 @@ function unwrap( node ) { parent.removeChild( node ); } -export default function( predicate ) { +export default function( predicate, after ) { return ( node ) => { if ( node.nodeType !== ELEMENT_NODE ) { return; @@ -23,6 +23,12 @@ export default function( predicate ) { return; } + const afterNode = after && after( node ); + + if ( afterNode ) { + node.appendChild( afterNode ); + } + unwrap( node ); }; } diff --git a/blocks/api/paste/index.js b/blocks/api/paste/index.js index a8721465f1edf5..ca826bb3bb009f 100644 --- a/blocks/api/paste/index.js +++ b/blocks/api/paste/index.js @@ -19,7 +19,9 @@ import msListConverter from './ms-list-converter'; import listMerger from './list-merger'; import imageCorrector from './image-corrector'; import blockquoteNormaliser from './blockquote-normaliser'; -import { deepFilter, isInvalidInline, isNotWhitelisted, isPlain, isInline } from './utils'; +import tableNormaliser from './table-normaliser'; +import inlineContentConverter from './inline-content-converter'; +import { deepFilterHTML, isInvalidInline, isNotWhitelisted, isPlain, isInline } from './utils'; import showdown from 'showdown'; export default function( { HTML, plainText, inline } ) { @@ -34,16 +36,17 @@ export default function( { HTML, plainText, inline } ) { const converter = new showdown.Converter(); converter.setOption( 'noHeaderId', true ); + converter.setOption( 'tables', true ); HTML = converter.makeHtml( plainText ); } else { // Context dependent filters. Needs to run before we remove nodes. - HTML = deepFilter( HTML, [ + HTML = deepFilterHTML( HTML, [ msListConverter, ] ); } - HTML = deepFilter( HTML, [ + HTML = deepFilterHTML( HTML, [ listMerger, imageCorrector, // Add semantic formatting before attributes are stripped. @@ -52,6 +55,8 @@ export default function( { HTML, plainText, inline } ) { commentRemover, createUnwrapper( ( node ) => isNotWhitelisted( node ) || ( inline && ! isInline( node ) ) ), blockquoteNormaliser, + tableNormaliser, + inlineContentConverter, ] ); // Inline paste. @@ -62,7 +67,7 @@ export default function( { HTML, plainText, inline } ) { return HTML; } - HTML = deepFilter( HTML, [ + HTML = deepFilterHTML( HTML, [ createUnwrapper( isInvalidInline ), ] ); diff --git a/blocks/api/paste/inline-content-converter.js b/blocks/api/paste/inline-content-converter.js new file mode 100644 index 00000000000000..1d36ebc8fb4650 --- /dev/null +++ b/blocks/api/paste/inline-content-converter.js @@ -0,0 +1,27 @@ +/** + * Browser dependencies + */ +const { ELEMENT_NODE } = window.Node; + +/** + * Internal dependencies + */ +import { isInlineWrapper, isInline, isAllowedBlock, deepFilterNodeList } from './utils'; +import createUnwrapper from './create-unwrapper'; + +export default function( node, doc ) { + if ( node.nodeType !== ELEMENT_NODE ) { + return; + } + + if ( ! isInlineWrapper( node ) ) { + return; + } + + deepFilterNodeList( node.childNodes, [ + createUnwrapper( + ( childNode ) => ! isInline( childNode ) && ! isAllowedBlock( node, childNode ), + ( childNode ) => childNode.nextElementSibling && doc.createElement( 'BR' ) + ), + ], doc ); +} diff --git a/blocks/api/paste/readme.md b/blocks/api/paste/readme.md new file mode 100644 index 00000000000000..a921aab1a4f82f --- /dev/null +++ b/blocks/api/paste/readme.md @@ -0,0 +1,26 @@ +# Paste + +This folder contains all paste specific logic (filters, converters, normalisers...). Each module is tested on their own, and in addition we have some integration tests for frequently used editors. + +## Support table + +| Source | Formatting | Headings | Lists | Image | Separator | Table | +| ---------------- | ---------- | -------- | ----- | ----- | --------- | ----- | +| Google Docs | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Apple Pages | ✓ | ✘ [1] | ✓ | ✘ [1] | n/a | ✓ | +| MS Word | ✓ | ✓ | ✓ | ✘ [2] | n/a | ✓ | +| MS Word Online | ✓ | ✘ [3] | ✓ | ✓ | n/a | ✓ | +| Markdown | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Legacy WordPress | ✓ | ✓ | ✓ | … [4] | ✓ | ✓ | +| Web | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | + + +1. Apple Pages does not pass heading and image information. +2. MS Word only provides a local file path, which cannot be accessed in JavaScript for security reasons. +3. Still to do for MS Word Online. +4. For caption and gallery shortcodes, see #2874. + +## Other notable capabilities + +* Filters out analytics trackers in the form of images. +* Direct image data pasting coming soon. diff --git a/blocks/api/paste/table-normaliser.js b/blocks/api/paste/table-normaliser.js new file mode 100644 index 00000000000000..34ee6ed666faf2 --- /dev/null +++ b/blocks/api/paste/table-normaliser.js @@ -0,0 +1,18 @@ +/** + * Browser dependencies + */ +const { TEXT_NODE } = window.Node; + +export default function( node ) { + if ( node.nodeType !== TEXT_NODE ) { + return; + } + + const parentNode = node.parentNode; + + if ( [ 'TR', 'TBODY', 'THEAD', 'TFOOT', 'TABLE' ].indexOf( parentNode.nodeName ) === -1 ) { + return; + } + + parentNode.removeChild( node ); +} diff --git a/blocks/api/paste/test/blockquote-normaliser.js b/blocks/api/paste/test/blockquote-normaliser.js index 1b0d8bec7d832e..151b545f578628 100644 --- a/blocks/api/paste/test/blockquote-normaliser.js +++ b/blocks/api/paste/test/blockquote-normaliser.js @@ -7,12 +7,12 @@ import { equal } from 'assert'; * Internal dependencies */ import blockquoteNormaliser from '../blockquote-normaliser'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'blockquoteNormaliser', () => { it( 'should normalise blockquote', () => { const input = '
test'; const output = '
'; - equal( deepFilter( input, [ blockquoteNormaliser ] ), output ); + equal( deepFilterHTML( input, [ blockquoteNormaliser ] ), output ); } ); } ); diff --git a/blocks/api/paste/test/comment-remover.js b/blocks/api/paste/test/comment-remover.js index eaae0ab64502e4..91222acfdd8a79 100644 --- a/blocks/api/paste/test/comment-remover.js +++ b/blocks/api/paste/test/comment-remover.js @@ -7,14 +7,14 @@ import { equal } from 'assert'; * Internal dependencies */ import commentRemover from '../comment-remover'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; -describe( 'stripWrappers', () => { +describe( 'commentRemover', () => { it( 'should remove comments', () => { - equal( deepFilter( '', [ commentRemover ] ), '' ); + equal( deepFilterHTML( '', [ commentRemover ] ), '' ); } ); it( 'should deep remove comments', () => { - equal( deepFilter( 'test
test
', [ commentRemover ] ), 'test
' ); + equal( deepFilterHTML( 'test
', [ commentRemover ] ), 'test
' ); } ); } ); diff --git a/blocks/api/paste/test/create-unwrapper.js b/blocks/api/paste/test/create-unwrapper.js index 2cea35c055fd61..772ccff0d2c9e1 100644 --- a/blocks/api/paste/test/create-unwrapper.js +++ b/blocks/api/paste/test/create-unwrapper.js @@ -7,28 +7,36 @@ import { equal } from 'assert'; * Internal dependencies */ import createUnwrapper from '../create-unwrapper'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; const unwrapper = createUnwrapper( ( node ) => node.nodeName === 'SPAN' ); +const unwrapperWithAfter = createUnwrapper( + ( node ) => node.nodeName === 'P', + () => document.createElement( 'BR' ) +); -describe( 'stripWrappers', () => { +describe( 'createUnwrapper', () => { it( 'should remove spans', () => { - equal( deepFilter( 'test', [ unwrapper ] ), 'test' ); + equal( deepFilterHTML( 'test', [ unwrapper ] ), 'test' ); } ); it( 'should remove wrapped spans', () => { - equal( deepFilter( 'test
', [ unwrapper ] ), 'test
' ); + equal( deepFilterHTML( 'test
', [ unwrapper ] ), 'test
' ); } ); it( 'should remove spans with attributes', () => { - equal( deepFilter( 'test
', [ unwrapper ] ), 'test
' ); + equal( deepFilterHTML( 'test
', [ unwrapper ] ), 'test
' ); } ); it( 'should remove nested spans', () => { - equal( deepFilter( 'test
', [ unwrapper ] ), 'test
' ); + equal( deepFilterHTML( 'test
', [ unwrapper ] ), 'test
' ); } ); it( 'should remove spans, but preserve nested structure', () => { - equal( deepFilter( 'test test
', [ unwrapper ] ), 'test test
' ); + equal( deepFilterHTML( 'test test
', [ unwrapper ] ), 'test test
' ); + } ); + + it( 'should remove paragraphs and insert line break', () => { + equal( deepFilterHTML( 'test
', [ unwrapperWithAfter ] ), 'testtest
test
This is a title
-This is a heading
-This is a paragraph with a link.
-This is a title
+This is a heading
+This is a paragraph with a link.
+An Image:
-
+ One + |
+
+ Two + |
+
+ Three + |
+
+ 1 + |
+
+ 2 + |
+
+ 3 + |
+
+ I + |
+
+ II + |
+
+ III + |
+
An image:
++ One + | ++ Two + | ++ Three + | +
+ 1 + | ++ 2 + | ++ 3 + | +
+ I + | ++ II + | ++ III + | +
An Image:
+An image:
diff --git a/blocks/api/paste/test/integration/google-docs-in.html b/blocks/api/paste/test/integration/google-docs-in.html index b0b025cac268d2..f69199a6d85fb0 100644 --- a/blocks/api/paste/test/integration/google-docs-in.html +++ b/blocks/api/paste/test/integration/google-docs-in.html @@ -1 +1 @@ -This is a title
This is a paragraph with a link.
A
Bulleted
Indented
List
One
Two
Three
An image:
+
This is a title
This is a paragraph with a link.
A
Bulleted
Indented
List
One
Two
Three
One | Two | Three |
1 | 2 | 3 |
I | II | III |
An image:
diff --git a/blocks/api/paste/test/integration/google-docs-out.html b/blocks/api/paste/test/integration/google-docs-out.html index 17d842698482b1..f9b95eb27ed4e1 100644 --- a/blocks/api/paste/test/integration/google-docs-out.html +++ b/blocks/api/paste/test/integration/google-docs-out.html @@ -12,37 +12,49 @@
A
-Bulleted
-Indented
-List
-One
-Two
-Three
-One | +Two | +Three | +
1 | +2 | +3 | +
I | +II | +III | +
An image:
+ One |
+
+ Two |
+
+ Three |
+
+ 1 |
+
+ 2 |
+
+ 3 |
+
+ I |
+
+ II |
+
+ III |
+
An image:
This is a heading
This is a paragraph with a link.
A
Bulleted
Indented
List
One
Two
Three
An image:
This is a heading
This is a paragraph with a link.
A
Bulleted
Indented
List
One
Two
Three
One | Two | Three |
1 | 2 | 3 |
I | II | III |
An image:
This is a heading
+This is a heading
@@ -8,35 +8,43 @@A
-Bulleted
-Indented
-List
-One
-Two
-Three
-One | +Two | +Three | +
1 | +2 | +3 | +
I | +II | +III | +
An image:
diff --git a/blocks/api/paste/test/integration/ms-word-out.html b/blocks/api/paste/test/integration/ms-word-out.html index 612d8bdfd3d133..81c43fd7fa0d13 100644 --- a/blocks/api/paste/test/integration/ms-word-out.html +++ b/blocks/api/paste/test/integration/ms-word-out.html @@ -40,6 +40,46 @@+ One + | ++ Two + | ++ Three + | +
+ 1 + | ++ 2 + | ++ 3 + | +
+ I + | ++ II + | ++ III + | +
An image:
diff --git a/blocks/api/paste/test/list-merger.js b/blocks/api/paste/test/list-merger.js index e36d1c572689f8..a82c6088a4bdff 100644 --- a/blocks/api/paste/test/list-merger.js +++ b/blocks/api/paste/test/list-merger.js @@ -7,27 +7,27 @@ import { equal } from 'assert'; * Internal dependencies */ import listMerger from '../list-merger'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'listMerger', () => { it( 'should merge lists', () => { const input = '* test
'; const output = '1 test
'; const output = '* test
'; const input3 = '* test
'; const output = '* test
'; const input4 = '* test
'; const output = 'test
', [ stripAttributes ] ), 'test
' ); + equal( deepFilterHTML( 'test
', [ stripAttributes ] ), 'test
' ); } ); it( 'should remove multiple attributes', () => { - equal( deepFilter( 'test
', [ stripAttributes ] ), 'test
' ); + equal( deepFilterHTML( 'test
', [ stripAttributes ] ), 'test
' ); } ); it( 'should deep remove attributes', () => { - equal( deepFilter( 'test test
', [ stripAttributes ] ), 'test test
' ); + equal( deepFilterHTML( 'test test
', [ stripAttributes ] ), 'test test
' ); } ); it( 'should remove data-* attributes', () => { - equal( deepFilter( 'test
', [ stripAttributes ] ), 'test
' ); + equal( deepFilterHTML( 'test
', [ stripAttributes ] ), 'test
' ); } ); it( 'should keep some attributes', () => { - equal( deepFilter( 'test', [ stripAttributes ] ), 'test' ); + equal( deepFilterHTML( 'test', [ stripAttributes ] ), 'test' ); } ); } ); diff --git a/blocks/api/paste/test/table-normaliser.js b/blocks/api/paste/test/table-normaliser.js new file mode 100644 index 00000000000000..27295ba22bcc40 --- /dev/null +++ b/blocks/api/paste/test/table-normaliser.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { equal } from 'assert'; + +/** + * Internal dependencies + */ +import tableNormaliser from '../table-normaliser'; +import { deepFilterHTML } from '../utils'; + +describe( 'tableNormaliser', () => { + it( 'should remove invalid text nodes in table', () => { + equal( + deepFilterHTML( '\n\n | \n
\n |