Skip to content

Commit

Permalink
Paste: add table support
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed Oct 10, 2017
1 parent ca7a4d4 commit dc472ce
Show file tree
Hide file tree
Showing 25 changed files with 559 additions and 152 deletions.
8 changes: 7 additions & 1 deletion blocks/api/paste/create-unwrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +23,12 @@ export default function( predicate ) {
return;
}

const afterNode = after && after( node );

if ( afterNode ) {
node.appendChild( afterNode );
}

unwrap( node );
};
}
13 changes: 9 additions & 4 deletions blocks/api/paste/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } ) {
Expand All @@ -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.
Expand All @@ -52,6 +55,8 @@ export default function( { HTML, plainText, inline } ) {
commentRemover,
createUnwrapper( ( node ) => isNotWhitelisted( node ) || ( inline && ! isInline( node ) ) ),
blockquoteNormaliser,
tableNormaliser,
inlineContentConverter,
] );

// Inline paste.
Expand All @@ -62,7 +67,7 @@ export default function( { HTML, plainText, inline } ) {
return HTML;
}

HTML = deepFilter( HTML, [
HTML = deepFilterHTML( HTML, [
createUnwrapper( isInvalidInline ),
] );

Expand Down
27 changes: 27 additions & 0 deletions blocks/api/paste/inline-content-converter.js
Original file line number Diff line number Diff line change
@@ -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 );
}
26 changes: 26 additions & 0 deletions blocks/api/paste/readme.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions blocks/api/paste/table-normaliser.js
Original file line number Diff line number Diff line change
@@ -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 );
}
4 changes: 2 additions & 2 deletions blocks/api/paste/test/blockquote-normaliser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<blockquote>test</blockquote>';
const output = '<blockquote><p>test</p></blockquote>';
equal( deepFilter( input, [ blockquoteNormaliser ] ), output );
equal( deepFilterHTML( input, [ blockquoteNormaliser ] ), output );
} );
} );
8 changes: 4 additions & 4 deletions blocks/api/paste/test/comment-remover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( '<!-- test -->', [ commentRemover ] ), '' );
equal( deepFilterHTML( '<!-- test -->', [ commentRemover ] ), '' );
} );

it( 'should deep remove comments', () => {
equal( deepFilter( '<p>test<!-- test --></p>', [ commentRemover ] ), '<p>test</p>' );
equal( deepFilterHTML( '<p>test<!-- test --></p>', [ commentRemover ] ), '<p>test</p>' );
} );
} );
22 changes: 15 additions & 7 deletions blocks/api/paste/test/create-unwrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( '<span>test</span>', [ unwrapper ] ), 'test' );
equal( deepFilterHTML( '<span>test</span>', [ unwrapper ] ), 'test' );
} );

it( 'should remove wrapped spans', () => {
equal( deepFilter( '<p><span>test</span></p>', [ unwrapper ] ), '<p>test</p>' );
equal( deepFilterHTML( '<p><span>test</span></p>', [ unwrapper ] ), '<p>test</p>' );
} );

it( 'should remove spans with attributes', () => {
equal( deepFilter( '<p><span id="test">test</span></p>', [ unwrapper ] ), '<p>test</p>' );
equal( deepFilterHTML( '<p><span id="test">test</span></p>', [ unwrapper ] ), '<p>test</p>' );
} );

it( 'should remove nested spans', () => {
equal( deepFilter( '<p><span><span>test</span></span></p>', [ unwrapper ] ), '<p>test</p>' );
equal( deepFilterHTML( '<p><span><span>test</span></span></p>', [ unwrapper ] ), '<p>test</p>' );
} );

it( 'should remove spans, but preserve nested structure', () => {
equal( deepFilter( '<p><span><em>test</em> <em>test</em></span></p>', [ unwrapper ] ), '<p><em>test</em> <em>test</em></p>' );
equal( deepFilterHTML( '<p><span><em>test</em> <em>test</em></span></p>', [ unwrapper ] ), '<p><em>test</em> <em>test</em></p>' );
} );

it( 'should remove paragraphs and insert line break', () => {
equal( deepFilterHTML( '<p>test</p>', [ unwrapperWithAfter ] ), 'test<br>' );
} );
} );
8 changes: 4 additions & 4 deletions blocks/api/paste/test/formatting-transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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( '<span style="font-weight:bold">test</span>', [ formattingTransformer ] ), '<strong>test</strong>' );
equal( deepFilterHTML( '<span style="font-weight:bold">test</span>', [ formattingTransformer ] ), '<strong>test</strong>' );
} );

it( 'should transform numeric font weight', () => {
equal( deepFilter( '<span style="font-weight:700">test</span>', [ formattingTransformer ] ), '<strong>test</strong>' );
equal( deepFilterHTML( '<span style="font-weight:700">test</span>', [ formattingTransformer ] ), '<strong>test</strong>' );
} );

it( 'should transform font style', () => {
equal( deepFilter( '<span style="font-style:italic">test</span>', [ formattingTransformer ] ), '<em>test</em>' );
equal( deepFilterHTML( '<span style="font-style:italic">test</span>', [ formattingTransformer ] ), '<em>test</em>' );
} );
} );
6 changes: 3 additions & 3 deletions blocks/api/paste/test/image-corrector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<img src="file:LOW-RES.png">';
const output = '<img src="">';
equal( deepFilter( input, [ imageCorrector ] ), output );
equal( deepFilterHTML( input, [ imageCorrector ] ), output );
} );

it( 'should remove trackers', () => {
const input = '<img src="" height="1" width="1">';
const output = '';
equal( deepFilter( input, [ imageCorrector ] ), output );
equal( deepFilterHTML( input, [ imageCorrector ] ), output );
} );
} );
19 changes: 19 additions & 0 deletions blocks/api/paste/test/inline-content-converter.js
Original file line number Diff line number Diff line change
@@ -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( '<figcaption><p>test</p><p>test</p></figcaption>', [ inlineContentConverter ] ),
'<figcaption>test<br>test</figcaption>'
);
} );
} );
76 changes: 55 additions & 21 deletions blocks/api/paste/test/integration/apple-in.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,61 @@
<!-- Apple Pages and Apple Notes have the same content structure. -->
<!-- We do not get any heading level info. The classes are useless. -->
<!-- Images are not included either. -->
<p class="p1"><b>This is a </b>title</p>
<p class="p2"><br></p>
<p class="p3"><b>This is a <i>heading</i></b></p>
<p class="p2"><i></i><br></p>
<p class="p4">This is a <b>paragraph</b> with a <a href="https://w.org"><span class="s1">link</span></a>.</p>
<p class="p2"><br></p>
<p class="p1"><span class="s1"><b>This is a </b>title</span></p>
<p class="p2"><span class="s1"></span><br></p>
<p class="p3"><span class="s1"><b>This is a <i>heading</i></b></span></p>
<p class="p2"><span class="s1"></span><br></p>
<p class="p4"><span class="s1">This is a <b>paragraph</b> with a <a href="https://w.org"><span class="s2">link</span></a>.</span></p>
<p class="p2"><span class="s1"></span><br></p>
<ul class="ul1">
<li class="li5"><span class="s2">A</span></li>
<li class="li4"><span class="s3"></span>Bulleted</li>
<li class="li4"><span class="s1">A</span></li>
<li class="li4"><span class="s1">Bulleted</span></li>
<ul class="ul1">
<li class="li4"><span class="s3"></span>Indented</li>
<li class="li4"><span class="s1">Indented</span></li>
</ul>
<li class="li4"><span class="s3"></span>List</li>
<li class="li4"><span class="s1">List</span></li>
</ul>
<p class="p2"><br></p>
<p class="p2"><span class="s1"></span><br></p>
<ol class="ol1">
<li class="li4">One</li>
<li class="li4">Two</li>
<li class="li4">Three</li>
<li class="li4"><span class="s1">One</span></li>
<li class="li4"><span class="s1">Two</span></li>
<li class="li4"><span class="s1">Three</span></li>
</ol>
<p class="p2"><br></p>
<p class="p4">An Image:</p>
<p class="p2"><br></p>
<p class="p6"><br></p>
<p class="p2"><span class="s1"></span><br></p>
<table cellspacing="0" cellpadding="0" class="t1">
<tbody>
<tr>
<td valign="top" class="td1">
<p class="p5"><span class="s1">One</span></p>
</td>
<td valign="top" class="td2">
<p class="p5"><span class="s1">Two</span></p>
</td>
<td valign="top" class="td1">
<p class="p5"><span class="s1">Three</span></p>
</td>
</tr>
<tr>
<td valign="top" class="td3">
<p class="p6"><span class="s1">1</span></p>
</td>
<td valign="top" class="td4">
<p class="p6"><span class="s1">2</span></p>
</td>
<td valign="top" class="td3">
<p class="p6"><span class="s1">3</span></p>
</td>
</tr>
<tr>
<td valign="top" class="td1">
<p class="p5"><span class="s1">I</span></p>
</td>
<td valign="top" class="td2">
<p class="p5"><span class="s1">II</span></p>
</td>
<td valign="top" class="td1">
<p class="p5"><span class="s1">III</span></p>
</td>
</tr>
</tbody>
</table>
<p class="p2"><span class="s1"></span><br></p>
<p class="p4"><span class="s1">An image: </span></p>
<p class="p2"><span class="s1"></span><br></p>
Loading

0 comments on commit dc472ce

Please sign in to comment.