Skip to content

Commit

Permalink
Parser: Client-side wpautop (#4005)
Browse files Browse the repository at this point in the history
* Framework: Drop server-side block serialization, wpautop

A parsed block has no awareness of where inner blocks exist in its innerHTML, so it cannot safely reserialize.

There are a few options:

- Since we merely skip wpautop for known blocks, we could avoid reserialization and return the block's original HTML verbatim if we had access to its outerHTML. See nylen/phpegjs#3

- Move wpautop behavior for freeform content to the editor client. This may align well with desires to transparently upgrade legacy paragraph content to paragraph blocks. This would also allow the server to avoid any preprocessing before showing a post on the front-end, assuming that the saved content has had wpautop applied already.

Acknowledging that this effectively reverts large parts of #2806

* Parser: Apply autop to fallback block content
  • Loading branch information
aduth authored Jan 30, 2018
1 parent 0dbab97 commit c6e8798
Show file tree
Hide file tree
Showing 18 changed files with 11,622 additions and 11,838 deletions.
73 changes: 44 additions & 29 deletions blocks/api/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
import { parse as hpqParse } from 'hpq';
import { mapValues, omit } from 'lodash';

/**
* WordPress dependencies
*/
import { autop } from '@wordpress/autop';

/**
* Internal dependencies
*/
Expand Down Expand Up @@ -191,7 +196,16 @@ export function createBlockWithFallback( name, innerHTML, attributes ) {

// Try finding type for known block name, else fall back again.
let blockType = getBlockType( name );

const fallbackBlock = getUnknownTypeHandlerName();

// Fallback content may be upgraded from classic editor expecting implicit
// automatic paragraphs, so preserve them. Assumes wpautop is idempotent,
// meaning there are no negative consequences to repeated autop calls.
if ( name === fallbackBlock ) {
innerHTML = autop( innerHTML ).trim();
}

if ( ! blockType ) {
// If detected as a block which is not registered, preserve comment
// delimiters in content of unknown type handler.
Expand All @@ -204,40 +218,41 @@ export function createBlockWithFallback( name, innerHTML, attributes ) {
}

// Include in set only if type were determined.
// TODO do we ever expect there to not be an unknown type handler?
if ( blockType && ( innerHTML || name !== fallbackBlock ) ) {
// TODO allow blocks to opt-in to receiving a tree instead of a string.
// Gradually convert all blocks to this new format, then remove the
// string serialization.
const block = createBlock(
name,
getBlockAttributes( blockType, innerHTML, attributes )
);
if ( ! blockType || ( ! innerHTML && name === fallbackBlock ) ) {
return;
}

const block = createBlock(
name,
getBlockAttributes( blockType, innerHTML, attributes )
);

// Validate that the parsed block is valid, meaning that if we were to
// reserialize it given the assumed attributes, the markup matches the
// original value.
// Validate that the parsed block is valid, meaning that if we were to
// reserialize it given the assumed attributes, the markup matches the
// original value.
if ( name !== fallbackBlock ) {
block.isValid = isValidBlock( innerHTML, blockType, block.attributes );
}

// Preserve original content for future use in case the block is parsed
// as invalid, or future serialization attempt results in an error
block.originalContent = innerHTML;

// When a block is invalid, attempt to parse it using a supplied `deprecated` definition.
// This allows blocks to modify their attribute and markup structure without invalidating
// content written in previous formats.
if ( ! block.isValid ) {
const attributesParsedWithDeprecatedVersion = getAttributesFromDeprecatedVersion(
blockType, innerHTML, attributes
);
if ( attributesParsedWithDeprecatedVersion ) {
block.isValid = true;
block.attributes = attributesParsedWithDeprecatedVersion;
}
}
// Preserve original content for future use in case the block is parsed as
// invalid, or future serialization attempt results in an error.
block.originalContent = innerHTML;

return block;
// When block is invalid, attempt to parse it using deprecated definition.
// This enables blocks to modify attribute and markup structure without
// invalidating content written in previous formats.
if ( ! block.isValid ) {
const attributesParsedWithDeprecatedVersion = getAttributesFromDeprecatedVersion(
blockType, innerHTML, attributes
);

if ( attributesParsedWithDeprecatedVersion ) {
block.isValid = true;
block.attributes = attributesParsedWithDeprecatedVersion;
}
}

return block;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion blocks/api/test/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ describe( 'block parser', () => {

const block = createBlockWithFallback( null, 'content' );
expect( block.name ).toEqual( 'core/unknown-block' );
expect( block.attributes ).toEqual( { content: 'content' } );
expect( block.attributes ).toEqual( { content: '<p>content</p>' } );
} );

it( 'should not create a block if no unknown type handler', () => {
Expand Down
4 changes: 2 additions & 2 deletions blocks/test/fixtures/core__4-invalid-starting-letter.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"name": "core/freeform",
"isValid": true,
"attributes": {
"content": "<!-- wp:core/4-invalid /-->"
"content": "<p><!-- wp:core/4-invalid /--></p>"
},
"originalContent": "<!-- wp:core/4-invalid /-->"
"originalContent": "<p><!-- wp:core/4-invalid /--></p>"
}
]
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!-- wp:core/4-invalid /-->
<p><!-- wp:core/4-invalid /--></p>
4 changes: 2 additions & 2 deletions blocks/test/fixtures/core__freeform.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"name": "core/freeform",
"isValid": true,
"attributes": {
"content": "Testing freeform block with some\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>"
"content": "<p>Testing freeform block with some\n</p><div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>"
},
"originalContent": "Testing freeform block with some\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>"
"originalContent": "<p>Testing freeform block with some\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>"
}
]
4 changes: 2 additions & 2 deletions blocks/test/fixtures/core__freeform.serialized.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Testing freeform block with some
<div class="wp-some-class">
<p>Testing freeform block with some
</p><div class="wp-some-class">
HTML <span style="color: red;">content</span>
</div>
4 changes: 2 additions & 2 deletions blocks/test/fixtures/core__freeform__undelimited.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"name": "core/freeform",
"isValid": true,
"attributes": {
"content": "Testing freeform block with some\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>"
"content": "<p>Testing freeform block with some\n</p><div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>"
},
"originalContent": "Testing freeform block with some\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>"
"originalContent": "<p>Testing freeform block with some\n<div class=\"wp-some-class\">\n\tHTML <span style=\"color: red;\">content</span>\n</div>"
}
]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Testing freeform block with some
<div class="wp-some-class">
<p>Testing freeform block with some
</p><div class="wp-some-class">
HTML <span style="color: red;">content</span>
</div>
4 changes: 2 additions & 2 deletions blocks/test/fixtures/core__invalid-Capitals.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"name": "core/freeform",
"isValid": true,
"attributes": {
"content": "<!-- wp:core/invalid-Capitals /-->"
"content": "<p><!-- wp:core/invalid-Capitals /--></p>"
},
"originalContent": "<!-- wp:core/invalid-Capitals /-->"
"originalContent": "<p><!-- wp:core/invalid-Capitals /--></p>"
}
]
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!-- wp:core/invalid-Capitals /-->
<p><!-- wp:core/invalid-Capitals /--></p>
4 changes: 2 additions & 2 deletions blocks/test/fixtures/core__invalid-special.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"name": "core/freeform",
"isValid": true,
"attributes": {
"content": "<!-- wp:core/invalid-$special /-->"
"content": "<p><!-- wp:core/invalid-$special /--></p>"
},
"originalContent": "<!-- wp:core/invalid-$special /-->"
"originalContent": "<p><!-- wp:core/invalid-$special /--></p>"
}
]
2 changes: 1 addition & 1 deletion blocks/test/fixtures/core__invalid-special.serialized.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!-- wp:core/invalid-$special /-->
<p><!-- wp:core/invalid-$special /--></p>
141 changes: 0 additions & 141 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,80 +70,6 @@ function gutenberg_parse_blocks( $content ) {
return $parser->parse( _gutenberg_utf8_split( $content ) );
}

/**
* Given an array of parsed blocks, returns content string.
*
* @since 1.7.0
*
* @param array $blocks Parsed blocks.
*
* @return string Content string.
*/
function gutenberg_serialize_blocks( $blocks ) {
return implode( '', array_map( 'gutenberg_serialize_block', $blocks ) );
}

/**
* Given a parsed block, returns content string.
*
* @since 1.7.0
*
* @param array $block Parsed block.
*
* @return string Content string.
*/
function gutenberg_serialize_block( $block ) {
// Return content of unknown block verbatim.
if ( ! isset( $block['blockName'] ) ) {
return $block['innerHTML'];
}

// Custom formatting for specific block types.
if ( 'core/more' === $block['blockName'] ) {
$content = '<!--more';
if ( ! empty( $block['attrs']['customText'] ) ) {
$content .= ' ' . $block['attrs']['customText'];
}

$content .= '-->';
if ( ! empty( $block['attrs']['noTeaser'] ) ) {
$content .= "\n" . '<!--noteaser-->';
}

return $content;
}

// For standard blocks, return with comment-delimited wrapper.
$content = '<!-- wp:' . $block['blockName'] . ' ';

if ( ! empty( $block['attrs'] ) ) {
$attrs_json = json_encode( $block['attrs'] );

// In PHP 5.4+, we would pass the `JSON_UNESCAPED_SLASHES` option to
// `json_encode`. To support older versions, we must apply manually.
$attrs_json = str_replace( '\\/', '/', $attrs_json );

// Don't break HTML comments.
$attrs_json = str_replace( '--', '\\u002d\\u002d', $attrs_json );

// Don't break standard-non-compliant tools.
$attrs_json = str_replace( '<', '\\u003c', $attrs_json );
$attrs_json = str_replace( '>', '\\u003e', $attrs_json );
$attrs_json = str_replace( '&', '\\u0026', $attrs_json );

$content .= $attrs_json . ' ';
}

if ( empty( $block['innerHTML'] ) ) {
return $content . '/-->';
}

$content .= '-->';
$content .= $block['innerHTML'];
$content .= '<!-- /wp:' . $block['blockName'] . ' -->';
return $content;
}

/**
* Renders a single block into a HTML string.
*
Expand Down Expand Up @@ -189,70 +115,3 @@ function do_blocks( $content ) {
return $content_after_blocks;
}
add_filter( 'the_content', 'do_blocks', 9 ); // BEFORE do_shortcode().

/**
* Given a string, returns content normalized with automatic paragraphs applied
* to text not identified as a block. Since this executes the block parser, it
* should not be used in a performance-critical flow such as content display.
* Block content will not have automatic paragraphs applied.
*
* @since 1.7.0
*
* @param string $content Original content.
* @return string Content formatted with automatic paragraphs applied
* to unknown blocks.
*/
function gutenberg_wpautop_block_content( $content ) {
$blocks = gutenberg_parse_blocks( $content );
foreach ( $blocks as $i => $block ) {
if ( isset( $block['blockName'] ) ) {
continue;
}

$content = $block['innerHTML'];

// wpautop will trim leading whitespace and return whitespace-only text
// as an empty string. Preserve to apply leading whitespace as prefix.
preg_match( '/^(\s+)/', $content, $prefix_match );
$prefix = empty( $prefix_match ) ? '' : $prefix_match[0];

$content = $prefix . wpautop( $content, false );

// To normalize as text where wpautop would not be applied, restore
// double newline to wpautop'd text if not at the end of content.
$is_last_block = ( count( $blocks ) === $i + 1 );
if ( ! $is_last_block ) {
$content = str_replace( "</p>\n", "</p>\n\n", $content );
}

$blocks[ $i ]['innerHTML'] = $content;
}

return gutenberg_serialize_blocks( $blocks );
}

/**
* Filters saved post data to apply wpautop to freeform block content.
*
* @since 1.7.0
*
* @param array $data An array of slashed post data.
* @return array An array of post data with wpautop applied to freeform
* block content.
*/
function gutenberg_wpautop_insert_post_data( $data ) {
if ( ! empty( $data['post_content'] ) && gutenberg_content_has_blocks( $data['post_content'] ) ) {
// WP_REST_Posts_Controller slashes post data before inserting/updating
// a post. This data gets unslashed by `wp_insert_post` right before
// saving to the DB. The PEG parser needs unslashed input in order to
// properly parse JSON attributes.
$content = wp_unslash( $data['post_content'] );
$content = gutenberg_wpautop_block_content( $content );
$content = wp_slash( $content );

$data['post_content'] = $content;
}

return $data;
}
add_filter( 'wp_insert_post_data', 'gutenberg_wpautop_insert_post_data' );
6 changes: 0 additions & 6 deletions lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -807,12 +807,6 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
);
}

// Set initial content to apply autop on unknown blocks, preserving this
// behavior for classic content while otherwise disabling for blocks.
if ( ! $is_new_post && is_array( $post_to_edit['content'] ) ) {
$post_to_edit['content']['raw'] = gutenberg_wpautop_block_content( $post_to_edit['content']['raw'] );
}

// Set the post type name.
$post_type = get_post_type( $post );

Expand Down
Loading

0 comments on commit c6e8798

Please sign in to comment.