diff --git a/blocks/README.md b/blocks/README.md index abee79e394c91..01b88ba4e5c78 100644 --- a/blocks/README.md +++ b/blocks/README.md @@ -33,7 +33,7 @@ function myplugin_enqueue_block_editor_assets() { wp_enqueue_script( 'myplugin-block', plugins_url( 'block.js', __FILE__ ), - array( 'wp-blocks', wp-element' ) + array( 'wp-blocks', 'wp-element' ) ); } add_action( 'enqueue_block_editor_assets', 'myplugin_enqueue_block_editor_assets' ); diff --git a/blocks/api/factory.js b/blocks/api/factory.js index 7a86f2c9f5055..e775765cab4fe 100644 --- a/blocks/api/factory.js +++ b/blocks/api/factory.js @@ -39,10 +39,17 @@ export function createBlock( name, blockAttributes = {} ) { return result; }, {} ); + + // Keep the anchor if the block supports it if ( blockType.supportAnchor && blockAttributes.anchor ) { attributes.anchor = blockAttributes.anchor; } + // Keep the className if the block supports it + if ( blockType.className !== false && blockAttributes.className ) { + attributes.className = blockAttributes.className; + } + // Blocks are stored with a unique ID, the assigned type name, // and the block attributes. return { diff --git a/blocks/api/index.js b/blocks/api/index.js index c6445f4a65f02..f9a45baca2754 100644 --- a/blocks/api/index.js +++ b/blocks/api/index.js @@ -5,9 +5,10 @@ import * as source from './source'; export { source }; export { createBlock, switchToBlockType } from './factory'; -export { default as parse } from './parser'; +export { default as parse, getSourcedAttributes } from './parser'; export { default as pasteHandler } from './paste'; -export { default as serialize, getBlockDefaultClassname } from './serializer'; +export { default as serialize, getBlockDefaultClassname, getBlockContent } from './serializer'; +export { isValidBlock } from './validation'; export { getCategories } from './categories'; export { registerBlockType, diff --git a/blocks/api/parser.js b/blocks/api/parser.js index 7f7cfc7c9bef6..dc8b6f03a6a19 100644 --- a/blocks/api/parser.js +++ b/blocks/api/parser.js @@ -153,6 +153,11 @@ export function getBlockAttributes( blockType, rawContent, attributes ) { blockAttributes.anchor = hpqParse( rawContent, attr( '*', 'id' ) ); } + // If the block supports a custom className parse it + if ( blockType.className !== false && attributes && attributes.className ) { + blockAttributes.className = attributes.className; + } + return blockAttributes; } diff --git a/blocks/api/paste/create-unwrapper.js b/blocks/api/paste/create-unwrapper.js index 277576a8cc0e0..3266cbe62e3d5 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 a8721465f1edf..ca826bb3bb009 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 0000000000000..1d36ebc8fb465 --- /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 0000000000000..a921aab1a4f82 --- /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 0000000000000..34ee6ed666faf --- /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 1b0d8bec7d832..151b545f57862 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 = '
'; - 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 eaae0ab64502e..91222acfdd8a7 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
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 2cea35c055fd6..772ccff0d2c9e 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 ] ), 'testtest
test
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.
+An Image:
-
+ One + |
+
+ Two + |
+
+ Three + |
+
+ 1 + |
+
+ 2 + |
+
+ 3 + |
+
+ I + |
+
+ II + |
+
+ III + |
+
An image:
+This is a title
- + - +This is a heading
- + - +This is a paragraph with a link.
- + - +An Image:
- + ++ One + | ++ Two + | ++ Three + | +
+ 1 + | ++ 2 + | ++ 3 + | +
+ I + | ++ II + | ++ III + | +
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 b0b025cac268d..f69199a6d85fb 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 paragraph with a link.
A
Bulleted
Indented
List
One
Two
Three
An image:
+
This is a title
This is a paragraph with a link.
A
Bulleted
Indented
List
One
Two
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 17d842698482b..8cb94dc1bbcb8 100644 --- a/blocks/api/paste/test/integration/google-docs-out.html +++ b/blocks/api/paste/test/integration/google-docs-out.html @@ -1,53 +1,65 @@ - +
This is a title
This is a paragraph with a link.
A
-Bulleted
-Indented
-List
-One
-Two
-Three
-One | +Two | +Three | +
1 | +2 | +3 | +
I | +II | +III | +
An image:
+ One |
+
+ Two |
+
+ Three |
+
+ 1 |
+
+ 2 |
+
+ 3 |
+
+ I |
+
+ II |
+
+ III |
+
An image:
This is a heading
This is a paragraph with a link.
A
Bulleted
Indented
List
One
Two
Three
An image:
This is a heading
This is a paragraph with a link.
A
Bulleted
Indented
List
One
Two
Three
One | Two | Three |
1 | 2 | 3 |
I | II | III |
An image:
This is a heading
- + +This is a heading
+ - +This is a paragraph with a link.
- + - +A
-Bulleted
-Indented
-List
-One
-Two
-Three
-One | +Two | +Three | +
1 | +2 | +3 | +
I | +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 612d8bdfd3d13..3f4687910c6ce 100644 --- a/blocks/api/paste/test/integration/ms-word-out.html +++ b/blocks/api/paste/test/integration/ms-word-out.html @@ -1,26 +1,26 @@ - +This is a title
- + - +This is a subtitle
- + - +This is a paragraph with a link.
- + - ++ One + | ++ Two + | ++ Three + | +
+ 1 + | ++ 2 + | ++ 3 + | +
+ I + | ++ II + | ++ III + | +
An image:
- + - + - + diff --git a/blocks/api/paste/test/integration/plain-out.html b/blocks/api/paste/test/integration/plain-out.html index 0779af0feb254..bc455fd4d0120 100644 --- a/blocks/api/paste/test/integration/plain-out.html +++ b/blocks/api/paste/test/integration/plain-out.html @@ -1,7 +1,7 @@ - +test
test
test
* test
'; const output = '1 test
'; const output = '* test
'; const input3 = '* test
'; const output = '* test
'; const input4 = '* test
'; const output = '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 0000000000000..27295ba22bcc4 --- /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 |
Broccoli
' + + 'Ribs' + ); + expect( parsed ).toHaveLength( 1 ); + expect( parsed[ 0 ].name ).toBe( 'core/test-block' ); + } ); + it( 'should parse the post content, using unknown block handler', () => { registerBlockType( 'core/test-block', defaultBlockSettings ); registerBlockType( 'core/unknown-block', defaultBlockSettings ); diff --git a/blocks/api/test/registration.js b/blocks/api/test/registration.js index de02dc61a0427..fcfa26d5748fb 100644 --- a/blocks/api/test/registration.js +++ b/blocks/api/test/registration.js @@ -63,6 +63,12 @@ describe( 'blocks', () => { expect( block ).toBeUndefined(); } ); + it( 'should reject blocks with uppercase characters', () => { + const block = registerBlockType( 'Core/Paragraph' ); + expect( console.error ).toHaveBeenCalledWith( 'Block names must not contain uppercase characters.' ); + expect( block ).toBeUndefined(); + } ); + it( 'should accept valid block names', () => { const block = registerBlockType( 'my-plugin/fancy-block-4', defaultBlockSettings ); expect( console.error ).not.toHaveBeenCalled(); diff --git a/blocks/api/test/serializer.js b/blocks/api/test/serializer.js index ffc61d23d79f0..7a64ebd33e3e7 100644 --- a/blocks/api/test/serializer.js +++ b/blocks/api/test/serializer.js @@ -14,6 +14,7 @@ import serialize, { serializeAttributes, getCommentDelimitedContent, serializeBlock, + getBlockContent, } from '../serializer'; import { getBlockTypes, @@ -167,7 +168,7 @@ describe( 'block serializer', () => { fruit: 'bananas', category: 'food', ripeness: 'ripe', - }, { + }, { attributes: { fruit: { type: 'string', source: text(), @@ -178,7 +179,7 @@ describe( 'block serializer', () => { ripeness: { type: 'string', }, - } ); + } } ); expect( attributes ).toEqual( { category: 'food', @@ -190,17 +191,33 @@ describe( 'block serializer', () => { const attributes = getCommentAttributes( { fruit: 'bananas', ripeness: undefined, - }, { + }, { attributes: { fruit: { type: 'string', }, ripeness: { type: 'string', }, - } ); + } } ); expect( attributes ).toEqual( { fruit: 'bananas' } ); } ); + + it( 'should return the className attribute if allowed', () => { + const attributes = getCommentAttributes( { + className: 'chicken', + }, { attributes: {} } ); + + expect( attributes ).toEqual( { className: 'chicken' } ); + } ); + + it( 'should not return the className attribute if not supported', () => { + const attributes = getCommentAttributes( { + className: 'chicken', + }, { attributes: {}, className: false } ); + + expect( attributes ).toEqual( {} ); + } ); } ); describe( 'serializeAttributes()', () => { @@ -226,7 +243,17 @@ describe( 'block serializer', () => { '' ); - expect( content ).toBe( '' ); + expect( content ).toBe( '' ); + } ); + + it( 'should include the namespace for non-core blocks', () => { + const content = getCommentDelimitedContent( + 'my-wonderful-namespace/test-block', + {}, + '' + ); + + expect( content ).toBe( '' ); } ); it( 'should generate empty attributes non-void', () => { @@ -236,7 +263,7 @@ describe( 'block serializer', () => { 'Delicious' ); - expect( content ).toBe( '\nDelicious\n' ); + expect( content ).toBe( '\nDelicious\n' ); } ); it( 'should generate non-empty attributes void', () => { @@ -247,7 +274,7 @@ describe( 'block serializer', () => { ); expect( content ).toBe( - '' + '' ); } ); @@ -259,7 +286,7 @@ describe( 'block serializer', () => { ); expect( content ).toBe( - '\nDelicious\n' + '\nDelicious\n' ); } ); } ); @@ -370,7 +397,7 @@ describe( 'block serializer', () => { content: 'Ribs & Chicken', stuff: 'left & right -- butRibs & Chicken
\n'; + const expectedPostContent = '\nRibs & Chicken
\n'; expect( serialize( [ block ] ) ).toEqual( expectedPostContent ); expect( serialize( block ) ).toEqual( expectedPostContent ); @@ -385,7 +412,7 @@ describe( 'block serializer', () => { block.originalContent = 'Correct'; expect( serialize( block ) ).toEqual( - '\nCorrect\n' + '\nCorrect\n' ); } ); @@ -398,8 +425,35 @@ describe( 'block serializer', () => { block.originalContent = 'Correct'; expect( serialize( block ) ).toEqual( - '\nCorrect\n' + '\nCorrect\n' ); } ); } ); + + describe( 'getBlockContent', () => { + it( 'should return the block\'s serialized inner HTML', () => { + const blockType = { + attributes: { + content: { + type: 'string', + source: text(), + }, + }, + save( { attributes } ) { + return attributes.content; + }, + category: 'common', + title: 'block title', + }; + registerBlockType( 'core/chicken', blockType ); + const block = { + name: 'core/chicken', + attributes: { + content: 'chicken
', + }, + isValid: true, + }; + expect( getBlockContent( block ) ).toBe( 'chicken
' ); + } ); + } ); } ); diff --git a/blocks/editable/style.scss b/blocks/editable/style.scss index 7fa2bf97d4473..38ce936953a20 100644 --- a/blocks/editable/style.scss +++ b/blocks/editable/style.scss @@ -47,6 +47,18 @@ background: $light-gray-400; } + &:focus { + b, i, strong, em, del, ins, sup, sub { + &[data-mce-selected] { + padding: 0 2px; + margin: 0 -2px; + border-radius: 2px; + box-shadow: 0 0 0 1px $light-gray-400; + background: $light-gray-400; + } + } + } + &[data-is-placeholder-visible="true"] { position: absolute; top: 0; diff --git a/blocks/editable/tinymce.js b/blocks/editable/tinymce.js index 415a388c27a4e..64d544a840be8 100644 --- a/blocks/editable/tinymce.js +++ b/blocks/editable/tinymce.js @@ -60,6 +60,7 @@ export default class TinyMCE extends Component { browser_spellcheck: true, entity_encoding: 'raw', convert_urls: false, + inline_boundaries_selector: 'a[href],code,b,i,strong,em,del,ins,sup,sub', plugins: [], formats: { strikethrough: { inline: 'del' }, diff --git a/blocks/library/audio/index.js b/blocks/library/audio/index.js index 671191335f713..c022d86c5580a 100644 --- a/blocks/library/audio/index.js +++ b/blocks/library/audio/index.js @@ -15,12 +15,13 @@ import { Component } from '@wordpress/element'; import './style.scss'; import { registerBlockType, source } from '../../api'; import MediaUploadButton from '../../media-upload-button'; +import Editable from '../../editable'; import BlockControls from '../../block-controls'; import BlockAlignmentToolbar from '../../block-alignment-toolbar'; import InspectorControls from '../../inspector-controls'; import BlockDescription from '../../block-description'; -const { attr } = source; +const { attr, children } = source; registerBlockType( 'core/audio', { title: __( 'Audio' ), @@ -37,6 +38,10 @@ registerBlockType( 'core/audio', { align: { type: 'string', }, + caption: { + type: 'array', + source: children( 'figcaption' ), + }, }, getEditWrapperProps( attributes ) { @@ -58,8 +63,8 @@ registerBlockType( 'core/audio', { }; } render() { - const { align } = this.props.attributes; - const { setAttributes, focus } = this.props; + const { align, caption } = this.props.attributes; + const { setAttributes, focus, setFocus } = this.props; const { editing, className, src } = this.state; const updateAlignment = ( nextAlign ) => setAttributes( { align: nextAlign } ); const switchToEditing = () => { @@ -113,6 +118,8 @@ registerBlockType( 'core/audio', { ); + const focusCaption = ( focusValue ) => setFocus( { editable: 'caption', ...focusValue } ); + if ( editing ) { return [ inspectorControls, @@ -150,20 +157,32 @@ registerBlockType( 'core/audio', { return [ controls, inspectorControls, -+ {__( 'Image galleries are a great way to share groups of pictures on your site.' )} +
+{ __( 'Image galleries are a great way to share groups of pictures on your site.' ) }
-export default function MyButton() {
return <Button>Click Me!</Button>;
}
-
+
diff --git a/blocks/test/fixtures/core__cover-image.serialized.html b/blocks/test/fixtures/core__cover-image.serialized.html
index de557f4357676..f2eb58b9e32cd 100644
--- a/blocks/test/fixtures/core__cover-image.serialized.html
+++ b/blocks/test/fixtures/core__cover-image.serialized.html
@@ -1,5 +1,5 @@
-
+
... like this one, which is separate from the above and right aligned.
- + diff --git a/blocks/test/fixtures/core__preformatted.serialized.html b/blocks/test/fixtures/core__preformatted.serialized.html index b8bceecf41f82..86bf1df537b25 100644 --- a/blocks/test/fixtures/core__preformatted.serialized.html +++ b/blocks/test/fixtures/core__preformatted.serialized.html @@ -1,3 +1,3 @@ - +Some preformatted text...- + diff --git a/blocks/test/fixtures/core__pullquote.serialized.html b/blocks/test/fixtures/core__pullquote.serialized.html index 73cc7b98cfd2b..340b4fef74d4c 100644 --- a/blocks/test/fixtures/core__pullquote.serialized.html +++ b/blocks/test/fixtures/core__pullquote.serialized.html @@ -1,6 +1,6 @@ - +
And more!
- + diff --git a/blocks/test/fixtures/core__pullquote__multi-paragraph.serialized.html b/blocks/test/fixtures/core__pullquote__multi-paragraph.serialized.html index ad3fbe53cefa1..8d90207d382dd 100644 --- a/blocks/test/fixtures/core__pullquote__multi-paragraph.serialized.html +++ b/blocks/test/fixtures/core__pullquote__multi-paragraph.serialized.html @@ -1,7 +1,7 @@ - +Testing pullquote block...
- + diff --git a/blocks/test/fixtures/core__quote__style-1.serialized.html b/blocks/test/fixtures/core__quote__style-1.serialized.html index fd253fa76b139..6eb5a4e20fcec 100644 --- a/blocks/test/fixtures/core__quote__style-1.serialized.html +++ b/blocks/test/fixtures/core__quote__style-1.serialized.html @@ -1,6 +1,6 @@ - +Paragraph one
Paragraph two
- + diff --git a/blocks/test/fixtures/core__quote__style-2.serialized.html b/blocks/test/fixtures/core__quote__style-2.serialized.html index 4234c53b74a8a..3c0d0a9b325eb 100644 --- a/blocks/test/fixtures/core__quote__style-2.serialized.html +++ b/blocks/test/fixtures/core__quote__style-2.serialized.html @@ -1,6 +1,6 @@ - +The editor will endeavour to create a new page and post building experience that makes writing rich posts effortless, and has “blocks” to make it easy what today might take shortcodes, custom HTML, or “mystery meat” embed discovery.
- + diff --git a/blocks/test/fixtures/core__separator.serialized.html b/blocks/test/fixtures/core__separator.serialized.html index d835a483147f1..102fede296a01 100644 --- a/blocks/test/fixtures/core__separator.serialized.html +++ b/blocks/test/fixtures/core__separator.serialized.html @@ -1,3 +1,3 @@ - +There is no greater agony than bearing an untold story inside you.
One
@@ -7,4 +7,4 @@Two
This is an old-style text block. Changed to core/paragraph
in #2135.
This is an old-style text block. Changed to paragraph
in #2135.
This is an old-style text block. Changed to core/paragraph
in #2135.
This is an old-style text block. Changed to paragraph
in #2135.
This is an old-style text block. Changed to core/paragraph
in #2135.
This is an old-style text block. Changed to paragraph
in #2135.
This is an old-style text block. Changed to core/paragraph
in #2135.
This is an old-style text block. Changed to paragraph
in #2135.
A verse…- + diff --git a/blocks/test/fixtures/core__video.serialized.html b/blocks/test/fixtures/core__video.serialized.html index 5b6c233e241f1..99abb6e0cf882 100644 --- a/blocks/test/fixtures/core__video.serialized.html +++ b/blocks/test/fixtures/core__video.serialized.html @@ -1,3 +1,3 @@ - + - + diff --git a/components/dropdown/index.js b/components/dropdown/index.js index 8d932b393d15f..f1475771be3a1 100644 --- a/components/dropdown/index.js +++ b/components/dropdown/index.js @@ -6,8 +6,11 @@ import { Component } from '@wordpress/element'; /** * Internal Dependencies */ +import withFocusReturn from '../higher-order/with-focus-return'; import Popover from '../popover'; +const FocusManaged = withFocusReturn( ( { children } ) => children ); + class Dropdown extends Component { constructor() { super( ...arguments ); @@ -51,9 +54,12 @@ class Dropdown extends Component { className={ contentClassName } isOpen={ isOpen } position={ position } + onClose={ this.close } onClickOutside={ this.clickOutside } > - { renderContent( args ) } +
And more!
Tester
\n", ) ); self::$post_without_blocks = self::factory()->post->create( array( - 'post_title' => 'Example', + 'post_title' => 'Example', 'post_content' => 'Tester', ) ); return parent::setUpBeforeClass(); @@ -90,12 +90,12 @@ function test_gutenberg_add_edit_links() { $trashed_post = $this->factory()->post->create( array( 'post_status' => 'trash', ) ); - $actions = apply_filters( 'post_row_actions', $original_actions, get_post( $trashed_post ) ); + $actions = apply_filters( 'post_row_actions', $original_actions, get_post( $trashed_post ) ); $this->assertArrayNotHasKey( 'gutenberg hide-if-no-js', $actions ); $this->assertArrayNotHasKey( 'classic hide-if-no-js', $actions ); register_post_type( 'not_shown_in_rest', array( - 'supports' => array( 'title', 'editor' ), + 'supports' => array( 'title', 'editor' ), 'show_in_rest' => false, ) ); $post_id = $this->factory()->post->create( array( @@ -107,7 +107,7 @@ function test_gutenberg_add_edit_links() { register_post_type( 'not_supports_editor', array( 'show_in_rest' => true, - 'supports' => array( 'title' ), + 'supports' => array( 'title' ), ) ); $post_id = $this->factory()->post->create( array( 'post_type' => 'not_supports_editor', diff --git a/phpunit/class-block-type-registry-test.php b/phpunit/class-block-type-registry-test.php index 3ae3fcb273266..0ba75cd8d07e7 100644 --- a/phpunit/class-block-type-registry-test.php +++ b/phpunit/class-block-type-registry-test.php @@ -59,6 +59,16 @@ function test_invalid_characters() { $this->assertFalse( $result ); } + /** + * Should reject blocks with uppercase characters + * + * @expectedIncorrectUsage WP_Block_Type_Registry::register + */ + function test_uppercase_characters() { + $result = $this->registry->register( 'Core/Paragraph', array() ); + $this->assertFalse( $result ); + } + /** * Should accept valid block names */ @@ -128,7 +138,7 @@ function test_unregister_block_type() { } function test_get_all_registered() { - $names = array( 'core/paragraph', 'core/image', 'core/blockquote' ); + $names = array( 'core/paragraph', 'core/image', 'core/blockquote' ); $settings = array( 'icon' => 'random', ); diff --git a/phpunit/class-block-type-test.php b/phpunit/class-block-type-test.php index dfa776cca1037..75b340071065e 100644 --- a/phpunit/class-block-type-test.php +++ b/phpunit/class-block-type-test.php @@ -32,7 +32,7 @@ function test_render() { $block_type = new WP_Block_Type( 'core/dummy', array( 'render_callback' => array( $this, 'render_dummy_block' ), ) ); - $output = $block_type->render( $attributes ); + $output = $block_type->render( $attributes ); $this->assertEquals( $attributes, json_decode( $output, true ) ); } @@ -41,12 +41,12 @@ function test_render_with_content() { 'foo' => 'bar', 'bar' => 'foo', ); - $content = 'Test content.
'; + $content = 'Test content.
'; - $block_type = new WP_Block_Type( 'core/dummy', array( + $block_type = new WP_Block_Type( 'core/dummy', array( 'render_callback' => array( $this, 'render_dummy_block_with_content' ), ) ); - $output = $block_type->render( $attributes, $content ); + $output = $block_type->render( $attributes, $content ); $attributes['_content'] = $content; $this->assertSame( $attributes, json_decode( $output, true ) ); } @@ -56,10 +56,10 @@ function test_render_without_callback() { 'foo' => 'bar', 'bar' => 'foo', ); - $content = 'Test content.
'; + $content = 'Test content.
'; $block_type = new WP_Block_Type( 'core/dummy' ); - $output = $block_type->render( $attributes, $content ); + $output = $block_type->render( $attributes, $content ); $this->assertSame( $content, $output ); } @@ -74,17 +74,17 @@ function test_prepare_attributes() { $block_type = new WP_Block_Type( 'core/dummy', array( 'attributes' => array( - 'correct' => array( + 'correct' => array( 'type' => 'string', ), - 'wrongType' => array( + 'wrongType' => array( 'type' => 'string', ), 'wrongTypeDefaulted' => array( 'type' => 'string', 'default' => 'defaulted', ), - 'missingDefaulted' => array( + 'missingDefaulted' => array( 'type' => 'string', 'default' => 'define', ), diff --git a/phpunit/class-performance-test.php b/phpunit/class-performance-test.php index 56df5b79db99b..20b58e722dda4 100644 --- a/phpunit/class-performance-test.php +++ b/phpunit/class-performance-test.php @@ -14,13 +14,13 @@ function test_parse_large_post() { dirname( __FILE__ ) . '/fixtures/long-content.html' ); - $start = microtime( true ); + $start = microtime( true ); $start_mem = memory_get_usage(); $blocks = gutenberg_parse_blocks( $html ); $time = microtime( true ) - $start; - $mem = memory_get_usage() - $start_mem; + $mem = memory_get_usage() - $start_mem; if ( getenv( 'SHOW_PERFORMANCE_INFO' ) ) { error_log( '' ); diff --git a/phpunit/class-rest-reusable-blocks-controller-test.php b/phpunit/class-rest-reusable-blocks-controller-test.php index 895cedcc2716f..270c1a2fb9014 100644 --- a/phpunit/class-rest-reusable-blocks-controller-test.php +++ b/phpunit/class-rest-reusable-blocks-controller-test.php @@ -38,14 +38,14 @@ class REST_Reusable_Blocks_Controller_Test extends WP_Test_REST_Controller_Testc */ public static function wpSetUpBeforeClass( $factory ) { self::$reusable_block_post_id = wp_insert_post( array( - 'post_type' => 'gb_reusable_block', - 'post_status' => 'publish', - 'post_name' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', - 'post_title' => 'My cool block', + 'post_type' => 'gb_reusable_block', + 'post_status' => 'publish', + 'post_name' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', + 'post_title' => 'My cool block', 'post_content' => 'Hello!
', ) ); - self::$editor_id = $factory->user->create( array( + self::$editor_id = $factory->user->create( array( 'role' => 'editor', ) ); self::$subscriber_id = $factory->user->create( array( @@ -81,14 +81,14 @@ public function test_register_routes() { public function test_get_items() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks' ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( array( array( - 'id' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', - 'name' => 'My cool block', + 'id' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', + 'name' => 'My cool block', 'content' => 'Hello!
', ), ), $response->get_data() ); @@ -100,9 +100,9 @@ public function test_get_items() { public function test_get_items_when_not_allowed() { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks' ); $response = $this->server->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertEquals( 403, $response->get_status() ); $this->assertEquals( 'gutenberg_reusable_block_cannot_read', $data['code'] ); @@ -114,13 +114,13 @@ public function test_get_items_when_not_allowed() { public function test_get_item() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); $response = $this->server->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( array( - 'id' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', - 'name' => 'My cool block', + 'id' => '2d66a5c5-776c-43b1-98c7-49521cef8ea6', + 'name' => 'My cool block', 'content' => 'Hello!
', ), $response->get_data() ); } @@ -131,9 +131,9 @@ public function test_get_item() { public function test_get_item_when_not_allowed() { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); $response = $this->server->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertEquals( 403, $response->get_status() ); $this->assertEquals( 'gutenberg_reusable_block_cannot_read', $data['code'] ); @@ -145,9 +145,9 @@ public function test_get_item_when_not_allowed() { public function test_get_item_invalid_id() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/invalid-uuid' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/invalid-uuid' ); $response = $this->server->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertEquals( 404, $response->get_status() ); $this->assertEquals( 'gutenberg_reusable_block_invalid_id', $data['code'] ); @@ -159,9 +159,9 @@ public function test_get_item_invalid_id() { public function test_get_item_not_found() { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/6e614ced-e80d-4e10-bd04-1e890b5f7f83' ); + $request = new WP_REST_Request( 'GET', '/gutenberg/v1/reusable-blocks/6e614ced-e80d-4e10-bd04-1e890b5f7f83' ); $response = $this->server->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertEquals( 404, $response->get_status() ); $this->assertEquals( 'gutenberg_reusable_block_not_found', $data['code'] ); @@ -175,7 +175,7 @@ public function test_update_item() { $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/75236553-f4ba-4f12-aa25-4ba402044bd5' ); $request->set_body_params( array( - 'name' => 'Another cool block', + 'name' => 'Another cool block', 'content' => '', ) ); @@ -183,8 +183,8 @@ public function test_update_item() { $this->assertEquals( 200, $response->get_status() ); $this->assertEquals( array( - 'id' => '75236553-f4ba-4f12-aa25-4ba402044bd5', - 'name' => 'Another cool block', + 'id' => '75236553-f4ba-4f12-aa25-4ba402044bd5', + 'name' => 'Another cool block', 'content' => '', ), $response->get_data() ); } @@ -195,9 +195,9 @@ public function test_update_item() { public function test_update_item_when_not_allowed() { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); + $request = new WP_REST_Request( 'PUT', '/gutenberg/v1/reusable-blocks/2d66a5c5-776c-43b1-98c7-49521cef8ea6' ); $response = $this->server->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertEquals( 403, $response->get_status() ); $this->assertEquals( 'gutenberg_reusable_block_cannot_edit', $data['code'] ); @@ -228,7 +228,7 @@ public function data_update_item_with_invalid_fields() { ), array( array( - 'name' => 'My cool block', + 'name' => 'My cool block', 'content' => 42, ), 'Invalid reusable block content.', @@ -248,7 +248,7 @@ public function test_update_item_with_invalid_fields( $body_params, $expected_me $request->set_body_params( $body_params ); $response = $this->server->dispatch( $request ); - $data = $response->get_data(); + $data = $response->get_data(); $this->assertEquals( 400, $response->get_status() ); $this->assertEquals( 'gutenberg_reusable_block_invalid_field', $data['code'] ); @@ -259,9 +259,9 @@ public function test_update_item_with_invalid_fields( $body_params, $expected_me * Check that we have defined a JSON schema. */ public function test_get_item_schema() { - $request = new WP_REST_Request( 'OPTIONS', '/gutenberg/v1/reusable-blocks' ); - $response = $this->server->dispatch( $request ); - $data = $response->get_data(); + $request = new WP_REST_Request( 'OPTIONS', '/gutenberg/v1/reusable-blocks' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); $properties = $data['schema']['properties']; $this->assertEquals( 3, count( $properties ) ); diff --git a/phpunit/class-vendor-script-filename-test.php b/phpunit/class-vendor-script-filename-test.php index ee42132f9effd..a16f2cc35b208 100644 --- a/phpunit/class-vendor-script-filename-test.php +++ b/phpunit/class-vendor-script-filename-test.php @@ -70,8 +70,8 @@ function vendor_script_filename_cases() { * @dataProvider vendor_script_filename_cases */ function test_gutenberg_vendor_script_filename( $url, $expected_filename_pattern ) { - $hash = substr( md5( $url ), 0, 8 ); - $actual_filename = gutenberg_vendor_script_filename( $url ); + $hash = substr( md5( $url ), 0, 8 ); + $actual_filename = gutenberg_vendor_script_filename( $url ); $actual_filename_pattern = str_replace( $hash, 'HASH', $actual_filename ); $this->assertEquals( $expected_filename_pattern,