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 = '

test

'; - 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

', [ 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 ] ), 'test
' ); } ); } ); diff --git a/blocks/api/paste/test/formatting-transformer.js b/blocks/api/paste/test/formatting-transformer.js index 2cc57e43b836cc..4389a3ba26176a 100644 --- a/blocks/api/paste/test/formatting-transformer.js +++ b/blocks/api/paste/test/formatting-transformer.js @@ -7,18 +7,18 @@ import { equal } from 'assert'; * Internal dependencies */ import formattingTransformer from '../formatting-transformer'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'formattingTransformer', () => { it( 'should transform font weight', () => { - equal( deepFilter( 'test', [ formattingTransformer ] ), 'test' ); + equal( deepFilterHTML( 'test', [ formattingTransformer ] ), 'test' ); } ); it( 'should transform numeric font weight', () => { - equal( deepFilter( 'test', [ formattingTransformer ] ), 'test' ); + equal( deepFilterHTML( 'test', [ formattingTransformer ] ), 'test' ); } ); it( 'should transform font style', () => { - equal( deepFilter( 'test', [ formattingTransformer ] ), 'test' ); + equal( deepFilterHTML( 'test', [ formattingTransformer ] ), 'test' ); } ); } ); diff --git a/blocks/api/paste/test/image-corrector.js b/blocks/api/paste/test/image-corrector.js index 40a899fc9ea4bb..93f11687e7c486 100644 --- a/blocks/api/paste/test/image-corrector.js +++ b/blocks/api/paste/test/image-corrector.js @@ -7,18 +7,18 @@ import { equal } from 'assert'; * Internal dependencies */ import imageCorrector from '../image-corrector'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'imageCorrector', () => { it( 'should correct image source', () => { const input = ''; const output = ''; - equal( deepFilter( input, [ imageCorrector ] ), output ); + equal( deepFilterHTML( input, [ imageCorrector ] ), output ); } ); it( 'should remove trackers', () => { const input = ''; const output = ''; - equal( deepFilter( input, [ imageCorrector ] ), output ); + equal( deepFilterHTML( input, [ imageCorrector ] ), output ); } ); } ); diff --git a/blocks/api/paste/test/inline-content-converter.js b/blocks/api/paste/test/inline-content-converter.js new file mode 100644 index 00000000000000..ee5decf487aad9 --- /dev/null +++ b/blocks/api/paste/test/inline-content-converter.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { equal } from 'assert'; + +/** + * Internal dependencies + */ +import inlineContentConverter from '../inline-content-converter'; +import { deepFilterHTML } from '../utils'; + +describe( 'inlineContentConverter', () => { + it( 'should remove non-inline content from inline wrapper', () => { + equal( + deepFilterHTML( '

test

test

', [ inlineContentConverter ] ), + '
test
test
' + ); + } ); +} ); diff --git a/blocks/api/paste/test/integration/apple-in.html b/blocks/api/paste/test/integration/apple-in.html index d181343a27e583..fb2ae65e3ae9d4 100644 --- a/blocks/api/paste/test/integration/apple-in.html +++ b/blocks/api/paste/test/integration/apple-in.html @@ -1,27 +1,61 @@ - - - -

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.

+


-


+


    -
  1. One
  2. -
  3. Two
  4. -
  5. Three
  6. +
  7. One
  8. +
  9. Two
  10. +
  11. Three
-


-

An Image:

-


-


+


+ + + + + + + + + + + + + + + + + + +
+

One

+
+

Two

+
+

Three

+
+

1

+
+

2

+
+

3

+
+

I

+
+

II

+
+

III

+
+


+

An image:

+


diff --git a/blocks/api/paste/test/integration/apple-out.html b/blocks/api/paste/test/integration/apple-out.html index 6efb087cf165c0..211098e408ecc9 100644 --- a/blocks/api/paste/test/integration/apple-out.html +++ b/blocks/api/paste/test/integration/apple-out.html @@ -29,6 +29,46 @@ + + + + + + + + + + + + + + + + + + + +
+ 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 heading


This is a paragraph with a link.



  1. One

  2. Two

  3. Three


An image:


+

This is a title


This is a heading


This is a paragraph with a link.



  1. One

  2. Two

  3. 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 @@

This is a heading

    -
  1. -

    One

    -
  2. -
  3. -

    Two

    -
  4. -
  5. -

    Three

    -
  6. +
  7. One
  8. +
  9. Two
  10. +
  11. Three
+ + + + + + + + + + + + + + + + + + + +
OneTwoThree
123
IIIIII
+ + + +
+ +

An image:

diff --git a/blocks/api/paste/test/integration/ms-word-in.html b/blocks/api/paste/test/integration/ms-word-in.html index 22fe7db6677058..d65299287e642e 100644 --- a/blocks/api/paste/test/integration/ms-word-in.html +++ b/blocks/api/paste/test/integration/ms-word-in.html @@ -73,6 +73,100 @@

This is a heading level 2 + + + + + + + + + + + + + + + + +
+

One

+
+

Two

+
+

Three

+
+

1

+
+

2

+
+

3

+
+

I

+
+

II

+
+

III

+
+

 

An image:

diff --git a/blocks/api/paste/test/integration/ms-word-online-in.html b/blocks/api/paste/test/integration/ms-word-online-in.html index e067d52847980c..4f36dba345d327 100644 --- a/blocks/api/paste/test/integration/ms-word-online-in.html +++ b/blocks/api/paste/test/integration/ms-word-online-in.html @@ -1 +1 @@ -

This is a heading 

This is a paragraph with a link. 

  • A 

  • Bulleted 

  • Indented 

  • List 

 

  1. One 

  1. Two 

  1. Three 

An image: 

 

+

This is a heading 

This is a paragraph with a link. 

  • A 

  • Bulleted 

  • Indented 

  • List 

 

  1. One 

  1. Two 

  1. Three 

One 

Two 

Three 

1 

2 

3 

I 

II 

III 

 

An image: 

 

diff --git a/blocks/api/paste/test/integration/ms-word-online-out.html b/blocks/api/paste/test/integration/ms-word-online-out.html index d87eaed98aacf5..2a7473c70e043a 100644 --- a/blocks/api/paste/test/integration/ms-word-online-out.html +++ b/blocks/api/paste/test/integration/ms-word-online-out.html @@ -1,5 +1,5 @@ -

This is a heading 

+

This is a heading 

@@ -8,35 +8,43 @@
    -
  • -

    -
  • -
  • -

    Bulleted 

    -
  • -
  • -

    Indented 

    -
  • -
  • -

    List 

    -
  • +
  • +
  • Bulleted 
  • +
  • Indented 
  • +
  • List 
    -
  1. -

    One 

    -
  2. -
  3. -

    Two 

    -
  4. -
  5. -

    Three 

    -
  6. +
  7. One 
  8. +
  9. Two 
  10. +
  11. Three 
+ + + + + + + + + + + + + + + + + + + +
One Two Three 
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 @@

This is a heading level 2

+ + + + + + + + + + + + + + + + + + + +
+ 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 = '
  • one
  • two
'; const output = '
  • one
  • two
'; - equal( deepFilter( input, [ listMerger ] ), output ); + equal( deepFilterHTML( input, [ listMerger ] ), output ); } ); it( 'should not merge lists if it has more than one item', () => { const input = '
  • one
  • two
  • three
'; - equal( deepFilter( input, [ listMerger ] ), input ); + equal( deepFilterHTML( input, [ listMerger ] ), input ); } ); it( 'should not merge list if the type is different', () => { const input = '
  • one
  1. two
'; - equal( deepFilter( input, [ listMerger ] ), input ); + equal( deepFilterHTML( input, [ listMerger ] ), input ); } ); it( 'should not merge lists if there is something in between', () => { const input = '
  • one

  • two
'; - equal( deepFilter( input, [ listMerger ] ), input ); + equal( deepFilterHTML( input, [ listMerger ] ), input ); } ); } ); diff --git a/blocks/api/paste/test/ms-list-converter.js b/blocks/api/paste/test/ms-list-converter.js index f5d71f161a1fad..96ec1a37964fdb 100644 --- a/blocks/api/paste/test/ms-list-converter.js +++ b/blocks/api/paste/test/ms-list-converter.js @@ -7,19 +7,19 @@ import { equal } from 'assert'; * Internal dependencies */ import msListConverter from '../ms-list-converter'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'msListConverter', () => { it( 'should convert unordered list', () => { const input = '

* test

'; const output = '
  • test
'; - equal( deepFilter( input, [ msListConverter ] ), output ); + equal( deepFilterHTML( input, [ msListConverter ] ), output ); } ); it( 'should convert ordered list', () => { const input = '

1 test

'; const output = '
  1. test
'; - equal( deepFilter( input, [ msListConverter ] ), output ); + equal( deepFilterHTML( input, [ msListConverter ] ), output ); } ); it( 'should convert indented list', () => { @@ -27,7 +27,7 @@ describe( 'msListConverter', () => { const input2 = '

* test

'; const input3 = '

* test

'; const output = '
  • test
    • test
  • test
'; - equal( deepFilter( input1 + input2 + input3, [ msListConverter ] ), output ); + equal( deepFilterHTML( input1 + input2 + input3, [ msListConverter ] ), output ); } ); it( 'should convert deep indented list', () => { @@ -36,6 +36,6 @@ describe( 'msListConverter', () => { const input3 = '

* test

'; const input4 = '

* test

'; const output = '
  • test
    • test
      • test
  • test
'; - equal( deepFilter( input1 + input2 + input3 + input4, [ msListConverter ] ), output ); + equal( deepFilterHTML( input1 + input2 + input3 + input4, [ msListConverter ] ), output ); } ); } ); diff --git a/blocks/api/paste/test/strip-attributes.js b/blocks/api/paste/test/strip-attributes.js index aa07ff2f7d6887..43dc12511364df 100644 --- a/blocks/api/paste/test/strip-attributes.js +++ b/blocks/api/paste/test/strip-attributes.js @@ -7,26 +7,26 @@ import { equal } from 'assert'; * Internal dependencies */ import stripAttributes from '../strip-attributes'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'stripAttributes', () => { it( 'should remove attributes', () => { - equal( deepFilter( '

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\n\n\n
\n
\n', [ tableNormaliser ] ), + '\n
\n
\n' + ); + } ); +} ); diff --git a/blocks/api/paste/test/utils.js b/blocks/api/paste/test/utils.js index 3e86e9791cabfd..411ff1a44b8a56 100644 --- a/blocks/api/paste/test/utils.js +++ b/blocks/api/paste/test/utils.js @@ -7,15 +7,15 @@ import { equal } from 'assert'; * Internal dependencies */ import createUnwrapper from '../create-unwrapper'; -import { deepFilter, isEmpty, isInvalidInline } from '../utils'; +import { deepFilterHTML, isEmpty, isInvalidInline, isPlain } from '../utils'; const spanUnwrapper = createUnwrapper( ( node ) => node.nodeName === 'SPAN' ); const inlineUnwrapper = createUnwrapper( ( node ) => node.nodeName === 'EM' ); -describe( 'deepFilter', () => { +describe( 'deepFilterHTML', () => { it( 'should not error', () => { - equal( deepFilter( 'test', [ spanUnwrapper, inlineUnwrapper ] ), 'test' ); - equal( deepFilter( 'test', [ spanUnwrapper, inlineUnwrapper ] ), 'test' ); + equal( deepFilterHTML( 'test', [ spanUnwrapper, inlineUnwrapper ] ), 'test' ); + equal( deepFilterHTML( 'test', [ spanUnwrapper, inlineUnwrapper ] ), 'test' ); } ); } ); @@ -78,3 +78,17 @@ describe( 'isInvalidInline', () => { equal( isInvalidInlineHTML( 'test' ), false ); } ); } ); + +describe( 'isPlain', () => { + it( 'should return true for plain text', () => { + equal( isPlain( 'test' ), true ); + } ); + + it( 'should return true for only line breaks', () => { + equal( isPlain( 'test
test' ), true ); + } ); + + it( 'should return false for formatted text', () => { + equal( isPlain( 'test' ), false ); + } ); +} ); diff --git a/blocks/api/paste/utils.js b/blocks/api/paste/utils.js index c48e6add8d0495..1d17199c97bad3 100644 --- a/blocks/api/paste/utils.js +++ b/blocks/api/paste/utils.js @@ -4,43 +4,47 @@ const { ELEMENT_NODE, TEXT_NODE } = window.Node; const inlineWhitelist = { - strong: [], - em: [], - del: [], - ins: [], - a: [ 'href' ], - code: [], - abbr: [ 'title' ], - sub: [], - sup: [], - br: [], + strong: {}, + em: {}, + del: {}, + ins: {}, + a: { attributes: [ 'href' ] }, + code: {}, + abbr: { attributes: [ 'title' ] }, + sub: {}, + sup: {}, + br: {}, +}; + +const inlineWrapperWhiteList = { + figcaption: {}, + h1: {}, + h2: {}, + h3: {}, + h4: {}, + h5: {}, + h6: {}, + p: { children: [ 'img' ] }, + li: { children: [ 'ul', 'ol', 'li' ] }, + pre: {}, + td: {}, + th: {}, }; const whitelist = { ...inlineWhitelist, - img: [ 'src', 'alt' ], - figure: [], - figcaption: [], - h1: [], - h2: [], - h3: [], - h4: [], - h5: [], - h6: [], - p: [], - blockquote: [], - hr: [], - ul: [], - ol: [ 'type' ], - li: [], - pre: [], - table: [], - thead: [], - tfoot: [], - tbody: [], - th: [], - tr: [], - td: [], + ...inlineWrapperWhiteList, + img: { attributes: [ 'src', 'alt' ] }, + figure: {}, + blockquote: {}, + hr: {}, + ul: {}, + ol: { attributes: [ 'type' ] }, + table: {}, + thead: {}, + tfoot: {}, + tbody: {}, + tr: {}, }; export function isWhitelisted( element ) { @@ -52,13 +56,32 @@ export function isNotWhitelisted( element ) { } export function isAttributeWhitelisted( tag, attribute ) { - return whitelist[ tag ] && whitelist[ tag ].indexOf( attribute ) !== -1; + return ( + whitelist[ tag ] && + whitelist[ tag ].attributes && + whitelist[ tag ].attributes.indexOf( attribute ) !== -1 + ); } export function isInline( node ) { return !! inlineWhitelist[ node.nodeName.toLowerCase() ]; } +export function isInlineWrapper( node ) { + return !! inlineWrapperWhiteList[ node.nodeName.toLowerCase() ]; +} + +export function isAllowedBlock( parentNode, node ) { + const parentNodeTag = parentNode.nodeName.toLowerCase(); + const nodeTag = node.nodeName.toLowerCase(); + + return ( + whitelist[ parentNodeTag ] && + whitelist[ parentNodeTag ].children && + whitelist[ parentNodeTag ].children.indexOf( nodeTag ) !== -1 + ); +} + export function isInvalidInline( element ) { if ( ! isInline( element ) ) { return false; @@ -125,12 +148,19 @@ export function isPlain( HTML ) { doc.body.normalize(); // If it's plain text, there should only be one node left. - return doc.body.childNodes.length === 1; + return doc.body.childNodes.length === 1 && doc.body.firstChild.nodeType === TEXT_NODE; } -function deepFilterHelper( nodeList, filters, doc ) { +/** + * Given node filters, deeply filters and mutates a NodeList. + * + * @param {NodeList} nodeList The nodeList to filter. + * @param {Array} filters An array of functions that can mutate with the provided node. + * @param {Document} doc The document of the nodeList. + */ +export function deepFilterNodeList( nodeList, filters, doc ) { Array.from( nodeList ).forEach( ( node ) => { - deepFilterHelper( node.childNodes, filters, doc ); + deepFilterNodeList( node.childNodes, filters, doc ); filters.forEach( ( filter ) => { // Make sure the node is still attached to the document. @@ -138,17 +168,24 @@ function deepFilterHelper( nodeList, filters, doc ) { return; } - filter( node ); + filter( node, doc ); } ); } ); } -export function deepFilter( HTML, filters = [] ) { +/** + * Given node filters, deeply filters HTML tags. + * + * @param {String} HTML The HTML to filter. + * @param {Array} filters An array of functions that can mutate with the provided node. + * @return {String} The filtered HTML. + */ +export function deepFilterHTML( HTML, filters = [] ) { const doc = document.implementation.createHTMLDocument( '' ); doc.body.innerHTML = HTML; - deepFilterHelper( doc.body.childNodes, filters, doc ); + deepFilterNodeList( doc.body.childNodes, filters, doc ); return doc.body.innerHTML; }