Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid cloning nodes and clunky tests by passing pasted HTML string #2479

Merged
merged 1 commit into from Aug 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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