Skip to content

Commit

Permalink
Avoid cloning nodes and clunky tests by passing pasted HTML string
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Aug 20, 2017
1 parent 4a99994 commit 5125ee1
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 109 deletions.
12 changes: 8 additions & 4 deletions blocks/api/paste/index.js
Expand Up @@ -13,19 +13,23 @@ import normaliseBlocks from './normalise-blocks';
import stripAttributes from './strip-attributes';
import stripWrappers from './strip-wrappers';

export default function( nodes ) {
export default function( HTML ) {
const prepare = flow( [
stripWrappers,
normaliseBlocks,
stripAttributes,
] );

const prepared = prepare( nodes );
const preparedHTML = prepare( HTML );

// Allows us to ask for this information when we get a report.
window.console.log( 'Processed HTML:\n\n', prepared.map( ( node ) => node.outerHTML ) );
window.console.log( 'Processed HTML piece:\n\n', HTML );

return prepared.map( ( node ) => {
const doc = document.implementation.createHTMLDocument( '' );

doc.body.innerHTML = preparedHTML;

return Array.from( doc.body.children ).map( ( node ) => {
const block = getBlockTypes().reduce( ( acc, blockType ) => {
if ( acc ) {
return acc;
Expand Down
20 changes: 8 additions & 12 deletions blocks/api/paste/normalise-blocks.js
Expand Up @@ -24,18 +24,14 @@ function isInline( node ) {
return inlineTags.indexOf( node.nodeName.toLowerCase() ) !== -1;
}

/**
* Normalises array nodes of any node type to an array of block level nodes.
*
* @param {Array} nodes Array of Nodes.
* @return {Array} Array of block level HTMLElements
*/
export default function( nodes ) {
const decu = document.createDocumentFragment();
const accu = document.createDocumentFragment();
export default function( HTML ) {
const decuDoc = document.implementation.createHTMLDocument( '' );
const accuDoc = document.implementation.createHTMLDocument( '' );

const decu = decuDoc.body;
const accu = accuDoc.body;

// A fragment is easier to work with.
nodes.forEach( node => decu.appendChild( node.cloneNode( true ) ) );
decu.innerHTML = HTML;

while ( decu.firstChild ) {
const node = decu.firstChild;
Expand Down Expand Up @@ -84,5 +80,5 @@ export default function( nodes ) {
}
}

return Array.from( accu.childNodes );
return accu.innerHTML;
}
14 changes: 7 additions & 7 deletions blocks/api/paste/strip-attributes.js
Expand Up @@ -4,18 +4,18 @@ const attributes = [
'id',
];

export default function( nodes ) {
const fragment = document.createDocumentFragment();
export default function( HTML ) {
const doc = document.implementation.createHTMLDocument( '' );

nodes.forEach( node => fragment.appendChild( node.cloneNode( true ) ) );
doc.body.innerHTML = HTML;

deepAttributeStrip( fragment.children );
deepAttributeStrip( doc.body.children );

return Array.from( fragment.childNodes );
return doc.body.innerHTML;
}

function deepAttributeStrip( nodes ) {
Array.from( nodes ).forEach( ( node ) => {
function deepAttributeStrip( nodeList ) {
Array.from( nodeList ).forEach( ( node ) => {
if ( node.hasAttributes() ) {
Array.from( node.attributes ).forEach( ( { name } ) => {
if ( attributes.indexOf( name ) !== -1 ) {
Expand Down
14 changes: 5 additions & 9 deletions blocks/api/paste/strip-wrappers.js
Expand Up @@ -21,18 +21,14 @@ function unwrap( node ) {
parent.removeChild( node );
}

/**
* @param {Array} nodes Array of nodes.
* @return {Array} Array of nodes without any SPANs or DIVs.
*/
export default function( nodes ) {
const fragment = document.createDocumentFragment();
export default function( HTML ) {
const doc = document.implementation.createHTMLDocument( '' );

nodes.forEach( node => fragment.appendChild( node.cloneNode( true ) ) );
doc.body.innerHTML = HTML;

const wrappers = fragment.querySelectorAll( tags );
const wrappers = doc.body.querySelectorAll( tags );

Array.from( wrappers ).forEach( ( wrapper ) => unwrap( wrapper ) );

return Array.from( fragment.childNodes );
return doc.body.innerHTML;
}
9 changes: 2 additions & 7 deletions blocks/api/paste/test/index.js
Expand Up @@ -11,11 +11,6 @@ import { registerBlockType, unregisterBlockType, setUnknownTypeHandlerName } fro
import { createBlock } from '../../factory';
import { children, prop } from '../../source';

function createNodes( HTML ) {
document.body.innerHTML = HTML;
return Array.from( document.body.childNodes );
}

describe( 'paste', () => {
beforeAll( () => {
registerBlockType( 'test/small', {
Expand Down Expand Up @@ -58,15 +53,15 @@ describe( 'paste', () => {
} );

it( 'should convert recognised pasted content', () => {
const pastedBlock = paste( createNodes( '<small>test</small>' ) )[ 0 ];
const pastedBlock = paste( '<small>test</small>' )[ 0 ];
const block = createBlock( 'test/small', { content: [ 'test' ] } );

equal( pastedBlock.name, block.name );
deepEqual( pastedBlock.attributes, block.attributes );
} );

it( 'should handle unknown pasted content', () => {
const pastedBlock = paste( createNodes( '<big>test</big>' ) )[ 0 ];
const pastedBlock = paste( '<big>test</big>' )[ 0 ];

equal( pastedBlock.name, 'test/unknown' );
equal( pastedBlock.attributes.content, '<big>test</big>' );
Expand Down
27 changes: 7 additions & 20 deletions blocks/api/paste/test/normalise-blocks.js
Expand Up @@ -8,43 +8,30 @@ import { equal } from 'assert';
*/
import normaliseBlocks from '../normalise-blocks';

function createNodes( HTML ) {
document.body.innerHTML = HTML;
return Array.from( document.body.childNodes );
}

function outerHTML( nodes ) {
return nodes.map( node => node.outerHTML ).join( '' );
}

function transform( HTML ) {
return outerHTML( normaliseBlocks( createNodes( HTML ) ) );
}

describe( 'normaliseBlocks', () => {
it( 'should convert double line breaks to paragraphs', () => {
equal( transform( 'test<br><br>test' ), '<p>test</p><p>test</p>' );
equal( normaliseBlocks( 'test<br><br>test' ), '<p>test</p><p>test</p>' );
} );

it( 'should not convert single line break to paragraphs', () => {
equal( transform( 'test<br>test' ), '<p>test<br>test</p>' );
equal( normaliseBlocks( 'test<br>test' ), '<p>test<br>test</p>' );
} );

it( 'should not add extra line at the start', () => {
equal( transform( 'test<br><br><br>test' ), '<p>test</p><p>test</p>' );
equal( transform( '<br>test<br><br>test' ), '<p>test</p><p>test</p>' );
equal( normaliseBlocks( 'test<br><br><br>test' ), '<p>test</p><p>test</p>' );
equal( normaliseBlocks( '<br>test<br><br>test' ), '<p>test</p><p>test</p>' );
} );

it( 'should preserve non-inline content', () => {
const HTML = '<p>test</p><div>test<br>test</div>';
equal( transform( HTML ), HTML );
equal( normaliseBlocks( HTML ), HTML );
} );

it( 'should remove empty paragraphs', () => {
equal( transform( '<p>&nbsp;</p>' ), '' );
equal( normaliseBlocks( '<p>&nbsp;</p>' ), '' );
} );

it( 'should wrap lose inline elements', () => {
equal( transform( '<a href="#">test</a>' ), '<p><a href="#">test</a></p>' );
equal( normaliseBlocks( '<a href="#">test</a>' ), '<p><a href="#">test</a></p>' );
} );
} );
23 changes: 5 additions & 18 deletions blocks/api/paste/test/strip-attributes.js
Expand Up @@ -8,37 +8,24 @@ import { equal } from 'assert';
*/
import stripAttributes from '../strip-attributes';

function createNodes( HTML ) {
document.body.innerHTML = HTML;
return Array.from( document.body.childNodes );
}

function outerHTML( nodes ) {
return nodes.map( node => node.outerHTML ).join( '' );
}

function transform( HTML ) {
return outerHTML( stripAttributes( createNodes( HTML ) ) );
}

describe( 'stripAttributes', () => {
it( 'should remove attributes', () => {
equal( transform( '<p class="test">test</p>' ), '<p>test</p>' );
equal( stripAttributes( '<p class="test">test</p>' ), '<p>test</p>' );
} );

it( 'should remove multiple attributes', () => {
equal( transform( '<p class="test" id="test">test</p>' ), '<p>test</p>' );
equal( stripAttributes( '<p class="test" id="test">test</p>' ), '<p>test</p>' );
} );

it( 'should deep remove attributes', () => {
equal( transform( '<p class="test">test <em id="test">test</em></p>' ), '<p>test <em>test</em></p>' );
equal( stripAttributes( '<p class="test">test <em id="test">test</em></p>' ), '<p>test <em>test</em></p>' );
} );

it( 'should remove data-* attributes', () => {
equal( transform( '<p data-reactid="1">test</p>' ), '<p>test</p>' );
equal( stripAttributes( '<p data-reactid="1">test</p>' ), '<p>test</p>' );
} );

it( 'should keep some attributes', () => {
equal( transform( '<a href="#keep">test</a>' ), '<a href="#keep">test</a>' );
equal( stripAttributes( '<a href="#keep">test</a>' ), '<a href="#keep">test</a>' );
} );
} );
32 changes: 5 additions & 27 deletions blocks/api/paste/test/strip-wrappers.js
Expand Up @@ -3,51 +3,29 @@
*/
import { equal } from 'assert';

/**
* WordPress dependencies
*/
import { ELEMENT_NODE } from 'utils/nodetypes';

/**
* Internal dependencies
*/
import stripWrappers from '../strip-wrappers';

function createNodes( HTML ) {
document.body.innerHTML = HTML;
return Array.from( document.body.childNodes );
}

function outerHTML( nodes ) {
return nodes.map( node =>
node.nodeType === ELEMENT_NODE ?
node.outerHTML :
node.nodeValue
).join( '' );
}

function transform( HTML ) {
return outerHTML( stripWrappers( createNodes( HTML ) ) );
}

describe( 'stripWrappers', () => {
it( 'should remove spans', () => {
equal( transform( '<span>test</span>' ), 'test' );
equal( stripWrappers( '<span>test</span>' ), 'test' );
} );

it( 'should remove wrapped spans', () => {
equal( transform( '<p><span>test</span></p>' ), '<p>test</p>' );
equal( stripWrappers( '<p><span>test</span></p>' ), '<p>test</p>' );
} );

it( 'should remove spans with attributes', () => {
equal( transform( '<p><span id="test">test</span></p>' ), '<p>test</p>' );
equal( stripWrappers( '<p><span id="test">test</span></p>' ), '<p>test</p>' );
} );

it( 'should remove nested spans', () => {
equal( transform( '<p><span><span>test</span></span></p>' ), '<p>test</p>' );
equal( stripWrappers( '<p><span><span>test</span></span></p>' ), '<p>test</p>' );
} );

it( 'should remove spans, but preserve nested structure', () => {
equal( transform( '<p><span><em>test</em> <em>test</em></span></p>' ), '<p><em>test</em> <em>test</em></p>' );
equal( stripWrappers( '<p><span><em>test</em> <em>test</em></span></p>' ), '<p><em>test</em> <em>test</em></p>' );
} );
} );
11 changes: 6 additions & 5 deletions blocks/editable/index.js
Expand Up @@ -180,9 +180,7 @@ export default class Editable extends Component {
}

onPastePostProcess( event ) {
// Allows us to ask for this information when we get a report.
window.console.log( 'MCE processed HTML:\n\n', event.node.innerHTML );

const HTML = event.node.innerHTML;
const childNodes = Array.from( event.node.childNodes );
const isBlockDelimiter = ( node ) =>
node.nodeType === 8 && /^ wp:/.test( node.nodeValue );
Expand All @@ -191,18 +189,21 @@ export default class Editable extends Component {
const isBlockPart = ( node ) =>
isDoubleBR( node ) || this.editor.dom.isBlock( node );

// Allows us to ask for this information when we get a report.
window.console.log( 'MCE processed HTML:\n\n', HTML );

// If there's no `onSplit` prop, content will later be converted to
// inline content.
if ( this.props.onSplit ) {
let blocks = [];

// Internal paste, so parse.
if ( childNodes.some( isBlockDelimiter ) ) {
blocks = parse( event.node.innerHTML.replace( /<meta[^>]+>/, '' ) );
blocks = parse( HTML.replace( /<meta[^>]+>/, '' ) );
// External paste with block level content, so attempt to assign
// blocks.
} else if ( childNodes.some( isBlockPart ) ) {
blocks = pasteHandler( childNodes );
blocks = pasteHandler( HTML.replace( /<meta[^>]+>/, '' ) );
}

if ( blocks.length ) {
Expand Down

0 comments on commit 5125ee1

Please sign in to comment.