diff --git a/README.md b/README.md index 4b5cf4c..71a4744 100644 --- a/README.md +++ b/README.md @@ -1218,6 +1218,99 @@ Direct block HTML can be accessed through `$block['innerHTML']`. This may be use For another example of how this filter can be used to extend block data, we have implemented a default image block filter in [`src/parser/block-additions/core-image.php`][repo-core-image-block-addition]. This filter is automatically called on `core/image` blocks to add `width` and `height` to image attributes. +--- + +### `vip_block_data_api__before_parse_post_content` + +Modify raw post content before it's parsed by the Block Data API. The `$post_content` provided by this filter is directly what is stored in the post database before any processing occurs. + +```php +/** + * Filters content before parsing blocks in a post. + * + * @param string $post_content The content of the post being parsed. + * @param int $post_id Post ID associated with the content. + */ +$post_content = apply_filters( 'vip_block_data_api__before_parse_post_content', $post_content, $post_id ); +``` + +For example, this could be used to modify a block's type before parsing. The code below replaces instances of `test/invalid-block` blocks with `core/paragraph`: + +```php +add_filter( 'vip_block_data_api__before_parse_post_content', 'replace_invalid_blocks' ); + +function replace_invalid_blocks( $post_content, $post_id ) { + return str_replace( 'wp:test/invalid-block', 'wp:paragraph', $post_content ); +} + +$html = ' + +

Block content!

+ +'; + +$content_parser = new ContentParser(); +$result = $content_parser->parse( $html ); + +// Evaluates to true +assertEquals( [ + [ + 'name' => 'core/paragraph', + 'attributes' => [ + 'content' => 'Block content!', + ], + ], +], $result['blocks'] ); +``` + +**Warning** + +Be careful with content modification before parsing. In the example above, if a block contained the text "wp:test/invalid-block" outside of a block header, this would also be changed to "wp:paragraph". This is likely not the intent of the code. + +All block markup is sensitive to changes, even changes in whitespace. We've added this filter to make the plugin flexible, but any transforms to `post_content` should be done with extreme care. Strongly consider adding tests to any usage of this filter. + +--- + +### `vip_block_data_api__after_parse_blocks` + +Modify the Block Data API REST endpoint response. + +```php +/** + * Filters the API result before returning parsed blocks in a post. + * + * @param string $result The successful API result, contains 'blocks' key with an array + * of block data, and optionally 'warnings' and 'debug' keys. + * @param int $post_id Post ID associated with the content. + */ +$result = apply_filters( 'vip_block_data_api__after_parse_blocks', $result, $post_id ); +``` + +This filter is called directly before returning a result in the REST API. Use this filter to add additional metadata or debug information to the API output. + +```php +add_action( 'vip_block_data_api__after_parse_blocks', 'add_block_data_debug_info', 10, 2 ); + +function add_block_data_debug_info( $result, $post_id ) { + $result['debug']['my-value'] = 123; + + return $result; +} +``` + +This would add `debug.my-value` to all Block Data API REST results: + +```bash +> curl https://my.site/wp-json/vip-block-data-api/v1/posts/1/blocks + +{ + "debug": { + "my-value": 123 + }, + "blocks": [ /* ... */ ] +} +``` + ## Analytics **Please note that, this is for VIP sites only. Analytics are disabled if this plugin is not being run on VIP sites.** diff --git a/src/parser/content-parser.php b/src/parser/content-parser.php index 599bfc2..18392ed 100644 --- a/src/parser/content-parser.php +++ b/src/parser/content-parser.php @@ -128,6 +128,14 @@ public function parse( $post_content, $post_id = null, $filter_options = [] ) { $parsing_error = false; try { + /** + * Filters content before parsing blocks in a post. + * + * @param string $post_content The content of the post being parsed. + * @param int $post_id Post ID associated with the content. + */ + $post_content = apply_filters( 'vip_block_data_api__before_parse_post_content', $post_content, $post_id ); + $blocks = parse_blocks( $post_content ); $blocks = array_values( array_filter( $blocks, function ( $block ) { $is_whitespace_block = ( null === $block['blockName'] && empty( trim( $block['innerHTML'] ) ) ); @@ -168,6 +176,15 @@ public function parse( $post_content, $post_id = null, $filter_options = [] ) { 'details' => $parsing_error->__toString(), ] ); } else { + /** + * Filters the API result before returning parsed blocks in a post. + * + * @param string $result The successful API result, contains 'blocks' + * key with an array of block data, and optionally 'warnings' and 'debug' keys. + * @param int $post_id Post ID associated with the content. + */ + $result = apply_filters( 'vip_block_data_api__after_parse_blocks', $result, $post_id ); + return $result; } } diff --git a/tests/graphql/test-graphql-api.php b/tests/graphql/test-graphql-api.php index 0683837..777cd39 100644 --- a/tests/graphql/test-graphql-api.php +++ b/tests/graphql/test-graphql-api.php @@ -30,12 +30,12 @@ public function test_get_blocks_data() {

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

- +

This is a heading inside a quote

- +

This is a heading

@@ -49,68 +49,60 @@ public function test_get_blocks_data() { [ 'name' => 'core/paragraph', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'Welcome to WordPress. This is your first post. Edit or delete it, then start writing!', - ), - array( + ], + [ 'name' => 'dropCap', 'value' => '', - ), + ], ], 'id' => '1', ], [ 'name' => 'core/quote', 'attributes' => [ - array( + [ 'name' => 'value', 'value' => '', - ), - array( - 'name' => 'citation', - 'value' => '', - ), + ], ], 'innerBlocks' => [ [ 'name' => 'core/paragraph', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'This is a heading inside a quote', - ), - array( + ], + [ 'name' => 'dropCap', 'value' => '', - ), + ], ], 'id' => '3', ], [ 'name' => 'core/quote', 'attributes' => [ - array( + [ 'name' => 'value', 'value' => '', - ), - array( - 'name' => 'citation', - 'value' => '', - ), + ], ], 'innerBlocks' => [ [ 'name' => 'core/heading', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'This is a heading', - ), - array( + ], + [ 'name' => 'level', 'value' => '2', - ), + ], ], 'id' => '5', ], @@ -137,68 +129,60 @@ public function test_flatten_inner_blocks() { [ 'name' => 'core/paragraph', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'Welcome to WordPress. This is your first post. Edit or delete it, then start writing!', - ), - array( + ], + [ 'name' => 'dropCap', 'value' => '', - ), + ], ], 'id' => '2', ], [ 'name' => 'core/quote', 'attributes' => [ - array( + [ 'name' => 'value', 'value' => '', - ), - array( - 'name' => 'citation', - 'value' => '', - ), + ], ], 'innerBlocks' => [ [ 'name' => 'core/paragraph', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'This is a heading inside a quote', - ), - array( + ], + [ 'name' => 'dropCap', 'value' => '', - ), + ], ], 'id' => '4', ], [ 'name' => 'core/quote', 'attributes' => [ - array( + [ 'name' => 'value', 'value' => '', - ), - array( - 'name' => 'citation', - 'value' => '', - ), + ], ], 'innerBlocks' => [ [ 'name' => 'core/heading', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'This is a heading', - ), - array( + ], + [ 'name' => 'level', 'value' => '2', - ), + ], ], 'id' => '6', ], @@ -214,14 +198,14 @@ public function test_flatten_inner_blocks() { [ 'name' => 'core/paragraph', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'Welcome to WordPress. This is your first post. Edit or delete it, then start writing!', - ), - array( + ], + [ 'name' => 'dropCap', 'value' => '', - ), + ], ], 'parentId' => '1', 'id' => '2', @@ -229,14 +213,10 @@ public function test_flatten_inner_blocks() { [ 'name' => 'core/quote', 'attributes' => [ - array( + [ 'name' => 'value', 'value' => '', - ), - array( - 'name' => 'citation', - 'value' => '', - ), + ], ], 'id' => '3', 'parentId' => '1', @@ -244,14 +224,14 @@ public function test_flatten_inner_blocks() { [ 'name' => 'core/paragraph', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'This is a heading inside a quote', - ), - array( + ], + [ 'name' => 'dropCap', 'value' => '', - ), + ], ], 'id' => '4', 'parentId' => '3', @@ -259,14 +239,10 @@ public function test_flatten_inner_blocks() { [ 'name' => 'core/quote', 'attributes' => [ - array( + [ 'name' => 'value', 'value' => '', - ), - array( - 'name' => 'citation', - 'value' => '', - ), + ], ], 'id' => '5', 'parentId' => '3', @@ -274,14 +250,14 @@ public function test_flatten_inner_blocks() { [ 'name' => 'core/heading', 'attributes' => [ - array( + [ 'name' => 'content', 'value' => 'This is a heading', - ), - array( + ], + [ 'name' => 'level', 'value' => '2', - ), + ], ], 'id' => '6', 'parentId' => '5', diff --git a/tests/parser/test-content-parser.php b/tests/parser/test-content-parser.php index f76c00f..a5b90d0 100644 --- a/tests/parser/test-content-parser.php +++ b/tests/parser/test-content-parser.php @@ -13,62 +13,6 @@ class ContentParserTest extends RegistryTestCase { /* Multiple attributes */ - public function test_block_filter_via_code() { - $this->register_block_with_attributes( 'test/block1', [ - 'content' => [ - 'type' => 'string', - 'source' => 'html', - 'selector' => 'div.a', - ], - ] ); - - $this->register_block_with_attributes( 'test/block2', [ - 'url' => [ - 'type' => 'string', - 'source' => 'attribute', - 'selector' => 'img', - 'attribute' => 'src', - ], - ] ); - - $html = ' - -
Block 1
- - - - - - '; - - $expected_blocks = [ - [ - 'name' => 'test/block1', - 'attributes' => [ - 'content' => 'Block 1', - ], - ], - ]; - - $block_filter_function = function ( $is_block_included, $block_name ) { - if ( 'test/block2' === $block_name ) { - return false; - } else { - return true; - } - }; - - add_filter( 'vip_block_data_api__allow_block', $block_filter_function, 10, 2 ); - $content_parser = new ContentParser( $this->registry ); - $blocks = $content_parser->parse( $html ); - remove_filter( 'vip_block_data_api__allow_block', $block_filter_function, 10, 2 ); - - $this->assertArrayNotHasKey( 'errors', $blocks ); - $this->assertNotEmpty( $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); - $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); - $this->assertEquals( $expected_blocks, $blocks['blocks'] ); - } - public function test_parse_multiple_attributes_from_block() { $this->register_block_with_attributes( 'test/captioned-image', [ 'caption' => [ diff --git a/tests/parser/test-parser-filters.php b/tests/parser/test-parser-filters.php new file mode 100644 index 0000000..de99543 --- /dev/null +++ b/tests/parser/test-parser-filters.php @@ -0,0 +1,156 @@ +register_block_with_attributes( 'test/block1', [ + 'content' => [ + 'type' => 'string', + 'source' => 'html', + 'selector' => 'div.a', + ], + ] ); + + $this->register_block_with_attributes( 'test/block2', [ + 'url' => [ + 'type' => 'string', + 'source' => 'attribute', + 'selector' => 'img', + 'attribute' => 'src', + ], + ] ); + + $html = ' + +
Block 1
+ + + + + + '; + + $expected_blocks = [ + [ + 'name' => 'test/block1', + 'attributes' => [ + 'content' => 'Block 1', + ], + ], + ]; + + $block_filter_function = function ( $is_block_included, $block_name ) { + if ( 'test/block2' === $block_name ) { + return false; + } else { + return true; + } + }; + + add_filter( 'vip_block_data_api__allow_block', $block_filter_function, 10, 2 ); + $content_parser = new ContentParser( $this->registry ); + $blocks = $content_parser->parse( $html ); + remove_filter( 'vip_block_data_api__allow_block', $block_filter_function, 10, 2 ); + + $this->assertArrayNotHasKey( 'errors', $blocks ); + $this->assertNotEmpty( $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertEquals( $expected_blocks, $blocks['blocks'] ); + } + + /* vip_block_data_api__before_parse_post_content */ + + public function test_before_parse_post_content_filter() { + $this->register_block_with_attributes( 'test/valid-block', [ + 'content' => [ + 'type' => 'rich-text', + 'source' => 'rich-text', + 'selector' => 'code', + ], + ] ); + + $html = ' + + Block content! + + '; + + $expected_blocks = [ + [ + 'name' => 'test/valid-block', + 'attributes' => [ + 'content' => 'Block content!', + ], + ], + ]; + + $replace_post_content_filter = function ( $post_content ) { + return str_replace( 'test/invalid-block', 'test/valid-block', $post_content ); + }; + + add_filter( 'vip_block_data_api__before_parse_post_content', $replace_post_content_filter ); + $content_parser = new ContentParser( $this->registry ); + $blocks = $content_parser->parse( $html ); + remove_filter( 'vip_block_data_api__before_parse_post_content', $replace_post_content_filter ); + + $this->assertArrayNotHasKey( 'errors', $blocks ); + $this->assertNotEmpty( $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) ); + $this->assertEquals( $expected_blocks, $blocks['blocks'] ); + } + + /* vip_block_data_api__after_parse_blocks */ + + public function test_after_parse_filter() { + $this->register_block_with_attributes( 'test/paragraph', [ + 'content' => [ + 'type' => 'rich-text', + 'source' => 'rich-text', + 'selector' => 'p', + ], + ] ); + + $html = ' + +

Paragaph text

+ + '; + + $expected_blocks = [ + [ + 'name' => 'test/paragraph', + 'attributes' => [ + 'content' => 'Paragaph text', + ], + ], + ]; + + $add_extra_data_filter = function ( $result ) { + $result['my-key'] = 'my-value'; + + return $result; + }; + + add_filter( 'vip_block_data_api__after_parse_blocks', $add_extra_data_filter ); + $content_parser = new ContentParser( $this->registry ); + $result = $content_parser->parse( $html ); + remove_filter( 'vip_block_data_api__after_parse_blocks', $add_extra_data_filter ); + + $this->assertArrayNotHasKey( 'errors', $result ); + $this->assertNotEmpty( $result, sprintf( 'Unexpected parser output: %s', wp_json_encode( $result ) ) ); + $this->assertArrayHasKey( 'blocks', $result, sprintf( 'Unexpected parser output: %s', wp_json_encode( $result ) ) ); + $this->assertEquals( $expected_blocks, $result['blocks'] ); + $this->assertEquals( 'my-value', $result['my-key'] ); + } +} diff --git a/tests/rest/test-rest-api.php b/tests/rest/test-rest-api.php index 2524297..0995c1a 100644 --- a/tests/rest/test-rest-api.php +++ b/tests/rest/test-rest-api.php @@ -209,7 +209,7 @@ public function test_rest_api_does_not_return_excluded_blocks_for_post() { $request = new WP_REST_Request( 'GET', sprintf( '/vip-block-data-api/v1/posts/%d/blocks', $post_id ) ); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude - $request->set_query_params( array( 'exclude' => 'core/paragraph,core/separator' ) ); + $request->set_query_params( [ 'exclude' => 'core/paragraph,core/separator' ] ); $response = $this->server->dispatch( $request ); @@ -270,7 +270,7 @@ public function test_rest_api_only_returns_included_blocks_for_post() { ]; $request = new WP_REST_Request( 'GET', sprintf( '/vip-block-data-api/v1/posts/%d/blocks', $post_id ) ); - $request->set_query_params( array( 'include' => 'core/heading' ) ); + $request->set_query_params( [ 'include' => 'core/heading' ] ); $response = $this->server->dispatch( $request ); @@ -430,11 +430,11 @@ public function test_rest_api_returns_error_for_include_and_exclude_filter() { $this->expectExceptionMessage( 'vip-block-data-api-invalid-params' ); $request = new WP_REST_Request( 'GET', sprintf( '/vip-block-data-api/v1/posts/%d/blocks', $post_id ) ); - $request->set_query_params( array( + $request->set_query_params( [ // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude 'exclude' => 'core/paragraph,core/separator', 'include' => 'core/heading,core/quote,core/media-text', - ) ); + ] ); $response = $this->server->dispatch( $request );