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 = '

test

'; - 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

', [ 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 ] ), 'test
' ); } ); } ); diff --git a/blocks/api/paste/test/formatting-transformer.js b/blocks/api/paste/test/formatting-transformer.js index 2cc57e43b836c..4389a3ba26176 100644 --- a/blocks/api/paste/test/formatting-transformer.js +++ b/blocks/api/paste/test/formatting-transformer.js @@ -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( 'test', [ formattingTransformer ] ), 'test' ); + equal( deepFilterHTML( 'test', [ formattingTransformer ] ), 'test' ); } ); it( 'should transform numeric font weight', () => { - equal( deepFilter( 'test', [ formattingTransformer ] ), 'test' ); + equal( deepFilterHTML( 'test', [ formattingTransformer ] ), 'test' ); } ); it( 'should transform font style', () => { - equal( deepFilter( 'test', [ formattingTransformer ] ), 'test' ); + equal( deepFilterHTML( 'test', [ formattingTransformer ] ), 'test' ); } ); } ); diff --git a/blocks/api/paste/test/image-corrector.js b/blocks/api/paste/test/image-corrector.js index 40a899fc9ea4b..93f11687e7c48 100644 --- a/blocks/api/paste/test/image-corrector.js +++ b/blocks/api/paste/test/image-corrector.js @@ -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 = ''; const output = ''; - equal( deepFilter( input, [ imageCorrector ] ), output ); + equal( deepFilterHTML( input, [ imageCorrector ] ), output ); } ); it( 'should remove trackers', () => { const input = ''; const output = ''; - equal( deepFilter( input, [ imageCorrector ] ), output ); + equal( deepFilterHTML( input, [ imageCorrector ] ), output ); } ); } ); diff --git a/blocks/api/paste/test/inline-content-converter.js b/blocks/api/paste/test/inline-content-converter.js new file mode 100644 index 0000000000000..ee5decf487aad --- /dev/null +++ b/blocks/api/paste/test/inline-content-converter.js @@ -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( '

test

test

', [ inlineContentConverter ] ), + '
test
test
' + ); + } ); +} ); diff --git a/blocks/api/paste/test/integration/apple-in.html b/blocks/api/paste/test/integration/apple-in.html index d181343a27e58..fb2ae65e3ae9d 100644 --- a/blocks/api/paste/test/integration/apple-in.html +++ b/blocks/api/paste/test/integration/apple-in.html @@ -1,27 +1,61 @@ - - - -

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.

+


-


+


    -
  1. One
  2. -
  3. Two
  4. -
  5. Three
  6. +
  7. One
  8. +
  9. Two
  10. +
  11. Three
-


-

An Image:

-


-


+


+ + + + + + + + + + + + + + + + + + +
+

One

+
+

Two

+
+

Three

+
+

1

+
+

2

+
+

3

+
+

I

+
+

II

+
+

III

+
+


+

An image:

+


diff --git a/blocks/api/paste/test/integration/apple-out.html b/blocks/api/paste/test/integration/apple-out.html index 6efb087cf165c..f3c8c473c00ae 100644 --- a/blocks/api/paste/test/integration/apple-out.html +++ b/blocks/api/paste/test/integration/apple-out.html @@ -1,16 +1,16 @@ - +

This is a title

- + - +

This is a heading

- + - +

This is a paragraph with a link.

- + - +
  • List
  • - + - +
    1. One
    2. Two
    3. Three
    - + - -

    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 heading


    This is a paragraph with a link.



    1. One

    2. Two

    3. Three


    An image:


    +

    This is a title


    This is a heading


    This is a paragraph with a link.



    1. One

    2. Two

    3. 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 heading

    - + - +

    This is a paragraph with a link.

    - + - + - + - +
      -
    1. -

      One

      -
    2. -
    3. -

      Two

      -
    4. -
    5. -

      Three

      -
    6. +
    7. One
    8. +
    9. Two
    10. +
    11. Three
    - + - + + + + + + + + + + + + + + + + + + + +
    OneTwoThree
    123
    IIIIII
    + + + +
    + + +

    An image:

    - + - +
    - + diff --git a/blocks/api/paste/test/integration/ms-word-in.html b/blocks/api/paste/test/integration/ms-word-in.html index 22fe7db667705..d65299287e642 100644 --- a/blocks/api/paste/test/integration/ms-word-in.html +++ b/blocks/api/paste/test/integration/ms-word-in.html @@ -73,6 +73,100 @@

    This is a heading level 2 + + + + + + + + + + + + + + + + +
    +

    One

    +
    +

    Two

    +
    +

    Three

    +
    +

    1

    +
    +

    2

    +
    +

    3

    +
    +

    I

    +
    +

    II

    +
    +

    III

    +
    +

     

    An image:

    diff --git a/blocks/api/paste/test/integration/ms-word-online-in.html b/blocks/api/paste/test/integration/ms-word-online-in.html index e067d52847980..4f36dba345d32 100644 --- a/blocks/api/paste/test/integration/ms-word-online-in.html +++ b/blocks/api/paste/test/integration/ms-word-online-in.html @@ -1 +1 @@ -

    This is a heading 

    This is a paragraph with a link. 

    • A 

    • Bulleted 

    • Indented 

    • List 

     

    1. One 

    1. Two 

    1. Three 

    An image: 

     

    +

    This is a heading 

    This is a paragraph with a link. 

    • A 

    • Bulleted 

    • Indented 

    • List 

     

    1. One 

    1. Two 

    1. Three 

    One 

    Two 

    Three 

    1 

    2 

    3 

    I 

    II 

    III 

     

    An image: 

     

    diff --git a/blocks/api/paste/test/integration/ms-word-online-out.html b/blocks/api/paste/test/integration/ms-word-online-out.html index d87eaed98aacf..6f740ff469a5e 100644 --- a/blocks/api/paste/test/integration/ms-word-online-out.html +++ b/blocks/api/paste/test/integration/ms-word-online-out.html @@ -1,46 +1,54 @@ - -

    This is a heading 

    - + +

    This is a heading 

    + - +

    This is a paragraph with a link

    - + - +
      -
    • -

      -
    • -
    • -

      Bulleted 

      -
    • -
    • -

      Indented 

      -
    • -
    • -

      List 

      -
    • +
    • +
    • Bulleted 
    • +
    • Indented 
    • +
    • List 
    - + - +
      -
    1. -

      One 

      -
    2. -
    3. -

      Two 

      -
    4. -
    5. -

      Three 

      -
    6. +
    7. One 
    8. +
    9. Two 
    10. +
    11. Three 
    - + - + + + + + + + + + + + + + + + + + + + +
    One Two Three 
    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 heading level 1

    - + - +

    This is a heading level 2

    - + - +

    This is a paragraph with a link.

    - + - +
    • A
    • Bulleted @@ -30,20 +30,60 @@

      This is a heading level 2

    • List
    - + - +
    1. One
    2. Two
    3. Three
    - + - + + + + + + + + + + + + + + + + + + + +
    + 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

    - + diff --git a/blocks/api/paste/test/list-merger.js b/blocks/api/paste/test/list-merger.js index e36d1c572689f..a82c6088a4bdf 100644 --- a/blocks/api/paste/test/list-merger.js +++ b/blocks/api/paste/test/list-merger.js @@ -7,27 +7,27 @@ import { equal } from 'assert'; * Internal dependencies */ import listMerger from '../list-merger'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'listMerger', () => { it( 'should merge lists', () => { const input = '
    • one
    • two
    '; const output = '
    • one
    • two
    '; - equal( deepFilter( input, [ listMerger ] ), output ); + equal( deepFilterHTML( input, [ listMerger ] ), output ); } ); it( 'should not merge lists if it has more than one item', () => { const input = '
    • one
    • two
    • three
    '; - equal( deepFilter( input, [ listMerger ] ), input ); + equal( deepFilterHTML( input, [ listMerger ] ), input ); } ); it( 'should not merge list if the type is different', () => { const input = '
    • one
    1. two
    '; - equal( deepFilter( input, [ listMerger ] ), input ); + equal( deepFilterHTML( input, [ listMerger ] ), input ); } ); it( 'should not merge lists if there is something in between', () => { const input = '
    • one

    • two
    '; - equal( deepFilter( input, [ listMerger ] ), input ); + equal( deepFilterHTML( input, [ listMerger ] ), input ); } ); } ); diff --git a/blocks/api/paste/test/ms-list-converter.js b/blocks/api/paste/test/ms-list-converter.js index f5d71f161a1fa..96ec1a37964fd 100644 --- a/blocks/api/paste/test/ms-list-converter.js +++ b/blocks/api/paste/test/ms-list-converter.js @@ -7,19 +7,19 @@ import { equal } from 'assert'; * Internal dependencies */ import msListConverter from '../ms-list-converter'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'msListConverter', () => { it( 'should convert unordered list', () => { const input = '

    * test

    '; const output = '
    • test
    '; - equal( deepFilter( input, [ msListConverter ] ), output ); + equal( deepFilterHTML( input, [ msListConverter ] ), output ); } ); it( 'should convert ordered list', () => { const input = '

    1 test

    '; const output = '
    1. test
    '; - equal( deepFilter( input, [ msListConverter ] ), output ); + equal( deepFilterHTML( input, [ msListConverter ] ), output ); } ); it( 'should convert indented list', () => { @@ -27,7 +27,7 @@ describe( 'msListConverter', () => { const input2 = '

    * test

    '; const input3 = '

    * test

    '; const output = '
    • test
      • test
    • test
    '; - equal( deepFilter( input1 + input2 + input3, [ msListConverter ] ), output ); + equal( deepFilterHTML( input1 + input2 + input3, [ msListConverter ] ), output ); } ); it( 'should convert deep indented list', () => { @@ -36,6 +36,6 @@ describe( 'msListConverter', () => { const input3 = '

    * test

    '; const input4 = '

    * test

    '; const output = '
    • test
      • test
        • test
    • test
    '; - equal( deepFilter( input1 + input2 + input3 + input4, [ msListConverter ] ), output ); + equal( deepFilterHTML( input1 + input2 + input3 + input4, [ msListConverter ] ), output ); } ); } ); diff --git a/blocks/api/paste/test/strip-attributes.js b/blocks/api/paste/test/strip-attributes.js index aa07ff2f7d688..43dc12511364d 100644 --- a/blocks/api/paste/test/strip-attributes.js +++ b/blocks/api/paste/test/strip-attributes.js @@ -7,26 +7,26 @@ import { equal } from 'assert'; * Internal dependencies */ import stripAttributes from '../strip-attributes'; -import { deepFilter } from '../utils'; +import { deepFilterHTML } from '../utils'; describe( 'stripAttributes', () => { it( 'should remove attributes', () => { - equal( deepFilter( '

    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\n\n\n
    \n
    \n', [ tableNormaliser ] ), + '\n
    \n
    \n' + ); + } ); +} ); diff --git a/blocks/api/paste/test/utils.js b/blocks/api/paste/test/utils.js index 3e86e9791cabf..411ff1a44b8a5 100644 --- a/blocks/api/paste/test/utils.js +++ b/blocks/api/paste/test/utils.js @@ -7,15 +7,15 @@ import { equal } from 'assert'; * Internal dependencies */ import createUnwrapper from '../create-unwrapper'; -import { deepFilter, isEmpty, isInvalidInline } from '../utils'; +import { deepFilterHTML, isEmpty, isInvalidInline, isPlain } from '../utils'; const spanUnwrapper = createUnwrapper( ( node ) => node.nodeName === 'SPAN' ); const inlineUnwrapper = createUnwrapper( ( node ) => node.nodeName === 'EM' ); -describe( 'deepFilter', () => { +describe( 'deepFilterHTML', () => { it( 'should not error', () => { - equal( deepFilter( 'test', [ spanUnwrapper, inlineUnwrapper ] ), 'test' ); - equal( deepFilter( 'test', [ spanUnwrapper, inlineUnwrapper ] ), 'test' ); + equal( deepFilterHTML( 'test', [ spanUnwrapper, inlineUnwrapper ] ), 'test' ); + equal( deepFilterHTML( 'test', [ spanUnwrapper, inlineUnwrapper ] ), 'test' ); } ); } ); @@ -78,3 +78,17 @@ describe( 'isInvalidInline', () => { equal( isInvalidInlineHTML( 'test' ), false ); } ); } ); + +describe( 'isPlain', () => { + it( 'should return true for plain text', () => { + equal( isPlain( 'test' ), true ); + } ); + + it( 'should return true for only line breaks', () => { + equal( isPlain( 'test
    test' ), true ); + } ); + + it( 'should return false for formatted text', () => { + equal( isPlain( 'test' ), false ); + } ); +} ); diff --git a/blocks/api/paste/utils.js b/blocks/api/paste/utils.js index c48e6add8d049..1d17199c97bad 100644 --- a/blocks/api/paste/utils.js +++ b/blocks/api/paste/utils.js @@ -4,43 +4,47 @@ const { ELEMENT_NODE, TEXT_NODE } = window.Node; const inlineWhitelist = { - strong: [], - em: [], - del: [], - ins: [], - a: [ 'href' ], - code: [], - abbr: [ 'title' ], - sub: [], - sup: [], - br: [], + strong: {}, + em: {}, + del: {}, + ins: {}, + a: { attributes: [ 'href' ] }, + code: {}, + abbr: { attributes: [ 'title' ] }, + sub: {}, + sup: {}, + br: {}, +}; + +const inlineWrapperWhiteList = { + figcaption: {}, + h1: {}, + h2: {}, + h3: {}, + h4: {}, + h5: {}, + h6: {}, + p: { children: [ 'img' ] }, + li: { children: [ 'ul', 'ol', 'li' ] }, + pre: {}, + td: {}, + th: {}, }; const whitelist = { ...inlineWhitelist, - img: [ 'src', 'alt' ], - figure: [], - figcaption: [], - h1: [], - h2: [], - h3: [], - h4: [], - h5: [], - h6: [], - p: [], - blockquote: [], - hr: [], - ul: [], - ol: [ 'type' ], - li: [], - pre: [], - table: [], - thead: [], - tfoot: [], - tbody: [], - th: [], - tr: [], - td: [], + ...inlineWrapperWhiteList, + img: { attributes: [ 'src', 'alt' ] }, + figure: {}, + blockquote: {}, + hr: {}, + ul: {}, + ol: { attributes: [ 'type' ] }, + table: {}, + thead: {}, + tfoot: {}, + tbody: {}, + tr: {}, }; export function isWhitelisted( element ) { @@ -52,13 +56,32 @@ export function isNotWhitelisted( element ) { } export function isAttributeWhitelisted( tag, attribute ) { - return whitelist[ tag ] && whitelist[ tag ].indexOf( attribute ) !== -1; + return ( + whitelist[ tag ] && + whitelist[ tag ].attributes && + whitelist[ tag ].attributes.indexOf( attribute ) !== -1 + ); } export function isInline( node ) { return !! inlineWhitelist[ node.nodeName.toLowerCase() ]; } +export function isInlineWrapper( node ) { + return !! inlineWrapperWhiteList[ node.nodeName.toLowerCase() ]; +} + +export function isAllowedBlock( parentNode, node ) { + const parentNodeTag = parentNode.nodeName.toLowerCase(); + const nodeTag = node.nodeName.toLowerCase(); + + return ( + whitelist[ parentNodeTag ] && + whitelist[ parentNodeTag ].children && + whitelist[ parentNodeTag ].children.indexOf( nodeTag ) !== -1 + ); +} + export function isInvalidInline( element ) { if ( ! isInline( element ) ) { return false; @@ -125,12 +148,19 @@ export function isPlain( HTML ) { doc.body.normalize(); // If it's plain text, there should only be one node left. - return doc.body.childNodes.length === 1; + return doc.body.childNodes.length === 1 && doc.body.firstChild.nodeType === TEXT_NODE; } -function deepFilterHelper( nodeList, filters, doc ) { +/** + * Given node filters, deeply filters and mutates a NodeList. + * + * @param {NodeList} nodeList The nodeList to filter. + * @param {Array} filters An array of functions that can mutate with the provided node. + * @param {Document} doc The document of the nodeList. + */ +export function deepFilterNodeList( nodeList, filters, doc ) { Array.from( nodeList ).forEach( ( node ) => { - deepFilterHelper( node.childNodes, filters, doc ); + deepFilterNodeList( node.childNodes, filters, doc ); filters.forEach( ( filter ) => { // Make sure the node is still attached to the document. @@ -138,17 +168,24 @@ function deepFilterHelper( nodeList, filters, doc ) { return; } - filter( node ); + filter( node, doc ); } ); } ); } -export function deepFilter( HTML, filters = [] ) { +/** + * Given node filters, deeply filters HTML tags. + * + * @param {String} HTML The HTML to filter. + * @param {Array} filters An array of functions that can mutate with the provided node. + * @return {String} The filtered HTML. + */ +export function deepFilterHTML( HTML, filters = [] ) { const doc = document.implementation.createHTMLDocument( '' ); doc.body.innerHTML = HTML; - deepFilterHelper( doc.body.childNodes, filters, doc ); + deepFilterNodeList( doc.body.childNodes, filters, doc ); return doc.body.innerHTML; } diff --git a/blocks/api/post.pegjs b/blocks/api/post.pegjs index 8ab56578e6319..d594d504b9cd8 100644 --- a/blocks/api/post.pegjs +++ b/blocks/api/post.pegjs @@ -148,7 +148,18 @@ WP_Block_End } WP_Block_Name - = $(ASCII_Letter (ASCII_AlphaNumeric / "/" ASCII_AlphaNumeric)*) + = WP_Namespaced_Block_Name + / WP_Core_Block_Name + +WP_Namespaced_Block_Name + = $(ASCII_Letter ASCII_AlphaNumeric* "/" ASCII_Letter ASCII_AlphaNumeric*) + +WP_Core_Block_Name + = type:$(ASCII_Letter ASCII_AlphaNumeric*) + { + /** **/ + return 'core/' + type; + } WP_Block_Attributes = attrs:$("{" (!("}" WS+ """/"? "-->") .)* "}") diff --git a/blocks/api/registration.js b/blocks/api/registration.js index 5709ca747a6d9..d703f103d1668 100644 --- a/blocks/api/registration.js +++ b/blocks/api/registration.js @@ -50,6 +50,12 @@ export function registerBlockType( name, settings ) { ); return; } + if ( /[A-Z]+/.test( name ) ) { + console.error( + 'Block names must not contain uppercase characters.' + ); + return; + } if ( ! /^[a-z0-9-]+\/[a-z0-9-]+$/.test( name ) ) { console.error( 'Block names must contain a namespace prefix. Example: my-plugin/my-custom-block' diff --git a/blocks/api/serializer.js b/blocks/api/serializer.js index 9e0e38b727751..7d46d61a5f734 100644 --- a/blocks/api/serializer.js +++ b/blocks/api/serializer.js @@ -89,11 +89,11 @@ export function getSaveContent( blockType, attributes ) { * which cannot be matched from the block content. * * @param {Object} allAttributes Attributes from in-memory block data - * @param {Object} schema Block type schema + * @param {Object} blockType Block type * @returns {Object} Subset of attributes for comment serialization */ -export function getCommentAttributes( allAttributes, schema ) { - return reduce( schema, ( result, attributeSchema, key ) => { +export function getCommentAttributes( allAttributes, blockType ) { + const attributes = reduce( blockType.attributes, ( result, attributeSchema, key ) => { const value = allAttributes[ key ]; // Ignore undefined values @@ -115,6 +115,12 @@ export function getCommentAttributes( allAttributes, schema ) { result[ key ] = value; return result; }, {} ); + + if ( blockType.className !== false && allAttributes.className ) { + attributes.className = allAttributes.className; + } + + return attributes; } export function serializeAttributes( attrs ) { @@ -139,26 +145,51 @@ export function getBeautifulContent( content ) { } ); } +/** + * Given a block object, returns the Block's Inner HTML markup + * @param {Object} block Block Object + * @return {String} HTML + */ +export function getBlockContent( block ) { + const blockType = getBlockType( block.name ); + + // If block was parsed as invalid or encounters an error while generating + // save content, use original content instead to avoid content loss. + let saveContent = block.originalContent; + if ( block.isValid ) { + try { + saveContent = getSaveContent( blockType, block.attributes ); + } catch ( error ) {} + } + + return getUnknownTypeHandlerName() === block.name || ! saveContent ? saveContent : getBeautifulContent( saveContent ); +} + /** * Returns the content of a block, including comment delimiters. * - * @param {String} blockName Block name - * @param {Object} attributes Block attributes - * @param {String} content Block save content - * @return {String} Comment-delimited block content + * @param {String} rawBlockName Block name + * @param {Object} attributes Block attributes + * @param {String} content Block save content + * @return {String} Comment-delimited block content */ -export function getCommentDelimitedContent( blockName, attributes, content ) { +export function getCommentDelimitedContent( rawBlockName, attributes, content ) { const serializedAttributes = ! isEmpty( attributes ) ? serializeAttributes( attributes ) + ' ' : ''; + // strip core blocks of their namespace prefix + const blockName = rawBlockName.startsWith( 'core/' ) + ? rawBlockName.slice( 5 ) + : rawBlockName; + if ( ! content ) { return ``; } return ( `\n` + - getBeautifulContent( content ) + + content + `\n` ); } @@ -173,17 +204,8 @@ export function getCommentDelimitedContent( blockName, attributes, content ) { export function serializeBlock( block ) { const blockName = block.name; const blockType = getBlockType( blockName ); - - // If block was parsed as invalid or encounters an error while generating - // save content, use original content instead to avoid content loss. - let saveContent = block.originalContent; - if ( block.isValid ) { - try { - saveContent = getSaveContent( blockType, block.attributes ); - } catch ( error ) {} - } - - const saveAttributes = getCommentAttributes( block.attributes, blockType.attributes ); + const saveContent = getBlockContent( block ); + const saveAttributes = getCommentAttributes( block.attributes, blockType ); switch ( blockName ) { case 'core/more': diff --git a/blocks/api/test/factory.js b/blocks/api/test/factory.js index dd0c3b67e5412..0be1b0707c1f0 100644 --- a/blocks/api/test/factory.js +++ b/blocks/api/test/factory.js @@ -80,6 +80,39 @@ describe( 'block factory', () => { } ); expect( block.isValid ).toBe( true ); } ); + + it( 'should keep the className if the block supports it', () => { + registerBlockType( 'core/test-block', { + attributes: {}, + save: noop, + category: 'common', + title: 'test block', + } ); + const block = createBlock( 'core/test-block', { + className: 'chicken', + } ); + + expect( block.attributes ).toEqual( { + className: 'chicken', + } ); + expect( block.isValid ).toBe( true ); + } ); + + it( 'should not keep the className if the block supports it', () => { + registerBlockType( 'core/test-block', { + attributes: {}, + save: noop, + category: 'common', + title: 'test block', + className: false, + } ); + const block = createBlock( 'core/test-block', { + className: 'chicken', + } ); + + expect( block.attributes ).toEqual( {} ); + expect( block.isValid ).toBe( true ); + } ); } ); describe( 'switchToBlockType()', () => { diff --git a/blocks/api/test/parser.js b/blocks/api/test/parser.js index 44dc519e425c2..fd247c26cef6c 100644 --- a/blocks/api/test/parser.js +++ b/blocks/api/test/parser.js @@ -176,6 +176,31 @@ describe( 'block parser', () => { anchor: 'chicken', } ); } ); + + it( 'should parse the className if the block supports it', () => { + const blockType = { + attributes: {}, + }; + + const rawContent = '
    Ribs
    '; + const attrs = { className: 'chicken' }; + + expect( getBlockAttributes( blockType, rawContent, attrs ) ).toEqual( { + className: 'chicken', + } ); + } ); + + it( 'should not parse the className if the block supports it', () => { + const blockType = { + attributes: {}, + className: false, + }; + + const rawContent = '
    Ribs
    '; + const attrs = { className: 'chicken' }; + + expect( getBlockAttributes( blockType, rawContent, attrs ) ).toEqual( {} ); + } ); } ); describe( 'createBlockWithFallback', () => { @@ -223,7 +248,7 @@ describe( 'block parser', () => { ); expect( block.name ).toBe( 'core/unknown-block' ); expect( block.attributes.fruit ).toBe( 'Bananas' ); - expect( block.attributes.content ).toContain( 'core/test-block' ); + expect( block.attributes.content ).toContain( 'wp:test-block' ); } ); it( 'should fall back to the unknown type handler if block type not specified', () => { @@ -308,6 +333,31 @@ describe( 'block parser', () => { expect( typeof parsed[ 0 ].uid ).toBe( 'string' ); } ); + it( 'should add the core namespace to un-namespaced blocks', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + + const parsed = parse( + '\nRibs\n' + ); + + expect( parsed ).toHaveLength( 1 ); + expect( parsed[ 0 ].name ).toBe( 'core/test-block' ); + } ); + + it( 'should ignore blocks with a bad namespace', () => { + registerBlockType( 'core/test-block', defaultBlockSettings ); + + setUnknownTypeHandlerName( 'core/unknown-block' ); + + const parsed = parse( + 'Ribs' + + '

    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 -- but ', } ); - const expectedPostContent = '\n

    Ribs & Chicken

    \n'; + const expectedPostContent = '\n

    Ribs & 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, -
    +
    , + { ( ( caption && caption.length ) || !! focus ) && ( + setAttributes( { caption: value } ) } + inlineToolbar + /> + ) } + , ]; /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ } }, save( { attributes } ) { - const { align, src } = attributes; + const { align, src, caption } = attributes; return ( -
    +
    + { caption && caption.length > 0 &&
    { caption }
    } + ); }, } ); diff --git a/blocks/library/audio/style.scss b/blocks/library/audio/style.scss index d60399e36633e..addac2b7f6b74 100644 --- a/blocks/library/audio/style.scss +++ b/blocks/library/audio/style.scss @@ -5,3 +5,14 @@ .wp-block-audio .button.button-large { margin-top: 0.5em; } + +.wp-block-audio audio { + width: 100%; +} + +.wp-block-audio figcaption { + margin-top: 0.5em; + color: $dark-gray-100; + text-align: center; + font-size: $default-font-size; +} diff --git a/blocks/library/categories/index.php b/blocks/library/categories/index.php index 40503eb4a1263..490b3ed39f8b5 100644 --- a/blocks/library/categories/index.php +++ b/blocks/library/categories/index.php @@ -30,20 +30,20 @@ function gutenberg_render_block_core_categories( $attributes ) { ); if ( ! empty( $attributes['displayAsDropdown'] ) ) { - $id = 'wp-block-categories-' . $block_id; - $args['id'] = $id; + $id = 'wp-block-categories-' . $block_id; + $args['id'] = $id; $args['show_option_none'] = __( 'Select Category', 'gutenberg' ); - $wrapper_markup = '
    %2$s
    '; - $items_markup = wp_dropdown_categories( $args ); - $type = 'dropdown'; + $wrapper_markup = '
    %2$s
    '; + $items_markup = wp_dropdown_categories( $args ); + $type = 'dropdown'; if ( ! is_admin() ) { $wrapper_markup .= gutenberg_build_dropdown_script_block_core_categories( $id ); } } else { $wrapper_markup = '
      %2$s
    '; - $items_markup = wp_list_categories( $args ); - $type = 'list'; + $items_markup = wp_list_categories( $args ); + $type = 'list'; } $class = "wp-block-categories wp-block-categories-{$type} align{$align}"; diff --git a/blocks/library/gallery/block.js b/blocks/library/gallery/block.js index 19712352623a9..951ded42e5ec9 100644 --- a/blocks/library/gallery/block.js +++ b/blocks/library/gallery/block.js @@ -98,6 +98,22 @@ class GalleryBlock extends Component { const { attributes, focus, className } = this.props; const { images, columns = defaultColumnsNumber( attributes ), align, imageCrop, linkTo } = attributes; + const blockDescription = ( + +

    + {__( 'Image galleries are a great way to share groups of pictures on your site.' )} +

    +
    + ); + + const inspectorControls = ( + focus && ( + + {blockDescription} + + ) + ); + const controls = ( focus && ( @@ -133,6 +149,7 @@ class GalleryBlock extends Component { return [ controls, + inspectorControls, 1 && ( - -

    { __( 'Image galleries are a great way to share groups of pictures on your site.' ) }

    -
    + {blockDescription}

    { __( 'Gallery Settings' ) }

    array( - 'postsToShow' => array( - 'type' => 'number', + 'attributes' => array( + 'postsToShow' => array( + 'type' => 'number', 'default' => 5, ), 'displayPostDate' => array( - 'type' => 'boolean', + 'type' => 'boolean', 'default' => false, ), - 'layout' => array( - 'type' => 'string', + 'layout' => array( + 'type' => 'string', 'default' => 'list', ), - 'columns' => array( - 'type' => 'number', + 'columns' => array( + 'type' => 'number', 'default' => 3, ), - 'align' => array( - 'type' => 'string', + 'align' => array( + 'type' => 'string', 'default' => 'center', ), ), - 'render_callback' => 'gutenberg_render_block_core_latest_posts', ) ); diff --git a/blocks/test/fixtures/core__audio.html b/blocks/test/fixtures/core__audio.html index 0222d32fa8814..cfa3f20267c4b 100644 --- a/blocks/test/fixtures/core__audio.html +++ b/blocks/test/fixtures/core__audio.html @@ -1,5 +1,5 @@ -
    +
    -
    + diff --git a/blocks/test/fixtures/core__audio.json b/blocks/test/fixtures/core__audio.json index a2a9a89a963da..99b43e37d313d 100644 --- a/blocks/test/fixtures/core__audio.json +++ b/blocks/test/fixtures/core__audio.json @@ -5,8 +5,9 @@ "isValid": true, "attributes": { "src": "https://media.simplecast.com/episodes/audio/80564/draft-podcast-51-livePublish2.mp3", - "align": "right" + "align": "right", + "caption": [] }, - "originalContent": "
    \n \n
    " + "originalContent": "
    \n \n
    " } ] diff --git a/blocks/test/fixtures/core__audio.parsed.json b/blocks/test/fixtures/core__audio.parsed.json index 1066d69b9570b..b6afa780e2dea 100644 --- a/blocks/test/fixtures/core__audio.parsed.json +++ b/blocks/test/fixtures/core__audio.parsed.json @@ -4,7 +4,7 @@ "attrs": { "align": "right" }, - "rawContent": "\n
    \n \n
    \n" + "rawContent": "\n
    \n \n
    \n" }, { "attrs": {}, diff --git a/blocks/test/fixtures/core__audio.serialized.html b/blocks/test/fixtures/core__audio.serialized.html index c3bef79c980c8..d851452c2d2b2 100644 --- a/blocks/test/fixtures/core__audio.serialized.html +++ b/blocks/test/fixtures/core__audio.serialized.html @@ -1,3 +1,3 @@ - -
    - + +
    + diff --git a/blocks/test/fixtures/core__button__center.serialized.html b/blocks/test/fixtures/core__button__center.serialized.html index 0376ad37ecc3c..7b2eb1b315a85 100644 --- a/blocks/test/fixtures/core__button__center.serialized.html +++ b/blocks/test/fixtures/core__button__center.serialized.html @@ -1,3 +1,3 @@ - + - + diff --git a/blocks/test/fixtures/core__categories.serialized.html b/blocks/test/fixtures/core__categories.serialized.html index 5b9e6d885657a..7816d6b192f6d 100644 --- a/blocks/test/fixtures/core__categories.serialized.html +++ b/blocks/test/fixtures/core__categories.serialized.html @@ -1 +1 @@ - + diff --git a/blocks/test/fixtures/core__code.serialized.html b/blocks/test/fixtures/core__code.serialized.html index 5d3f951c17f71..3a45062483ea3 100644 --- a/blocks/test/fixtures/core__code.serialized.html +++ b/blocks/test/fixtures/core__code.serialized.html @@ -1,5 +1,5 @@ - +
    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 @@ - +

    Guten Berg!

    - + diff --git a/blocks/test/fixtures/core__embed.serialized.html b/blocks/test/fixtures/core__embed.serialized.html index a75136b0413ef..3996ecd6425b1 100644 --- a/blocks/test/fixtures/core__embed.serialized.html +++ b/blocks/test/fixtures/core__embed.serialized.html @@ -1,6 +1,6 @@ - +
    https://example.com/
    Embedded content from an example URL
    - + diff --git a/blocks/test/fixtures/core__gallery.serialized.html b/blocks/test/fixtures/core__gallery.serialized.html index 1703362512af8..57a4f311f52dc 100644 --- a/blocks/test/fixtures/core__gallery.serialized.html +++ b/blocks/test/fixtures/core__gallery.serialized.html @@ -1,4 +1,4 @@ - + - + diff --git a/blocks/test/fixtures/core__gallery__columns.serialized.html b/blocks/test/fixtures/core__gallery__columns.serialized.html index a7b90a472283f..33822c13f3ef9 100644 --- a/blocks/test/fixtures/core__gallery__columns.serialized.html +++ b/blocks/test/fixtures/core__gallery__columns.serialized.html @@ -1,4 +1,4 @@ - + - + diff --git a/blocks/test/fixtures/core__heading__h2-em.serialized.html b/blocks/test/fixtures/core__heading__h2-em.serialized.html index 7755d1dcf4eae..55ce73502b618 100644 --- a/blocks/test/fixtures/core__heading__h2-em.serialized.html +++ b/blocks/test/fixtures/core__heading__h2-em.serialized.html @@ -1,3 +1,3 @@ - +

    The Inserter Tool

    - + diff --git a/blocks/test/fixtures/core__heading__h2.serialized.html b/blocks/test/fixtures/core__heading__h2.serialized.html index a55b391a25057..1212bf83c3029 100644 --- a/blocks/test/fixtures/core__heading__h2.serialized.html +++ b/blocks/test/fixtures/core__heading__h2.serialized.html @@ -1,3 +1,3 @@ - +

    A picture is worth a thousand words, or so the saying goes

    - + diff --git a/blocks/test/fixtures/core__html.serialized.html b/blocks/test/fixtures/core__html.serialized.html index 4824e6e373c06..8abffc385d4e1 100644 --- a/blocks/test/fixtures/core__html.serialized.html +++ b/blocks/test/fixtures/core__html.serialized.html @@ -1,4 +1,4 @@ - +

    Some HTML code

    This text will scroll from right to left - + diff --git a/blocks/test/fixtures/core__image.serialized.html b/blocks/test/fixtures/core__image.serialized.html index 2a414855b0344..fb41212fe367e 100644 --- a/blocks/test/fixtures/core__image.serialized.html +++ b/blocks/test/fixtures/core__image.serialized.html @@ -1,3 +1,3 @@ - +
    - + diff --git a/blocks/test/fixtures/core__image__center-caption.serialized.html b/blocks/test/fixtures/core__image__center-caption.serialized.html index 53e7f036c8a95..fc8b1f4263ac2 100644 --- a/blocks/test/fixtures/core__image__center-caption.serialized.html +++ b/blocks/test/fixtures/core__image__center-caption.serialized.html @@ -1,5 +1,5 @@ - +
    Give it a try. Press the "really wide" button on the image toolbar.
    - + diff --git a/blocks/test/fixtures/core__latest-posts.serialized.html b/blocks/test/fixtures/core__latest-posts.serialized.html index 6c80203bbda2b..2c5b3f264c887 100644 --- a/blocks/test/fixtures/core__latest-posts.serialized.html +++ b/blocks/test/fixtures/core__latest-posts.serialized.html @@ -1 +1 @@ - + diff --git a/blocks/test/fixtures/core__latest-posts__displayPostDate.serialized.html b/blocks/test/fixtures/core__latest-posts__displayPostDate.serialized.html index ab37368489fda..0d80d351d6f83 100644 --- a/blocks/test/fixtures/core__latest-posts__displayPostDate.serialized.html +++ b/blocks/test/fixtures/core__latest-posts__displayPostDate.serialized.html @@ -1 +1 @@ - + diff --git a/blocks/test/fixtures/core__list__ul.serialized.html b/blocks/test/fixtures/core__list__ul.serialized.html index d5636ea9b3813..3032e179bd7a5 100644 --- a/blocks/test/fixtures/core__list__ul.serialized.html +++ b/blocks/test/fixtures/core__list__ul.serialized.html @@ -1,4 +1,4 @@ - +
    • Text & Headings
    • Images & Videos
    • @@ -7,4 +7,4 @@
    • Layout blocks, like Buttons, Hero Images, Separators, etc.
    • And Lists like this one of course :)
    - + diff --git a/blocks/test/fixtures/core__paragraph__align-right.serialized.html b/blocks/test/fixtures/core__paragraph__align-right.serialized.html index d41481a57f2e5..4ec4638ee27b8 100644 --- a/blocks/test/fixtures/core__paragraph__align-right.serialized.html +++ b/blocks/test/fixtures/core__paragraph__align-right.serialized.html @@ -1,3 +1,3 @@ - +

    ... 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...
    And more!
    - + 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 @@ - +

    Testing pullquote block...

    ...with a caption
    - + 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 @@ - +

    Paragraph one

    Paragraph two

    by whomever
    - + 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 @@ - +

    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.

    Matt Mullenweg, 2017
    - + 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 @@ - +

    There is no greater agony than bearing an untold story inside you.

    Maya Angelou
    - + 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 @@ - +
    - + diff --git a/blocks/test/fixtures/core__shortcode.serialized.html b/blocks/test/fixtures/core__shortcode.serialized.html index ed4bd8928dbee..293b6983594e7 100644 --- a/blocks/test/fixtures/core__shortcode.serialized.html +++ b/blocks/test/fixtures/core__shortcode.serialized.html @@ -1,3 +1,3 @@ - + [gallery ids="238,338"] - + diff --git a/blocks/test/fixtures/core__table.serialized.html b/blocks/test/fixtures/core__table.serialized.html index eb88517a0b91a..9fe01de807a50 100644 --- a/blocks/test/fixtures/core__table.serialized.html +++ b/blocks/test/fixtures/core__table.serialized.html @@ -1,4 +1,4 @@ - + @@ -45,4 +45,4 @@
    - + diff --git a/blocks/test/fixtures/core__text-columns.serialized.html b/blocks/test/fixtures/core__text-columns.serialized.html index fcd9d49a9e79d..cfed9fc433e6a 100644 --- a/blocks/test/fixtures/core__text-columns.serialized.html +++ b/blocks/test/fixtures/core__text-columns.serialized.html @@ -1,4 +1,4 @@ - +

    One

    @@ -7,4 +7,4 @@

    Two

    - + diff --git a/blocks/test/fixtures/core__text__converts-to-paragraph.html b/blocks/test/fixtures/core__text__converts-to-paragraph.html index f345de97ccc4d..a1d47a56e0022 100644 --- a/blocks/test/fixtures/core__text__converts-to-paragraph.html +++ b/blocks/test/fixtures/core__text__converts-to-paragraph.html @@ -1,3 +1,3 @@ -

    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.

    diff --git a/blocks/test/fixtures/core__text__converts-to-paragraph.json b/blocks/test/fixtures/core__text__converts-to-paragraph.json index 49ec2acfa5159..0efed063a93be 100644 --- a/blocks/test/fixtures/core__text__converts-to-paragraph.json +++ b/blocks/test/fixtures/core__text__converts-to-paragraph.json @@ -8,12 +8,12 @@ "This is an old-style text block. Changed to ", { "type": "code", - "children": "core/paragraph" + "children": "paragraph" }, " in #2135." ], "dropCap": false }, - "originalContent": "

    This is an old-style text block. Changed to core/paragraph in #2135.

    " + "originalContent": "

    This is an old-style text block. Changed to paragraph in #2135.

    " } ] diff --git a/blocks/test/fixtures/core__text__converts-to-paragraph.parsed.json b/blocks/test/fixtures/core__text__converts-to-paragraph.parsed.json index dd84d957d4663..129ad5eff70e4 100644 --- a/blocks/test/fixtures/core__text__converts-to-paragraph.parsed.json +++ b/blocks/test/fixtures/core__text__converts-to-paragraph.parsed.json @@ -2,7 +2,7 @@ { "blockName": "core/text", "attrs": null, - "rawContent": "\n

    This is an old-style text block. Changed to core/paragraph in #2135.

    \n" + "rawContent": "\n

    This is an old-style text block. Changed to paragraph in #2135.

    \n" }, { "attrs": {}, diff --git a/blocks/test/fixtures/core__text__converts-to-paragraph.serialized.html b/blocks/test/fixtures/core__text__converts-to-paragraph.serialized.html index c7bb894abee45..7a11c004984d1 100644 --- a/blocks/test/fixtures/core__text__converts-to-paragraph.serialized.html +++ b/blocks/test/fixtures/core__text__converts-to-paragraph.serialized.html @@ -1,3 +1,3 @@ - -

    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.

    + diff --git a/blocks/test/fixtures/core__verse.serialized.html b/blocks/test/fixtures/core__verse.serialized.html index b65e2819131c2..ff4983491f13d 100644 --- a/blocks/test/fixtures/core__verse.serialized.html +++ b/blocks/test/fixtures/core__verse.serialized.html @@ -1,3 +1,3 @@ - +
    A verse
    And more!
    - + 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 ) } + + { renderContent( args ) } + ); diff --git a/components/dropdown/test/index.js b/components/dropdown/test/index.js index 9c936342703ba..3ba2557d3fad7 100644 --- a/components/dropdown/test/index.js +++ b/components/dropdown/test/index.js @@ -16,7 +16,7 @@ describe( 'Dropdown', () => { renderToggle={ ( { isOpen, onToggle } ) => ( ) } - renderContent={ () => 'content' } + renderContent={ () => null } /> ); const button = wrapper.find( 'button' ); @@ -38,7 +38,7 @@ describe( 'Dropdown', () => { , , ] } - renderContent={ () => 'content' } + renderContent={ () => null } /> ); const openButton = wrapper.find( '.open' ); diff --git a/composer.lock b/composer.lock index 1e3f2145762e7..7f48eb72be5a3 100644 --- a/composer.lock +++ b/composer.lock @@ -9,24 +9,26 @@ "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.4.2", + "version": "v0.4.3", "source": { "type": "git", "url": "https://github.com/DealerDirect/phpcodesniffer-composer-installer.git", - "reference": "17130f536db62570bcfc5cce59464b36e82eb092" + "reference": "63c0ec0ac286d31651d3c70e5bf76ad87db3ba23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/DealerDirect/phpcodesniffer-composer-installer/zipball/17130f536db62570bcfc5cce59464b36e82eb092", - "reference": "17130f536db62570bcfc5cce59464b36e82eb092", + "url": "https://api.github.com/repos/DealerDirect/phpcodesniffer-composer-installer/zipball/63c0ec0ac286d31651d3c70e5bf76ad87db3ba23", + "reference": "63c0ec0ac286d31651d3c70e5bf76ad87db3ba23", "shasum": "" }, "require": { "composer-plugin-api": "^1.0", + "php": "^5.3|^7", "squizlabs/php_codesniffer": "*" }, "require-dev": { - "composer/composer": "*" + "composer/composer": "*", + "wimg/php-compatibility": "^8.0" }, "suggest": { "dealerdirect/qa-tools": "All the PHP QA tools you'll need" @@ -71,20 +73,20 @@ "stylecheck", "tests" ], - "time": "2017-08-16T10:25:17+00:00" + "time": "2017-09-18T07:49:36+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.0.2", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "c7594a88ae75401e8f8d0bd4deb8431b39045c51" + "reference": "3c2d0a0fe39684ba0c1eb842a6a775d0b938d699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/c7594a88ae75401e8f8d0bd4deb8431b39045c51", - "reference": "c7594a88ae75401e8f8d0bd4deb8431b39045c51", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/3c2d0a0fe39684ba0c1eb842a6a775d0b938d699", + "reference": "3c2d0a0fe39684ba0c1eb842a6a775d0b938d699", "shasum": "" }, "require": { @@ -94,7 +96,7 @@ "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "~4.0" + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" }, "bin": [ "bin/phpcs", @@ -122,7 +124,7 @@ "phpcs", "standards" ], - "time": "2017-07-18T01:12:32+00:00" + "time": "2017-09-19T22:47:14+00:00" }, { "name": "wimg/php-compatibility", @@ -174,7 +176,7 @@ "phpcs", "standards" ], - "time": "2017-08-07T19:44:46+00:00" + "time": "2017-08-07 19:44:46" }, { "name": "wp-coding-standards/wpcs", @@ -182,12 +184,12 @@ "source": { "type": "git", "url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git", - "reference": "7f208f3c6d740f7f4a9e2e3906dcc5e145dd771b" + "reference": "b19535707965e6f185f6fc678b044d8c7f0b1b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/7f208f3c6d740f7f4a9e2e3906dcc5e145dd771b", - "reference": "7f208f3c6d740f7f4a9e2e3906dcc5e145dd771b", + "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/b19535707965e6f185f6fc678b044d8c7f0b1b50", + "reference": "b19535707965e6f185f6fc678b044d8c7f0b1b50", "shasum": "" }, "require": { @@ -195,7 +197,7 @@ "squizlabs/php_codesniffer": "^2.9.0 || ^3.0.2" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1" + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3" }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -214,7 +216,7 @@ "standards", "wordpress" ], - "time": "2017-08-30T06:57:47+00:00" + "time": "2017-10-09T13:04:35+00:00" } ], "aliases": [], diff --git a/editor/actions.js b/editor/actions.js index 445a91787839a..072232bc7d2f7 100644 --- a/editor/actions.js +++ b/editor/actions.js @@ -11,9 +11,9 @@ import { partial, castArray } from 'lodash'; * @param {Object} post Post object * @return {Object} Action object */ -export function setInitialPost( post ) { +export function setupEditor( post ) { return { - type: 'SET_INITIAL_POST', + type: 'SETUP_EDITOR', post, }; } @@ -62,7 +62,7 @@ export function resetBlocks( blocks ) { } /** - * Returns an action object used in signalling that the block with the + * Returns an action object used in signalling that the block attributes with the * specified UID has been updated. * * @param {String} uid Block UID @@ -77,6 +77,22 @@ export function updateBlockAttributes( uid, attributes ) { }; } +/** + * Returns an action object used in signalling that the block with the + * specified UID has been updated. + * + * @param {String} uid Block UID + * @param {Object} updates Block attributes to be merged + * @return {Object} Action object + */ +export function updateBlock( uid, updates ) { + return { + type: 'UPDATE_BLOCK', + uid, + updates, + }; +} + export function focusBlock( uid, config ) { return { type: 'UPDATE_FOCUS', @@ -241,6 +257,19 @@ export function removeBlock( uid ) { return removeBlocks( [ uid ] ); } +/** + * Returns an action object used to toggle the block editing mode (visual/html) + * + * @param {String} uid Block UID + * @return {Object} Action object + */ +export function toggleBlockMode( uid ) { + return { + type: 'TOGGLE_BLOCK_MODE', + uid, + }; +} + /** * Returns an action object used in signalling that the user has begun to type. * @@ -263,6 +292,30 @@ export function stopTyping() { }; } +/** + * Returns an action object used in signalling that the user toggled the sidebar + * + * @return {Object} Action object + */ +export function toggleSidebar() { + return { + type: 'TOGGLE_SIDEBAR', + }; +} + +/** + * Returns an action object used in signalling that the user switched the active sidebar tab panel + * + * @param {String} panel The panel name + * @return {Object} Action object + */ +export function setActivePanel( panel ) { + return { + type: 'SET_ACTIVE_PANEL', + panel, + }; +} + /** * Returns an action object used in signalling that the user toggled a sidebar panel * diff --git a/editor/assets/stylesheets/_z-index.scss b/editor/assets/stylesheets/_z-index.scss index 8d43f00a0f173..7352b15a88e40 100644 --- a/editor/assets/stylesheets/_z-index.scss +++ b/editor/assets/stylesheets/_z-index.scss @@ -21,6 +21,10 @@ $z-layers: ( '.editor-header': 20, '.editor-text-editor__formatting': 20, + // Show Block SettingsToggle should under its content + '.editor-block-settings-menu__toggle': 1, + '.editor-block-settings-menu__content': 2, + // Show drop zone above most standard content, but below any overlays '.components-drop-zone': 100, '.components-drop-zone__content': 110, diff --git a/editor/block-mover/style.scss b/editor/block-mover/style.scss index b93ca93655320..ed9f6c2e9c2f2 100644 --- a/editor/block-mover/style.scss +++ b/editor/block-mover/style.scss @@ -1,7 +1,7 @@ .editor-block-mover { position: absolute; top: 0; - left: 0; + left: 4px; height: $text-editor-font-size * 4; // same height as an empty paragraph padding: 6px 14px 6px 0; // handles hover area z-index: z-index( '.editor-block-mover' ); diff --git a/editor/block-settings-menu/content.js b/editor/block-settings-menu/content.js new file mode 100644 index 0000000000000..e5d278a4447fd --- /dev/null +++ b/editor/block-settings-menu/content.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import { connect } from 'react-redux'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { IconButton } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { isEditorSidebarOpened } from '../selectors'; +import { selectBlock, removeBlock, toggleSidebar, setActivePanel, toggleBlockMode } from '../actions'; + +function BlockSettingsMenuContent( { onDelete, onSelect, isSidebarOpened, onToggleSidebar, onShowInspector, onToggleMode } ) { + const toggleInspector = () => { + onSelect(); + onShowInspector(); + if ( ! isSidebarOpened ) { + onToggleSidebar(); + } + }; + + return ( +
    + + + +
    + ); +} + +export default connect( + ( state ) => ( { + isSidebarOpened: isEditorSidebarOpened( state ), + } ), + ( dispatch, ownProps ) => ( { + onDelete() { + dispatch( removeBlock( ownProps.uid ) ); + }, + onSelect() { + dispatch( selectBlock( ownProps.uid ) ); + }, + onShowInspector() { + dispatch( setActivePanel( 'block' ) ); + }, + onToggleSidebar() { + dispatch( toggleSidebar() ); + }, + onToggleMode() { + dispatch( toggleBlockMode( ownProps.uid ) ); + }, + } ) +)( BlockSettingsMenuContent ); diff --git a/editor/block-settings-menu/index.js b/editor/block-settings-menu/index.js index d6121c7c290b4..c4071727d8d40 100644 --- a/editor/block-settings-menu/index.js +++ b/editor/block-settings-menu/index.js @@ -1,6 +1,7 @@ /** * External dependencies */ +import classnames from 'classnames'; import { connect } from 'react-redux'; /** @@ -8,63 +9,58 @@ import { connect } from 'react-redux'; */ import { __ } from '@wordpress/i18n'; import { IconButton } from '@wordpress/components'; +import { Component } from '@wordpress/element'; /** * Internal dependencies */ import './style.scss'; -import { isEditorSidebarOpened } from '../selectors'; +import BlockSettingsMenuContent from './content'; import { selectBlock } from '../actions'; -function BlockSettingsMenu( { onDelete, onSelect, isSidebarOpened, toggleSidebar, setActivePanel } ) { - const toggleInspector = () => { - onSelect(); - setActivePanel(); - if ( ! isSidebarOpened ) { - toggleSidebar(); - } - }; +class BlockSettingsMenu extends Component { + constructor() { + super( ...arguments ); + this.toggleMenu = this.toggleMenu.bind( this ); + this.state = { + opened: false, + }; + } - return ( -
    - - -
    - ); + toggleMenu() { + this.props.onSelect(); + this.setState( ( state ) => ( { + opened: ! state.opened, + } ) ); + } + + render() { + const { opened } = this.state; + const { uid } = this.props; + const toggleClassname = classnames( 'editor-block-settings-menu__toggle', 'editor-block-settings-menu__control', { + 'is-opened': opened, + } ); + + return ( +
    + + + { opened && } +
    + ); + } } export default connect( - ( state ) => ( { - isSidebarOpened: isEditorSidebarOpened( state ), - } ), + undefined, ( dispatch, ownProps ) => ( { - onDelete() { - dispatch( { - type: 'REMOVE_BLOCKS', - uids: [ ownProps.uid ], - } ); - }, onSelect() { dispatch( selectBlock( ownProps.uid ) ); }, - setActivePanel() { - dispatch( { - type: 'SET_ACTIVE_PANEL', - panel: 'block', - } ); - }, - toggleSidebar() { - dispatch( { type: 'TOGGLE_SIDEBAR' } ); - }, } ) )( BlockSettingsMenu ); diff --git a/editor/block-settings-menu/style.scss b/editor/block-settings-menu/style.scss index 39176b763dece..5e16977989cd8 100644 --- a/editor/block-settings-menu/style.scss +++ b/editor/block-settings-menu/style.scss @@ -1,15 +1,31 @@ .editor-block-settings-menu { position: absolute; top: 0; - right: 0; - height: $text-editor-font-size * 4; // same height as an empty paragraph + right: 4px; padding: 6px 0 6px 14px; // handles hover area // Mobile display: none; @include break-small { - display: block; + display: flex; + flex-direction: column; + } +} + +.editor-block-settings-menu__content { + margin-top: 8px; + z-index: z-index( '.editor-block-settings-menu__content' ); +} + +.editor-block-settings-menu__toggle { + transform: rotate( 90deg ); + transition-duration: 0.3s; + transition-property: transform; + z-index: z-index( '.editor-block-settings-menu__toggle' ); + + &.is-opened { + transform: rotate( 0 ); } } @@ -35,7 +51,11 @@ display: block; } - &:first-child { - margin-bottom: 4px; + .editor-block-settings-menu__content & { + margin-bottom: 8px; + } + + .editor-block-settings-menu__content &:last-child { + margin-bottom: 0; } } diff --git a/editor/effects.js b/editor/effects.js index 163982fba0e50..08485ac6de563 100644 --- a/editor/effects.js +++ b/editor/effects.js @@ -251,7 +251,7 @@ export default { dispatch( savePost() ); }, - SET_INITIAL_POST( action ) { + SETUP_EDITOR( action ) { const { post } = action; const effects = []; diff --git a/editor/header/index.js b/editor/header/index.js index aa0f6a0e0f8b7..0d48769319655 100644 --- a/editor/header/index.js +++ b/editor/header/index.js @@ -19,7 +19,7 @@ import PreviewButton from './preview-button'; import ModeSwitcher from './mode-switcher'; import Inserter from '../inserter'; import { getMultiSelectedBlockUids, hasEditorUndo, hasEditorRedo, isEditorSidebarOpened } from '../selectors'; -import { clearSelectedBlock } from '../actions'; +import { clearSelectedBlock, toggleSidebar, removeBlocks } from '../actions'; function Header( { multiSelectedBlockUids, @@ -29,7 +29,7 @@ function Header( { redo, hasRedo, hasUndo, - toggleSidebar, + onToggleSidebar, isSidebarOpened, } ) { const count = multiSelectedBlockUids.length; @@ -90,7 +90,7 @@ function Header( { @@ -109,12 +109,9 @@ export default connect( } ), ( dispatch ) => ( { onDeselect: () => dispatch( clearSelectedBlock() ), - onRemove: ( uids ) => dispatch( { - type: 'REMOVE_BLOCKS', - uids, - } ), + onRemove: ( uids ) => dispatch( removeBlocks( uids ) ), undo: () => dispatch( { type: 'UNDO' } ), redo: () => dispatch( { type: 'REDO' } ), - toggleSidebar: () => dispatch( { type: 'TOGGLE_SIDEBAR' } ), + onToggleSidebar: () => dispatch( toggleSidebar() ), } ) )( Header ); diff --git a/editor/header/style.scss b/editor/header/style.scss index 37f4da4e05cc5..3b90340f7a726 100644 --- a/editor/header/style.scss +++ b/editor/header/style.scss @@ -105,6 +105,10 @@ .editor-mode-switcher { margin-left: $item-spacing; + + .components-button svg { + transform: rotate( 90deg ); + } } .editor-header__left .components-button { diff --git a/editor/index.js b/editor/index.js index edb969d2ce5ce..ec61bdb13c6e3 100644 --- a/editor/index.js +++ b/editor/index.js @@ -1,19 +1,13 @@ /** * External dependencies */ -import { bindActionCreators } from 'redux'; -import { Provider as ReduxProvider } from 'react-redux'; -import { Provider as SlotFillProvider } from 'react-slot-fill'; -import { flow, pick } from 'lodash'; import moment from 'moment-timezone'; import 'moment-timezone/moment-timezone-utils'; /** * WordPress dependencies */ -import { EditableProvider } from '@wordpress/blocks'; -import { createElement, render } from '@wordpress/element'; -import { APIProvider, PopoverProvider, DropZoneProvider } from '@wordpress/components'; +import { render } from '@wordpress/element'; import { settings as dateSettings } from '@wordpress/date'; /** @@ -21,25 +15,7 @@ import { settings as dateSettings } from '@wordpress/date'; */ import './assets/stylesheets/main.scss'; import Layout from './layout'; -import createReduxStore from './store'; -import { setInitialPost, undo } from './actions'; -import EditorSettingsProvider from './settings/provider'; - -/** - * The default editor settings - * You can override any default settings when calling createEditorInstance - * - * wideImages boolean Enable/Disable Wide/Full Alignments - * - * @var {Object} DEFAULT_SETTINGS - */ -const DEFAULT_SETTINGS = { - wideImages: false, - - // This is current max width of the block inner area - // It's used to constraint image resizing and this value could be overriden later by themes - maxWidth: 608, -}; +import EditorProvider from './provider'; // Configure moment globally moment.locale( dateSettings.l10n.locale ); @@ -57,6 +33,15 @@ if ( dateSettings.timezone.string ) { moment.tz.setDefault( 'WP' ); } +/** + * Configure heartbeat to refresh the wp-api nonce, keeping the editor authorization intact. + */ +window.jQuery( document ).on( 'heartbeat-tick', ( event, response ) => { + if ( response[ 'rest-nonce' ] ) { + window.wpApiSettings.nonce = response[ 'rest-nonce' ]; + } +} ); + /** * Initializes and returns an instance of Editor. * @@ -65,88 +50,12 @@ if ( dateSettings.timezone.string ) { * @param {?Object} settings Editor settings object */ export function createEditorInstance( id, post, settings ) { - const store = createReduxStore(); const target = document.getElementById( id ); - settings = { - ...DEFAULT_SETTINGS, - ...settings, - }; - - store.dispatch( { type: 'SETUP_EDITOR' } ); - - store.dispatch( setInitialPost( post ) ); - - const providers = [ - // Redux provider: - // - // - context.store - [ - ReduxProvider, - { store }, - ], - - // Slot / Fill provider: - // - // - context.slots - // - context.fills - [ - SlotFillProvider, - ], - - // Editable provider: - // - // - context.onUndo - [ - EditableProvider, - bindActionCreators( { - onUndo: undo, - }, store.dispatch ), - ], - - // Editor settings provider: - // - // - context.editor - [ - EditorSettingsProvider, - { settings }, - ], - - // Popover provider: - // - // - context.popoverTarget - [ - PopoverProvider, - { target }, - ], - - // APIProvider - // - // - context.getAPISchema - // - context.getAPIPostTypeRestBaseMapping - // - context.getAPITaxonomyRestBaseMapping - [ - APIProvider, - { - ...wpApiSettings, - ...pick( wp.api, [ - 'postTypeRestBaseMapping', - 'taxonomyRestBaseMapping', - ] ), - }, - ], - - // DropZone provider: - [ - DropZoneProvider, - ], - ]; - - const createEditorElement = flow( - providers.map( ( [ Component, props ] ) => ( - ( children ) => createElement( Component, props, children ) - ) ) + render( + + + , + target ); - - render( createEditorElement( ), target ); } diff --git a/editor/modes/visual-editor/block-html.js b/editor/modes/visual-editor/block-html.js new file mode 100644 index 0000000000000..4a6e62271d239 --- /dev/null +++ b/editor/modes/visual-editor/block-html.js @@ -0,0 +1,75 @@ +/** + * WordPress Dependencies + */ +import { isEqual } from 'lodash'; +import { Component } from '@wordpress/element'; +import { getBlockContent, getSourcedAttributes, getBlockType, isValidBlock } from '@wordpress/blocks'; + +/** + * External Dependencies + */ +import { connect } from 'react-redux'; +import TextareaAutosize from 'react-autosize-textarea'; + +/** + * Internal Dependencies + */ +import { updateBlock } from '../../actions'; +import { getBlock } from '../../selectors'; + +class BlockHTML extends Component { + constructor( props ) { + super( ...arguments ); + this.onChange = this.onChange.bind( this ); + this.onBlur = this.onBlur.bind( this ); + this.state = { + html: props.block.isValid ? getBlockContent( props.block ) : props.block.originalContent, + }; + } + + componentWillReceiveProps( nextProps ) { + if ( ! isEqual( nextProps.block.attributes, this.props.block.attributes ) ) { + this.setState( { + html: getBlockContent( nextProps.block ), + } ); + } + } + + onBlur() { + const blockType = getBlockType( this.props.block.name ); + const sourcedAttributes = getSourcedAttributes( this.state.html, blockType.attributes ); + const attributes = { + ...this.props.block.attributes, + ...sourcedAttributes, + }; + const isValid = isValidBlock( this.state.html, blockType, attributes ); + this.props.onChange( this.props.uid, attributes, this.state.html, isValid ); + } + + onChange( event ) { + this.setState( { html: event.target.value } ); + } + + render() { + const { html } = this.state; + return ( + + ); + } +} + +export default connect( + ( state, ownProps ) => ( { + block: getBlock( state, ownProps.uid ), + } ), + { + onChange( uid, attributes, originalContent, isValid ) { + return updateBlock( uid, { attributes, originalContent, isValid } ); + }, + } +)( BlockHTML ); diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 773efc9bdb439..e8c41c96ae32c 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -20,6 +20,7 @@ import InvalidBlockWarning from './invalid-block-warning'; import BlockCrashWarning from './block-crash-warning'; import BlockCrashBoundary from './block-crash-boundary'; import BlockDropZone from './block-drop-zone'; +import BlockHtml from './block-html'; import BlockMover from '../../block-mover'; import BlockRightMenu from '../../block-settings-menu'; import BlockToolbar from '../../block-toolbar'; @@ -49,6 +50,7 @@ import { isBlockSelected, isFirstMultiSelectedBlock, isTyping, + getBlockMode, } from '../../selectors'; const { BACKSPACE, ESCAPE, DELETE, ENTER } = keycodes; @@ -85,6 +87,9 @@ class VisualEditorBlock extends Component { if ( this.props.isTyping ) { document.addEventListener( 'mousemove', this.stopTypingOnMouseMove ); } + + // Not Ideal, but it's the easiest way to get the scrollable container + this.editorLayout = document.querySelector( '.editor-layout__editor' ); } componentWillReceiveProps( newProps ) { @@ -100,10 +105,9 @@ class VisualEditorBlock extends Component { componentDidUpdate( prevProps ) { // Preserve scroll prosition when block rearranged if ( this.previousOffset ) { - window.scrollTo( - window.scrollX, - window.scrollY + this.node.getBoundingClientRect().top - this.previousOffset - ); + this.editorLayout.scrollTop = this.editorLayout.scrollTop + + this.node.getBoundingClientRect().top + - this.previousOffset; this.previousOffset = null; } @@ -278,7 +282,7 @@ class VisualEditorBlock extends Component { } render() { - const { block, multiSelectedBlockUids, order } = this.props; + const { block, multiSelectedBlockUids, order, mode } = this.props; const { name: blockName, isValid } = block; const blockType = getBlockType( blockName ); // translators: %s: Type of block (i.e. Text, Image etc) @@ -341,7 +345,7 @@ class VisualEditorBlock extends Component { { ( showUI || isHovered ) && } { ( showUI || isHovered ) && } - { showUI && isValid && } + { showUI && isValid && mode === 'visual' && } { isFirstMultiSelected && ( @@ -353,32 +357,33 @@ class VisualEditorBlock extends Component { className="editor-visual-editor__block-edit" > - { isValid - ? ( - - ) - : [ - createElement( blockType.save, { - key: 'invalid-preview', - attributes: block.attributes, - className, - } ), - , - ] - } + { isValid && mode === 'visual' && ( + + ) } + { isValid && mode === 'html' && ( + + ) } + { ! isValid && [ + createElement( blockType.save, { + key: 'invalid-preview', + attributes: block.attributes, + className, + } ), + , + ] } { !! error && } @@ -403,6 +408,7 @@ export default connect( order: getBlockIndex( state, ownProps.uid ), multiSelectedBlockUids: getMultiSelectedBlockUids( state ), meta: getEditedPostAttribute( state, 'meta' ), + mode: getBlockMode( state, ownProps.uid ), }; }, ( dispatch, ownProps ) => ( { diff --git a/editor/modes/visual-editor/index.js b/editor/modes/visual-editor/index.js index 6e31faff576e3..fbafded4aceb8 100644 --- a/editor/modes/visual-editor/index.js +++ b/editor/modes/visual-editor/index.js @@ -34,14 +34,6 @@ class VisualEditor extends Component { this.deleteSelectedBlocks = this.deleteSelectedBlocks.bind( this ); } - componentDidMount() { - document.addEventListener( 'keydown', this.onKeyDown ); - } - - componentWillUnmount() { - document.removeEventListener( 'keydown', this.onKeyDown ); - } - bindContainer( ref ) { this.container = ref; } @@ -91,7 +83,6 @@ class VisualEditor extends Component { className="editor-visual-editor" onMouseDown={ this.onClick } onTouchStart={ this.onClick } - onKeyDown={ this.onKeyDown } ref={ this.bindContainer } > - - { __( 'Paragraph' ) } - - - { __( 'Image' ) } - + { mostFrequentlyUsedBlocks && mostFrequentlyUsedBlocks.map( + ( block ) => + this.insertBlock( block.name ) } + label={ sprintf( __( 'Insert %s' ), block.title ) } + > + { block.title } + + ) } ); } } export default connect( - null, + ( state ) => { + return { + mostFrequentlyUsedBlocks: getMostFrequentlyUsedBlocks( state ), + }; + }, { onInsertBlock: insertBlock }, )( VisualEditorInserter ); diff --git a/editor/modes/visual-editor/style.scss b/editor/modes/visual-editor/style.scss index b3b32fc2fdcb8..00a28e6d91b77 100644 --- a/editor/modes/visual-editor/style.scss +++ b/editor/modes/visual-editor/style.scss @@ -93,7 +93,7 @@ &.is-multi-selected:before { background: $blue-medium-100; outline: 2px solid $blue-medium-200; - transition: 0.2s outline; + transition: 0s outline; } .iframe-overlay { @@ -421,3 +421,22 @@ .visual-editor__invalid-block-warning-buttons .components-button { margin-bottom: 5px; } + +.editor-visual-editor__block .blocks-visual-editor__block-html-textarea { + display: block; + margin: 0; + width: 100%; + border: none; + outline: none; + box-shadow: none; + resize: none; + overflow: hidden; + font-family: $editor-html-font; + font-size: $text-editor-font-size; + line-height: 150%; + transition: padding .2s linear; + + &:focus { + box-shadow: none; + } +} diff --git a/editor/modes/visual-editor/test/inserter.js b/editor/modes/visual-editor/test/inserter.js index 8d16992690820..a4b6c7f3a7c85 100644 --- a/editor/modes/visual-editor/test/inserter.js +++ b/editor/modes/visual-editor/test/inserter.js @@ -3,6 +3,11 @@ */ import { shallow } from 'enzyme'; +/** + * WordPress dependencies + */ +import { getBlockType } from '@wordpress/blocks'; + /** * Internal dependencies */ @@ -26,11 +31,18 @@ describe( 'VisualEditorInserter', () => { expect( wrapper.state( 'isShowingControls' ) ).toBe( false ); } ); - it( 'should insert paragraph block', () => { + it( 'should insert frequently used blocks', () => { const onInsertBlock = jest.fn(); + const mostFrequentlyUsedBlocks = [ getBlockType( 'core/paragraph' ), getBlockType( 'core/image' ) ]; const wrapper = shallow( - + ); + wrapper.state.preferences = { + blockUsage: { + 'core/paragraph': 42, + 'core/image': 34, + }, + }; wrapper .findWhere( ( node ) => node.prop( 'children' ) === 'Paragraph' ) @@ -39,18 +51,4 @@ describe( 'VisualEditorInserter', () => { expect( onInsertBlock ).toHaveBeenCalled(); expect( onInsertBlock.mock.calls[ 0 ][ 0 ].name ).toBe( 'core/paragraph' ); } ); - - it( 'should insert image block', () => { - const onInsertBlock = jest.fn(); - const wrapper = shallow( - - ); - - wrapper - .findWhere( ( node ) => node.prop( 'children' ) === 'Image' ) - .simulate( 'click' ); - - expect( onInsertBlock ).toHaveBeenCalled(); - expect( onInsertBlock.mock.calls[ 0 ][ 0 ].name ).toBe( 'core/image' ); - } ); } ); diff --git a/editor/provider/index.js b/editor/provider/index.js new file mode 100644 index 0000000000000..bc24b0bd220cd --- /dev/null +++ b/editor/provider/index.js @@ -0,0 +1,143 @@ +/** + * External dependencies + */ +import { bindActionCreators } from 'redux'; +import { Provider as ReduxProvider } from 'react-redux'; +import { Provider as SlotFillProvider } from 'react-slot-fill'; +import { flow, pick, noop } from 'lodash'; + +/** + * WordPress Dependencies + */ +import { createElement, Component } from '@wordpress/element'; +import { EditableProvider } from '@wordpress/blocks'; +import { APIProvider, PopoverProvider, DropZoneProvider } from '@wordpress/components'; + +/** + * Internal Dependencies + */ +import { setupEditor, undo } from '../actions'; +import createReduxStore from '../store'; + +/** + * The default editor settings + * You can override any default settings when calling createEditorInstance + * + * wideImages boolean Enable/Disable Wide/Full Alignments + * + * @var {Object} DEFAULT_SETTINGS + */ +const DEFAULT_SETTINGS = { + wideImages: false, + + // This is current max width of the block inner area + // It's used to constraint image resizing and this value could be overriden later by themes + maxWidth: 608, +}; + +class EditorProvider extends Component { + constructor( props ) { + super( ...arguments ); + + const store = createReduxStore(); + store.dispatch( setupEditor( props.post ) ); + + this.store = store; + this.settings = { + ...DEFAULT_SETTINGS, + ...props.settings, + }; + this.target = props.target; + } + + getChildContext() { + return { + editor: this.settings, + }; + } + + componentWillReceiveProps( nextProps ) { + if ( + nextProps.store !== this.props.store || + nextProps.settings !== this.props.settings || + nextProps.target !== this.props.target + ) { + // eslint-disable-next-line no-console + console.error( 'The Editor Provider Props are immutable.' ); + } + } + + render() { + const { children } = this.props; + const providers = [ + // Redux provider: + // + // - context.store + [ + ReduxProvider, + { store: this.store }, + ], + + // Slot / Fill provider: + // + // - context.slots + // - context.fills + [ + SlotFillProvider, + ], + + // Editable provider: + // + // - context.onUndo + [ + EditableProvider, + bindActionCreators( { + onUndo: undo, + }, this.store.dispatch ), + ], + + // Popover provider: + // + // - context.popoverTarget + [ + PopoverProvider, + { target: this.target }, + ], + + // APIProvider + // + // - context.getAPISchema + // - context.getAPIPostTypeRestBaseMapping + // - context.getAPITaxonomyRestBaseMapping + [ + APIProvider, + { + ...wpApiSettings, + ...pick( wp.api, [ + 'postTypeRestBaseMapping', + 'taxonomyRestBaseMapping', + ] ), + }, + ], + + // DropZone provider: + [ + DropZoneProvider, + ], + ]; + + const createEditorElement = flow( + providers.map( ( [ Provider, props ] ) => ( + ( arg ) => createElement( Provider, props, arg ) + ) ) + ); + + return createEditorElement( children ); + } +} + +EditorProvider.childContextTypes = { + editor: noop, +}; + +export default EditorProvider; diff --git a/editor/reducer.js b/editor/reducer.js index b761af8502cfc..df8a45f1015ff 100644 --- a/editor/reducer.js +++ b/editor/reducer.js @@ -135,6 +135,20 @@ export const editor = combineUndoableReducers( { }, }; + case 'UPDATE_BLOCK': + // Ignore updates if block isn't known + if ( ! state[ action.uid ] ) { + return state; + } + + return { + ...state, + [ action.uid ]: { + ...state[ action.uid ], + ...action.updates, + }, + }; + case 'INSERT_BLOCKS': return { ...state, @@ -378,6 +392,18 @@ export function hoveredBlock( state = null, action ) { return state; } +export function blocksMode( state = {}, action ) { + if ( action.type === 'TOGGLE_BLOCK_MODE' ) { + const { uid } = action; + return { + ...state, + [ uid ]: state[ uid ] && state[ uid ] === 'html' ? 'visual' : 'html', + }; + } + + return state; +} + /** * Reducer returning the block insertion point * @@ -530,6 +556,7 @@ export default optimist( combineReducers( { isTyping, blockSelection, hoveredBlock, + blocksMode, showInsertionPoint, preferences, panel, diff --git a/editor/selectors.js b/editor/selectors.js index bed57fdcd9efc..09df4ed4185b8 100644 --- a/editor/selectors.js +++ b/editor/selectors.js @@ -11,6 +11,8 @@ import { reduce, some, values, + keys, + without, } from 'lodash'; import createSelector from 'rememo'; @@ -21,6 +23,11 @@ import { serialize, getBlockType } from '@wordpress/blocks'; import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; +/*** + * Module constants + */ +const MAX_FREQUENT_BLOCKS = 3; + /** * Returns the current editing mode. * @@ -704,6 +711,16 @@ export function getBlockFocus( state, uid ) { return state.blockSelection.focus; } +/** + * Returns thee block's editing mode + * + * @param {Object} state Global application state + * @param {String} uid Block unique ID + * @return {Object} Block editing mode + */ +export function getBlockMode( state, uid ) { + return state.blocksMode[ uid ] || 'visual'; +} /** * Returns true if the user is typing, or false otherwise. @@ -854,3 +871,23 @@ export function getRecentlyUsedBlocks( state ) { // resolves the block names in the state to the block type settings return state.preferences.recentlyUsedBlocks.map( blockType => getBlockType( blockType ) ); } + +/** + * Resolves the block usage stats into a list of the most frequently used blocks. + * Memoized so we're not generating block lists every time we render the list + * in the inserter. + * + * @param {Object} state Global application state + * @return {Array} List of block type settings + */ +export const getMostFrequentlyUsedBlocks = createSelector( + ( state ) => { + const { blockUsage } = state.preferences; + const orderedByUsage = keys( blockUsage ).sort( ( a, b ) => blockUsage[ b ] - blockUsage[ a ] ); + // add in paragraph and image blocks if they're not already in the usage data + return [ ...orderedByUsage, ...without( [ 'core/paragraph', 'core/image' ], ...orderedByUsage ) ] + .slice( 0, MAX_FREQUENT_BLOCKS ) + .map( blockType => getBlockType( blockType ) ); + }, + ( state ) => state.preferences.blockUsage +); diff --git a/editor/settings/provider.js b/editor/settings/provider.js deleted file mode 100644 index 742e6c18dffc5..0000000000000 --- a/editor/settings/provider.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * External dependencies - */ -import { noop } from 'lodash'; - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; - -/** - * The Editor Settings Provider allows any compoent to access the editor settings - */ -class EditorSettingsProvider extends Component { - getChildContext() { - return { - editor: this.props.settings, - }; - } - - render() { - return this.props.children; - } -} - -EditorSettingsProvider.childContextTypes = { - editor: noop, -}; - -export default EditorSettingsProvider; diff --git a/editor/sidebar/header.js b/editor/sidebar/header.js index 8f589fe84b0ba..3970128ef4061 100644 --- a/editor/sidebar/header.js +++ b/editor/sidebar/header.js @@ -13,8 +13,9 @@ import { IconButton } from '@wordpress/components'; * Internal Dependencies */ import { getActivePanel } from '../selectors'; +import { toggleSidebar, setActivePanel } from '../actions'; -const SidebarHeader = ( { panel, onSetPanel, toggleSidebar } ) => { +const SidebarHeader = ( { panel, onSetPanel, onToggleSidebar } ) => { return (
    @@ -45,12 +46,7 @@ export default connect( panel: getActivePanel( state ), } ), ( dispatch ) => ( { - onSetPanel( panel ) { - dispatch( { - type: 'SET_ACTIVE_PANEL', - panel: panel, - } ); - }, - toggleSidebar: () => dispatch( { type: 'TOGGLE_SIDEBAR' } ), + onSetPanel: ( panel ) => dispatch( setActivePanel( panel ) ), + onToggleSidebar: () => dispatch( toggleSidebar() ), } ) )( SidebarHeader ); diff --git a/editor/test/effects.js b/editor/test/effects.js index a5064e64db78c..b53134146aef4 100644 --- a/editor/test/effects.js +++ b/editor/test/effects.js @@ -249,8 +249,8 @@ describe( 'effects', () => { } ); } ); - describe( '.SET_INITIAL_POST', () => { - const handler = effects.SET_INITIAL_POST; + describe( '.SETUP_EDITOR', () => { + const handler = effects.SETUP_EDITOR; it( 'should return post reset action', () => { const post = { diff --git a/editor/test/reducer.js b/editor/test/reducer.js index 15cc07abfa26c..453b8b73af237 100644 --- a/editor/test/reducer.js +++ b/editor/test/reducer.js @@ -23,6 +23,7 @@ import { saving, notices, showInsertionPoint, + blocksMode, } from '../reducer'; describe( 'state', () => { @@ -120,6 +121,33 @@ describe( 'state', () => { expect( state.blockOrder ).toEqual( [ 'wings' ] ); } ); + it( 'should update the block', () => { + const original = editor( undefined, { + type: 'RESET_BLOCKS', + blocks: [ { + uid: 'chicken', + name: 'core/test-block', + attributes: {}, + isValid: false, + } ], + } ); + const state = editor( deepFreeze( original ), { + type: 'UPDATE_BLOCK', + uid: 'chicken', + updates: { + attributes: { content: 'ribs' }, + isValid: true, + }, + } ); + + expect( state.blocksByUid.chicken ).toEqual( { + uid: 'chicken', + name: 'core/test-block', + attributes: { content: 'ribs' }, + isValid: true, + } ); + } ); + it( 'should move the block up', () => { const original = editor( undefined, { type: 'RESET_BLOCKS', @@ -989,4 +1017,26 @@ describe( 'state', () => { } ); } ); } ); + + describe( 'blocksMode', () => { + it( 'should set mode to html if not set', () => { + const action = { + type: 'TOGGLE_BLOCK_MODE', + uid: 'chicken', + }; + const value = blocksMode( deepFreeze( {} ), action ); + + expect( value ).toEqual( { chicken: 'html' } ); + } ); + + it( 'should toggle mode to visual if set as html', () => { + const action = { + type: 'TOGGLE_BLOCK_MODE', + uid: 'chicken', + }; + const value = blocksMode( deepFreeze( { chicken: 'html' } ), action ); + + expect( value ).toEqual( { chicken: 'visual' } ); + } ); + } ); } ); diff --git a/editor/test/selectors.js b/editor/test/selectors.js index bf2df0a1164ff..5f8f3987a4be1 100644 --- a/editor/test/selectors.js +++ b/editor/test/selectors.js @@ -54,6 +54,7 @@ import { isFirstMultiSelectedBlock, isBlockHovered, getBlockFocus, + getBlockMode, isTyping, getBlockInsertionPoint, isBlockInsertionPointVisible, @@ -62,6 +63,7 @@ import { didPostSaveRequestFail, getSuggestedPostFormat, getNotices, + getMostFrequentlyUsedBlocks, } from '../selectors'; describe( 'selectors', () => { @@ -1504,6 +1506,26 @@ describe( 'selectors', () => { } ); } ); + describe( 'geteBlockMode', () => { + it( 'should return "visual" if unset', () => { + const state = { + blocksMode: {}, + }; + + expect( getBlockMode( state, 123 ) ).toEqual( 'visual' ); + } ); + + it( 'should return the block mode', () => { + const state = { + blocksMode: { + 123: 'html', + }, + }; + + expect( getBlockMode( state, 123 ) ).toEqual( 'html' ); + } ); + } ); + describe( 'isTyping', () => { it( 'should return the isTyping flag if the block is selected', () => { const state = { @@ -1727,4 +1749,32 @@ describe( 'selectors', () => { ] ); } ); } ); + + describe( 'getMostFrequentlyUsedBlocks', () => { + it( 'should have paragraph and image to bring frequently used blocks up to three blocks', () => { + const noUsage = { preferences: { blockUsage: {} } }; + const someUsage = { preferences: { blockUsage: { 'core/paragraph': 1 } } }; + + expect( getMostFrequentlyUsedBlocks( noUsage ).map( ( block ) => block.name ) ) + .toEqual( [ 'core/paragraph', 'core/image' ] ); + + expect( getMostFrequentlyUsedBlocks( someUsage ).map( ( block ) => block.name ) ) + .toEqual( [ 'core/paragraph', 'core/image' ] ); + } ); + it( 'should return the top 3 most recently used blocks', () => { + const state = { + preferences: { + blockUsage: { + 'core/paragraph': 4, + 'core/image': 11, + 'core/quote': 2, + 'core/gallery': 1, + }, + }, + }; + + expect( getMostFrequentlyUsedBlocks( state ).map( ( block ) => block.name ) ) + .toEqual( [ 'core/image', 'core/paragraph', 'core/quote' ] ); + } ); + } ); } ); diff --git a/editor/writing-flow/index.js b/editor/writing-flow/index.js index fd9b508076057..29903bf931fb0 100644 --- a/editor/writing-flow/index.js +++ b/editor/writing-flow/index.js @@ -19,10 +19,7 @@ class WritingFlow extends Component { super( ...arguments ); this.onKeyDown = this.onKeyDown.bind( this ); - this.onKeyUp = this.onKeyUp.bind( this ); this.bindContainer = this.bindContainer.bind( this ); - - this.shouldMove = false; } bindContainer( ref ) { @@ -63,19 +60,8 @@ class WritingFlow extends Component { const moveDown = ( keyCode === DOWN || keyCode === RIGHT ); if ( ( moveUp || moveDown ) && isEdge( target, moveUp ) ) { - event.preventDefault(); - this.shouldMove = true; - } - } - - onKeyUp( event ) { - const { keyCode, target } = event; - const moveUp = ( keyCode === UP || keyCode === LEFT ); - - if ( this.shouldMove ) { event.preventDefault(); this.moveFocusInContainer( target, moveUp ? 'UP' : 'DOWN' ); - this.shouldMove = false; } } @@ -85,9 +71,7 @@ class WritingFlow extends Component { return (
    + onKeyDown={ this.onKeyDown }> { children }
    ); diff --git a/gutenberg.php b/gutenberg.php index dfe8764b81b37..95f71a1182dd1 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. Meant for development, do not run on real sites. - * Version: 1.3.0 + * Version: 1.4.0 * Author: Gutenberg Team * * @package gutenberg @@ -25,6 +25,7 @@ require_once dirname( __FILE__ ) . '/lib/i18n.php'; require_once dirname( __FILE__ ) . '/lib/parser.php'; require_once dirname( __FILE__ ) . '/lib/register.php'; + require_once dirname( __FILE__ ) . '/lib/plugin-compat.php'; // Register server-side code for individual blocks. foreach ( glob( dirname( __FILE__ ) . '/blocks/library/*/index.php' ) as $block_logic ) { diff --git a/lib/blocks.php b/lib/blocks.php index dc329e94ef6f5..87e1c9637fae3 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -73,8 +73,8 @@ function do_blocks( $content ) { $content_after_blocks = ''; foreach ( $blocks as $block ) { - $block_name = isset( $block['blockName'] ) ? $block['blockName'] : null; - $attributes = is_array( $block['attrs'] ) ? $block['attrs'] : array(); + $block_name = isset( $block['blockName'] ) ? $block['blockName'] : null; + $attributes = is_array( $block['attrs'] ) ? $block['attrs'] : array(); $raw_content = isset( $block['rawContent'] ) ? $block['rawContent'] : null; if ( $block_name ) { diff --git a/lib/class-wp-block-type-registry.php b/lib/class-wp-block-type-registry.php index 3c0c5221dba91..07a3fd665f09d 100644 --- a/lib/class-wp-block-type-registry.php +++ b/lib/class-wp-block-type-registry.php @@ -53,7 +53,7 @@ public function register( $name, $args = array() ) { $block_type = null; if ( is_a( $name, 'WP_Block_Type' ) ) { $block_type = $name; - $name = $block_type->name; + $name = $block_type->name; } if ( ! is_string( $name ) ) { @@ -62,6 +62,12 @@ public function register( $name, $args = array() ) { return false; } + if ( preg_match( '/[A-Z]+/', $name ) ) { + $message = __( 'Block type names must not contain uppercase characters.', 'gutenberg' ); + _doing_it_wrong( __METHOD__, $message, '1.5.0' ); + return false; + } + $name_matcher = '/^[a-z0-9-]+\/[a-z0-9-]+$/'; if ( ! preg_match( $name_matcher, $name ) ) { $message = __( 'Block type names must contain a namespace prefix. Example: my-plugin/my-custom-block-type', 'gutenberg' ); diff --git a/lib/class-wp-rest-reusable-blocks-controller.php b/lib/class-wp-rest-reusable-blocks-controller.php index 534af993e1c20..b28aae9be1855 100644 --- a/lib/class-wp-rest-reusable-blocks-controller.php +++ b/lib/class-wp-rest-reusable-blocks-controller.php @@ -97,7 +97,7 @@ public function get_items( $request ) { $collection = array(); foreach ( $reusable_blocks as $reusable_block ) { - $response = $this->prepare_item_for_response( $reusable_block, $request ); + $response = $this->prepare_item_for_response( $reusable_block, $request ); $collection[] = $this->prepare_response_for_collection( $response ); } @@ -219,7 +219,7 @@ protected function prepare_item_for_database( $request ) { $prepared_reusable_block->ID = $existing_reusable_block->ID; } - $prepared_reusable_block->post_type = 'gb_reusable_block'; + $prepared_reusable_block->post_type = 'gb_reusable_block'; $prepared_reusable_block->post_status = 'publish'; // ID. We already validated this in self::update_item(). @@ -258,8 +258,8 @@ protected function prepare_item_for_database( $request ) { */ public function prepare_item_for_response( $reusable_block, $request ) { $data = array( - 'id' => $reusable_block->post_name, - 'name' => $reusable_block->post_title, + 'id' => $reusable_block->post_name, + 'name' => $reusable_block->post_title, 'content' => $reusable_block->post_content, ); @@ -289,27 +289,27 @@ public function prepare_response_for_collection( $response ) { */ public function get_item_schema() { return array( - '$schema' => 'http://json-schema.org/schema#', - 'title' => 'reusable-block', - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'UUID that identifies this reusable block.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, + '$schema' => 'http://json-schema.org/schema#', + 'title' => 'reusable-block', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'UUID that identifies this reusable block.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, ), - 'name' => array( - 'description' => __( 'Name that identifies this reusable block', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'required' => true, + 'name' => array( + 'description' => __( 'Name that identifies this reusable block', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, ), - 'content' => array( - 'description' => __( 'The block\'s HTML content.', 'gutenberg' ), - 'type' => 'object', - 'context' => array( 'view', 'edit' ), - 'required' => true, + 'content' => array( + 'description' => __( 'The block\'s HTML content.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'required' => true, ), ), ); @@ -328,7 +328,7 @@ public function get_item_schema() { private function get_reusable_block( $uuid ) { $reusable_blocks = get_posts( array( 'post_type' => 'gb_reusable_block', - 'name' => $uuid, + 'name' => $uuid, ) ); return array_shift( $reusable_blocks ); diff --git a/lib/client-assets.php b/lib/client-assets.php index 03cc2bbc01bd6..3d4cf6dbfae67 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -90,21 +90,21 @@ function gutenberg_register_scripts_and_styles() { ); global $wp_locale; wp_add_inline_script( 'wp-date', 'window._wpDateSettings = ' . wp_json_encode( array( - 'l10n' => array( + 'l10n' => array( 'locale' => get_locale(), 'months' => array_values( $wp_locale->month ), 'monthsShort' => array_values( $wp_locale->month_abbrev ), 'weekdays' => array_values( $wp_locale->weekday ), 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), 'meridiem' => (object) $wp_locale->meridiem, - 'relative' => array( + 'relative' => array( /* translators: %s: duration */ 'future' => __( '%s from now', 'default' ), /* translators: %s: duration */ 'past' => __( '%s ago', 'default' ), ), ), - 'formats' => array( + 'formats' => array( 'time' => get_option( 'time_format', __( 'g:i a', 'default' ) ), 'date' => get_option( 'date_format', __( 'F j, Y', 'default' ) ), 'datetime' => __( 'F j, Y g:i a', 'default' ), @@ -291,7 +291,7 @@ function gutenberg_register_vendor_scripts() { */ function gutenberg_vendor_script_filename( $src ) { $filename = basename( $src ); - $hash = substr( md5( $src ), 0, 8 ); + $hash = substr( md5( $src ), 0, 8 ); $match = preg_match( '/^' @@ -452,7 +452,7 @@ function gutenberg_extend_wp_api_backbone_client() { $taxonomy_rest_base_mapping[ $taxonomy_object->name ] = $rest_base; } - $script = sprintf( 'wp.api.postTypeRestBaseMapping = %s;', wp_json_encode( $post_type_rest_base_mapping ) ); + $script = sprintf( 'wp.api.postTypeRestBaseMapping = %s;', wp_json_encode( $post_type_rest_base_mapping ) ); $script .= sprintf( 'wp.api.taxonomyRestBaseMapping = %s;', wp_json_encode( $taxonomy_rest_base_mapping ) ); $script .= << array( - 'baseURL' => includes_url( 'js/tinymce' ), - 'suffix' => SCRIPT_DEBUG ? '' : '.min', + 'baseURL' => includes_url( 'js/tinymce' ), + 'suffix' => SCRIPT_DEBUG ? '' : '.min', 'settings' => apply_filters( 'tiny_mce_before_init', array( 'external_plugins' => apply_filters( 'mce_external_plugins', array() ), - 'plugins' => array_unique( apply_filters( 'tiny_mce_plugins', array( + 'plugins' => array_unique( apply_filters( 'tiny_mce_plugins', array( 'charmap', 'colorpicker', 'hr', @@ -644,7 +644,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'wptextpattern', 'wpview', ) ) ), - 'toolbar1' => implode( ',', array_merge( apply_filters( 'mce_buttons', array( + 'toolbar1' => implode( ',', array_merge( apply_filters( 'mce_buttons', array( 'formatselect', 'bold', 'italic', @@ -659,7 +659,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'wp_more', 'spellchecker', ), 'editor' ), array( 'kitchensink' ) ) ), - 'toolbar2' => implode( ',', apply_filters( 'mce_buttons_2', array( + 'toolbar2' => implode( ',', apply_filters( 'mce_buttons_2', array( 'strikethrough', 'hr', 'forecolor', @@ -672,8 +672,8 @@ function gutenberg_editor_scripts_and_styles( $hook ) { 'redo', 'wp_help', ), 'editor' ) ), - 'toolbar3' => implode( ',', apply_filters( 'mce_buttons_3', array(), 'editor' ) ), - 'toolbar4' => implode( ',', apply_filters( 'mce_buttons_4', array(), 'editor' ) ), + 'toolbar3' => implode( ',', apply_filters( 'mce_buttons_3', array(), 'editor' ) ), + 'toolbar4' => implode( ',', apply_filters( 'mce_buttons_4', array(), 'editor' ) ), ), 'editor' ), ), ) ); @@ -708,7 +708,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { // Create an auto-draft if new post. if ( ! $post_id ) { $default_post_to_edit = get_default_post_to_edit( $post_type, true ); - $post_id = $default_post_to_edit->ID; + $post_id = $default_post_to_edit->ID; } // Generate API-prepared post from post ID. @@ -720,7 +720,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { // Set initial title to empty string for auto draft for duration of edit. $is_new_post = 'auto-draft' === $post_to_edit['status']; if ( $is_new_post ) { - $default_title = apply_filters( 'default_title', '' ); + $default_title = apply_filters( 'default_title', '' ); $post_to_edit['title'] = array( 'raw' => $default_title, 'rendered' => apply_filters( 'the_title', $default_title, $post_id ), @@ -771,7 +771,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { // Preload server-registered block schemas. $block_registry = WP_Block_Type_Registry::get_instance(); - $schemas = array(); + $schemas = array(); foreach ( $block_registry->get_all_registered() as $block_name => $block_type ) { if ( isset( $block_type->attributes ) ) { $schemas[ $block_name ] = $block_type->attributes; @@ -781,15 +781,15 @@ function gutenberg_editor_scripts_and_styles( $hook ) { // Initialize the editor. $gutenberg_theme_support = get_theme_support( 'gutenberg' ); - $color_palette = gutenberg_color_palette(); + $color_palette = gutenberg_color_palette(); - if ( $gutenberg_theme_support && $gutenberg_theme_support[0]['colors'] ) { + if ( ! empty( $gutenberg_theme_support[0]['colors'] ) ) { $color_palette = $gutenberg_theme_support[0]['colors']; } $editor_settings = array( - 'wideImages' => $gutenberg_theme_support ? $gutenberg_theme_support[0]['wide-images'] : false, - 'colors' => $color_palette, + 'wideImages' => ! empty( $gutenberg_theme_support[0]['wide-images'] ), + 'colors' => $color_palette, ); wp_add_inline_script( 'wp-editor', 'wp.api.init().done( function() {' diff --git a/lib/compat.php b/lib/compat.php index 78ecb01011476..efa2912c3022a 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -132,10 +132,23 @@ function gutenberg_ensure_wp_api_request() { */ function gutenberg_disable_editor_settings_wpautop( $settings ) { $post = get_post(); - if ( ! isset( $settings['wpautop'] ) ) { - $settings['wpautop'] = ! ( is_object( $post ) && gutenberg_post_has_blocks( $post->ID ) ); + if ( is_object( $post ) && gutenberg_post_has_blocks( $post->ID ) ) { + $settings['wpautop'] = false; } return $settings; } add_filter( 'wp_editor_settings', 'gutenberg_disable_editor_settings_wpautop' ); + +/** + * Add rest nonce to the heartbeat response. + * + * @param array $response Original heartbeat response. + * @return array New heartbeat response. + */ +function gutenberg_add_rest_nonce_to_heartbeat_response_headers( $response ) { + $response['rest-nonce'] = wp_create_nonce( 'wp_rest' ); + return $response; +} + +add_filter( 'wp_refresh_nonces', 'gutenberg_add_rest_nonce_to_heartbeat_response_headers' ); diff --git a/lib/i18n.php b/lib/i18n.php index ceed236263a14..7a68454f1d906 100644 --- a/lib/i18n.php +++ b/lib/i18n.php @@ -22,7 +22,7 @@ function gutenberg_get_jed_locale_data( $domain ) { $translations = get_translations_for_domain( $domain ); $locale = array( - 'domain' => $domain, + 'domain' => $domain, 'locale_data' => array( $domain => array( '' => array( diff --git a/lib/parser.php b/lib/parser.php index dddd7b442abb0..5d9961d16faa5 100644 --- a/lib/parser.php +++ b/lib/parser.php @@ -310,7 +310,8 @@ private function peg_f10($blockName) { 'blockName' => $blockName, ); } - private function peg_f11($attrs) { return json_decode( $attrs, true ); } + private function peg_f11($type) { return "core/$type"; } + private function peg_f12($attrs) { return json_decode( $attrs, true ); } private function peg_parseDocument() { @@ -1176,69 +1177,60 @@ private function peg_parseWP_Block_End() { private function peg_parseWP_Block_Name() { + $s0 = $this->peg_parseWP_Namespaced_Block_Name(); + if ($s0 === $this->peg_FAILED) { + $s0 = $this->peg_parseWP_Core_Block_Name(); + } + + return $s0; + } + + private function peg_parseWP_Namespaced_Block_Name() { + $s0 = $this->peg_currPos; $s1 = $this->peg_currPos; $s2 = $this->peg_parseASCII_Letter(); if ($s2 !== $this->peg_FAILED) { $s3 = array(); $s4 = $this->peg_parseASCII_AlphaNumeric(); - if ($s4 === $this->peg_FAILED) { - $s4 = $this->peg_currPos; + while ($s4 !== $this->peg_FAILED) { + $s3[] = $s4; + $s4 = $this->peg_parseASCII_AlphaNumeric(); + } + if ($s3 !== $this->peg_FAILED) { if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c15) { - $s5 = $this->peg_c15; + $s4 = $this->peg_c15; $this->peg_currPos++; } else { - $s5 = $this->peg_FAILED; + $s4 = $this->peg_FAILED; if ($this->peg_silentFails === 0) { $this->peg_fail($this->peg_c16); } } - if ($s5 !== $this->peg_FAILED) { - $s6 = $this->peg_parseASCII_AlphaNumeric(); - if ($s6 !== $this->peg_FAILED) { - $s5 = array($s5, $s6); - $s4 = $s5; - } else { - $this->peg_currPos = $s4; - $s4 = $this->peg_FAILED; - } - } else { - $this->peg_currPos = $s4; - $s4 = $this->peg_FAILED; - } - } - while ($s4 !== $this->peg_FAILED) { - $s3[] = $s4; - $s4 = $this->peg_parseASCII_AlphaNumeric(); - if ($s4 === $this->peg_FAILED) { - $s4 = $this->peg_currPos; - if ($this->input_substr($this->peg_currPos, 1) === $this->peg_c15) { - $s5 = $this->peg_c15; - $this->peg_currPos++; - } else { - $s5 = $this->peg_FAILED; - if ($this->peg_silentFails === 0) { - $this->peg_fail($this->peg_c16); - } - } + if ($s4 !== $this->peg_FAILED) { + $s5 = $this->peg_parseASCII_Letter(); if ($s5 !== $this->peg_FAILED) { - $s6 = $this->peg_parseASCII_AlphaNumeric(); + $s6 = array(); + $s7 = $this->peg_parseASCII_AlphaNumeric(); + while ($s7 !== $this->peg_FAILED) { + $s6[] = $s7; + $s7 = $this->peg_parseASCII_AlphaNumeric(); + } if ($s6 !== $this->peg_FAILED) { - $s5 = array($s5, $s6); - $s4 = $s5; + $s2 = array($s2, $s3, $s4, $s5, $s6); + $s1 = $s2; } else { - $this->peg_currPos = $s4; - $s4 = $this->peg_FAILED; + $this->peg_currPos = $s1; + $s1 = $this->peg_FAILED; } } else { - $this->peg_currPos = $s4; - $s4 = $this->peg_FAILED; + $this->peg_currPos = $s1; + $s1 = $this->peg_FAILED; } + } else { + $this->peg_currPos = $s1; + $s1 = $this->peg_FAILED; } - } - if ($s3 !== $this->peg_FAILED) { - $s2 = array($s2, $s3); - $s1 = $s2; } else { $this->peg_currPos = $s1; $s1 = $this->peg_FAILED; @@ -1256,6 +1248,44 @@ private function peg_parseWP_Block_Name() { return $s0; } + private function peg_parseWP_Core_Block_Name() { + + $s0 = $this->peg_currPos; + $s1 = $this->peg_currPos; + $s2 = $this->peg_currPos; + $s3 = $this->peg_parseASCII_Letter(); + if ($s3 !== $this->peg_FAILED) { + $s4 = array(); + $s5 = $this->peg_parseASCII_AlphaNumeric(); + while ($s5 !== $this->peg_FAILED) { + $s4[] = $s5; + $s5 = $this->peg_parseASCII_AlphaNumeric(); + } + if ($s4 !== $this->peg_FAILED) { + $s3 = array($s3, $s4); + $s2 = $s3; + } else { + $this->peg_currPos = $s2; + $s2 = $this->peg_FAILED; + } + } else { + $this->peg_currPos = $s2; + $s2 = $this->peg_FAILED; + } + if ($s2 !== $this->peg_FAILED) { + $s1 = $this->input_substr($s1, $this->peg_currPos - $s1); + } else { + $s1 = $s2; + } + if ($s1 !== $this->peg_FAILED) { + $this->peg_reportedPos = $s0; + $s1 = $this->peg_f11($s1); + } + $s0 = $s1; + + return $s0; + } + private function peg_parseWP_Block_Attributes() { $s0 = $this->peg_currPos; @@ -1507,7 +1537,7 @@ private function peg_parseWP_Block_Attributes() { } if ($s1 !== $this->peg_FAILED) { $this->peg_reportedPos = $s0; - $s1 = $this->peg_f11($s1); + $s1 = $this->peg_f12($s1); } $s0 = $s1; diff --git a/lib/plugin-compat.php b/lib/plugin-compat.php new file mode 100644 index 0000000000000..de5271fd33c36 --- /dev/null +++ b/lib/plugin-compat.php @@ -0,0 +1,34 @@ + tags. This adds a filter prior to saving the post via + * REST API to disable markdown support. Fixes markdown support provided by + * plugins Jetpack, JP-Markdown, and WP Editor.MD + * + * @since 1.3.0 + * + * @param array $post Post object which contains content to check for block. + * @return array $post Post object. + */ +function gutenberg_remove_wpcom_markdown_support( $post ) { + if ( content_has_blocks( $post->post_content ) ) { + remove_post_type_support( 'post', 'wpcom-markdown' ); + } + return $post; +} +add_filter( 'rest_pre_insert_post', 'gutenberg_remove_wpcom_markdown_support' ); diff --git a/lib/register.php b/lib/register.php index 2669adf95e47d..73de3be9bd2a7 100644 --- a/lib/register.php +++ b/lib/register.php @@ -109,7 +109,7 @@ function gutenberg_add_admin_bar_edit_link( $wp_admin_bar ) { } $gutenberg_text = __( 'Edit in Gutenberg', 'gutenberg' ); - $gutenberg_url = gutenberg_get_edit_post_url( $post->ID ); + $gutenberg_url = gutenberg_get_edit_post_url( $post->ID ); $is_gutenberg_default = gutenberg_post_has_blocks( $post->ID ); @@ -123,10 +123,10 @@ function gutenberg_add_admin_bar_edit_link( $wp_admin_bar ) { // Add submenu item under link to go to Gutenberg editor or classic editor. $wp_admin_bar->add_node( array( - 'id' => 'edit_alt', + 'id' => 'edit_alt', 'parent' => 'edit', - 'href' => $is_gutenberg_default ? $classic_url : $gutenberg_url, - 'title' => $is_gutenberg_default ? $classic_text : $gutenberg_text, + 'href' => $is_gutenberg_default ? $classic_url : $gutenberg_url, + 'title' => $is_gutenberg_default ? $classic_text : $gutenberg_text, ) ); } @@ -170,9 +170,9 @@ function gutenberg_add_edit_links( $actions, $post ) { add_filter( 'get_edit_post_link', 'gutenberg_filter_edit_post_link', 10, 3 ); // Build the new edit actions. See also: WP_Posts_List_Table::handle_row_actions(). - $title = _draft_or_post_title( $post->ID ); + $title = _draft_or_post_title( $post->ID ); $edit_actions = array( - 'classic hide-if-no-js' => sprintf( + 'classic hide-if-no-js' => sprintf( '%s', esc_url( $classic_url ), esc_attr( sprintf( @@ -196,7 +196,7 @@ function gutenberg_add_edit_links( $actions, $post ) { // Insert the new actions in place of the Edit action. $edit_offset = array_search( 'edit', array_keys( $actions ), true ); - $actions = array_merge( + $actions = array_merge( array_slice( $actions, 0, $edit_offset ), $edit_actions, array_slice( $actions, $edit_offset + 1 ) @@ -284,7 +284,19 @@ function gutenberg_can_edit_post( $post_id ) { */ function gutenberg_post_has_blocks( $post_id ) { $post = get_post( $post_id ); - return $post && strpos( $post->post_content, ' - - gutenberg.php - + gutenberg.php diff --git a/phpunit/class-admin-test.php b/phpunit/class-admin-test.php index b55620a3ebb41..c6d0a2e539ccf 100644 --- a/phpunit/class-admin-test.php +++ b/phpunit/class-admin-test.php @@ -36,15 +36,15 @@ class Admin_Test extends WP_UnitTestCase { */ public static function setUpBeforeClass() { - self::$editor_user_id = self::factory()->user->create( array( + self::$editor_user_id = self::factory()->user->create( array( 'role' => 'editor', ) ); - self::$post_with_blocks = self::factory()->post->create( array( - 'post_title' => 'Example', + self::$post_with_blocks = self::factory()->post->create( array( + 'post_title' => 'Example', 'post_content' => "\n

    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' => '
    An image
    ', ) ); @@ -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' => '
    An image
    ', ), $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,