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

Paste shortcodes #2874

Merged
merged 3 commits into from
Oct 31, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 77 additions & 54 deletions blocks/api/raw-handling/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import { find, get } from 'lodash';
import showdown from 'showdown';

/**
* Internal dependencies
Expand All @@ -22,7 +23,7 @@ import blockquoteNormaliser from './blockquote-normaliser';
import tableNormaliser from './table-normaliser';
import inlineContentConverter from './inline-content-converter';
import { deepFilterHTML, isInvalidInline, isNotWhitelisted, isPlain, isInline } from './utils';
import showdown from 'showdown';
import shortcodeConverter from './shortcode-converter';

/**
* Converts an HTML string to known blocks. Strips everything else.
Expand All @@ -33,89 +34,111 @@ import showdown from 'showdown';
* @return {Array|String} A list of blocks or a string, depending on the `inline` option.
*/
export default function rawHandler( { HTML, plainText = '', inline = null } ) {
// First of all, strip any meta tags.
HTML = HTML.replace( /<meta[^>]+>/, '' );

// Block delimiters detected.
// If we detect block delimiters, parse entirely as blocks.
if ( ! inline && HTML.indexOf( '<!-- wp:' ) !== -1 ) {
return parseWithGrammar( HTML );
}

// If there is a plain text version, the HTML version has no formatting,
// and there is at least a double line break,
// parse any Markdown inside the plain text.
if ( plainText && isPlain( HTML ) && plainText.indexOf( '\n\n' ) !== -1 ) {
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 = deepFilterHTML( HTML, [
msListConverter,
] );
}

HTML = deepFilterHTML( HTML, [
listMerger,
imageCorrector,
// Add semantic formatting before attributes are stripped.
formattingTransformer,
stripAttributes,
commentRemover,
createUnwrapper( ( node ) => isNotWhitelisted( node ) || ( inline && ! isInline( node ) ) ),
blockquoteNormaliser,
tableNormaliser,
inlineContentConverter,
] );

// Inline paste.
// Return filtered HTML if it's inline paste or all content is inline.
if ( inline || ( inline === null && isInlineContent( HTML ) ) ) {
HTML = deepFilterHTML( HTML, [
// Add semantic formatting before attributes are stripped.
formattingTransformer,
stripAttributes,
commentRemover,
createUnwrapper( ( node ) => ! isInline( node ) ),
] );

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

return HTML;
}

HTML = deepFilterHTML( HTML, [
createUnwrapper( isInvalidInline ),
] );
// Before we parse any HTML, extract shorcodes so they don't get messed up.
return shortcodeConverter( HTML ).reduce( ( accu, piece ) => {
// Already a block from shortcode.
if ( typeof piece !== 'string' ) {
return [ ...accu, piece ];
}

HTML = normaliseBlocks( HTML );
// Context dependent filters. Needs to run before we remove nodes.
piece = deepFilterHTML( piece, [
msListConverter,
] );

// Allows us to ask for this information when we get a report.
window.console.log( 'Processed HTML piece:\n\n', HTML );
piece = deepFilterHTML( piece, [
listMerger,
imageCorrector,
// Add semantic formatting before attributes are stripped.
formattingTransformer,
stripAttributes,
commentRemover,
createUnwrapper( isNotWhitelisted ),
blockquoteNormaliser,
tableNormaliser,
inlineContentConverter,
] );

const doc = document.implementation.createHTMLDocument( '' );
piece = deepFilterHTML( piece, [
createUnwrapper( isInvalidInline ),
] );

doc.body.innerHTML = HTML;
piece = normaliseBlocks( piece );

return Array.from( doc.body.children ).map( ( node ) => {
const block = getBlockTypes().reduce( ( acc, blockType ) => {
if ( acc ) {
return acc;
}
// Allows us to ask for this information when we get a report.
window.console.log( 'Processed HTML piece:\n\n', piece );

const transformsFrom = get( blockType, 'transforms.from', [] );
const transform = find( transformsFrom, ( { type } ) => type === 'raw' );
const doc = document.implementation.createHTMLDocument( '' );

if ( ! transform || ! transform.isMatch( node ) ) {
return acc;
}
doc.body.innerHTML = piece;

return createBlock(
blockType.name,
getBlockAttributes(
blockType,
node.outerHTML
)
);
}, null );

if ( block ) {
return block;
}
const blocks = Array.from( doc.body.children ).map( ( node ) => {
const block = getBlockTypes().reduce( ( acc, blockType ) => {
if ( acc ) {
return acc;
}

return createBlock( getUnknownTypeHandlerName(), {
content: node.outerHTML,
const transformsFrom = get( blockType, 'transforms.from', [] );
const transform = find( transformsFrom, ( { type } ) => type === 'raw' );

if ( ! transform || ! transform.isMatch( node ) ) {
return acc;
}

return createBlock(
blockType.name,
getBlockAttributes(
blockType,
node.outerHTML
)
);
}, null );

if ( block ) {
return block;
}

return createBlock( getUnknownTypeHandlerName(), {
content: node.outerHTML,
} );
} );
} );

return [ ...accu, ...blocks ];
}, [] );
}
78 changes: 78 additions & 0 deletions blocks/api/raw-handling/shortcode-converter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* External dependencies
*/
import { find, get, dropRight, last, mapValues, pickBy } from 'lodash';

/**
* Internal dependencies
*/
import { createBlock } from '../factory';
import { getBlockTypes } from '../registration';
import { getBlockAttributes } from '../parser';

/**
* Browser dependencies
*/
const { shortcode } = window.wp;

export default function( HTML ) {
// Get all matches. These are *not* ordered.
const matches = getBlockTypes().reduce( ( acc, blockType ) => {
const transformsFrom = get( blockType, 'transforms.from', [] );
const transform = find( transformsFrom, ( { type } ) => type === 'shortcode' );

if ( ! transform ) {
return acc;
}

let match;
let lastIndex = 0;

while ( ( match = shortcode.next( transform.tag, HTML, lastIndex ) ) ) {
lastIndex = match.index + match.content.length;

const attributes = mapValues(
pickBy( transform.attributes, ( schema ) => schema.shortcode ),
( schema ) => schema.shortcode( match.shortcode.attrs ),
);

const block = createBlock(
blockType.name,
getBlockAttributes(
{
...blockType,
attributes: transform.attributes,
},
match.shortcode.content,
attributes,
)
);

acc[ match.index ] = { block, lastIndex };
}

return acc;
}, {} );

let negativeI = 0;

// Sort the matches and return an array of text pieces and blocks.
return Object.keys( matches ).sort().reduce( ( acc, index ) => {
const match = matches[ index ];

acc = [
// Add all pieces except the last text piece.
...dropRight( acc ),
// Add the start of the last text piece.
last( acc ).slice( 0, index - negativeI ),
// Add the block.
match.block,
// Add the rest of the last text piece.
last( acc ).slice( match.lastIndex - negativeI ),
];

negativeI = match.lastIndex;

return acc;
}, [ HTML ] );
}
23 changes: 22 additions & 1 deletion blocks/library/gallery/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class GalleryBlock extends Component {
this.toggleImageCrop = this.toggleImageCrop.bind( this );
this.uploadFromFiles = this.uploadFromFiles.bind( this );
this.onRemoveImage = this.onRemoveImage.bind( this );
this.setImageAttributes = this.setImageAttributes.bind( this );

this.state = {
selectedImage: null,
Expand Down Expand Up @@ -94,6 +95,21 @@ class GalleryBlock extends Component {
mediaUpload( event.target.files, this.props.setAttributes, isGallery );
}

setImageAttributes( index, attributes ) {
const { attributes: { images }, setAttributes } = this.props;

setAttributes( {
images: [
...images.slice( 0, index ),
{
...images[ index ],
...attributes,
},
...images.slice( index + 1 ),
],
} );
}

render() {
const { attributes, focus, className } = this.props;
const { images, columns = defaultColumnsNumber( attributes ), align, imageCrop, linkTo } = attributes;
Expand Down Expand Up @@ -204,10 +220,15 @@ class GalleryBlock extends Component {
),
<div key="gallery" className={ `${ className } align${ align } columns-${ columns } ${ imageCrop ? 'is-cropped' : '' }` }>
{ images.map( ( img, index ) => (
<GalleryImage key={ img.url } img={ img }
<GalleryImage
key={ img.id || img.url }
url={ img.url }
alt={ img.alt }
id={ img.id }
isSelected={ this.state.selectedImage === index }
onRemove={ this.onRemoveImage( index ) }
onClick={ this.onSelectImage( index ) }
setAttributes={ ( attrs ) => this.setImageAttributes( index, attrs ) }
/>
) ) }
</div>,
Expand Down
Loading