Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/phpcs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
php-versions:
- 8.1
- 8.2

steps:
- uses: actions/checkout@v7
Expand Down
20 changes: 6 additions & 14 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,16 @@ jobs:
# Alias 'master' to 'latest'
name: WP ${{ matrix.wp == 'master' && 'latest' || matrix.wp }} and PHP ${{ matrix.php }}
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.allowed_failure }}

strategy:
fail-fast: false
matrix:
include:
# Check lowest supported WP version, with the lowest supported PHP.
- php: "8.1"
wp: "6.0"
allowed_failure: false
# Check latest WP with the lowest supported PHP.
- php: "8.1"
wp: "master"
allowed_failure: false
# Check latest WP with the highest supported PHP.
- php: "8.3"
wp: "master"
allowed_failure: false
wp: [ "6.8", "6.9", "7.0", "7.1", "master" ]
php: [ "8.2", "8.3", "8.4", "8.5" ]
exclude:
# WordPress 6.8 does not support PHP 8.5.
- wp: "6.8"
php: "8.5"

steps:
- name: Checkout code
Expand Down
12 changes: 6 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

VIP Block Data API is a WordPress plugin that converts Gutenberg block editor content into structured JSON data. It provides both a REST API and a WPGraphQL integration. Primarily designed for decoupled/headless WordPress on the WordPress VIP platform. See `vip-block-data-api.php` for the current version.

- **Language:** PHP (8.1+)
- **WordPress:** 6.0+
- **Language:** PHP (8.2+)
- **WordPress:** 6.8+
- **Namespace:** `WPCOMVIP\BlockDataApi`
- **License:** GPL-2.0-or-later per `composer.json` (note: the plugin header in `vip-block-data-api.php` says GPL-3)
- **Repository:** https://github.com/Automattic/vip-block-data-api
Expand Down Expand Up @@ -127,7 +127,7 @@ These are the plugin's extension points:
### Prerequisites
- Node.js + npm (for `@wordpress/env`)
- Docker (for `wp-env`)
- PHP 8.1+
- PHP 8.2+
- Composer

### Local Environment
Expand Down Expand Up @@ -155,14 +155,14 @@ composer phpcs # Run PHP CodeSniffer
composer phpcs-fix # Auto-fix with phpcbf
```

**Coding standards:** WordPress-Extra, WordPress-VIP-Go, WordPress-Docs (docs excluded from tests/), PHPCompatibilityWP (PHP 8.1+).
**Coding standards:** WordPress-Extra, WordPress-VIP-Go, WordPress-Docs (docs excluded from tests/), PHPCompatibilityWP (PHP 8.2+).

## CI/CD

GitHub Actions workflows (trigger on PRs):

- **`phpcs.yml`** — Runs `composer phpcs` on PHP 8.1
- **`phpunit.yml`** — Test matrix: PHP 8.1 + WP 6.0, PHP 8.1 + WP latest, PHP 8.3 + WP latest. Runs both standard and multisite tests.
- **`phpcs.yml`** — Runs `composer phpcs` on PHP 8.2
- **`phpunit.yml`** — Tests WordPress 6.8, 6.9, 7.0, 7.1, and master against supported PHP versions 8.2 through 8.5 (excluding WP 6.8 with PHP 8.5). Runs both standard and multisite tests.
- **`release.yml`** — On push to `trunk`: detects version changes, validates version consistency between plugin header and `WPCOMVIP__BLOCK_DATA_API__PLUGIN_VERSION` constant, creates GitHub Release with ZIP.

## Release Process
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ This plugin is currently developed for use on WordPress sites hosted on the VIP

## Installation

Requires WordPress 6.8 or later and PHP 8.2 or later.

### Install on WordPress VIP

The Block Data API plugin is authored and maintained by [WordPress VIP][wpvip], and made available to all WordPress sites by the [VIP Integrations Center][vip-ic]. Customers who host on WordPress VIP or use [`vip dev-env`](https://docs.wpvip.com/how-tos/local-development/use-the-vip-local-development-environment/) to develop locally have access to the Block Data API automatically. We recommend this activation method for WordPress VIP customers.
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
]
},
"require": {
"php": ">=8.1",
"php": ">=8.2",
"masterminds/html5": "^2.8",
"symfony/dom-crawler": "^6.0",
"symfony/css-selector": "^6.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"wp-phpunit/wp-phpunit": "^6.3",
"wp-phpunit/wp-phpunit": "^6.8",
"phpcompatibility/phpcompatibility-wp": "^2.1",
"automattic/vipwpcs": "^3.0",
"yoast/phpunit-polyfills": "^2.0",
Expand Down
4 changes: 2 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<rule ref="PHPCompatibilityWP"/>
<!-- For help in understanding this testVersion:
https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions -->
<config name="testVersion" value="8.1-"/>
<config name="testVersion" value="8.2-"/>

<rule ref="WordPress-Docs">
<exclude-pattern>/tests</exclude-pattern>
Expand All @@ -46,7 +46,7 @@
<rule ref="WordPress-Extra"/>
<!-- For help in understanding these custom sniff properties:
https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties -->
<config name="minimum_supported_wp_version" value="6.0"/>
<config name="minimum_supported_wp_version" value="6.8"/>

<rule ref="WordPress-VIP-Go">
<!-- These disallow anonymous functions as action callbacks -->
Expand Down
6 changes: 3 additions & 3 deletions src/parser/block-additions/core-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ public static function get_inner_blocks( array $inner_blocks, string|null $block
* phpcs:disable Generic.Commenting.DocComment.LongNotCapital
* https://github.com/WordPress/WordPress/blob/6.6.1/wp-includes/blocks/block.php#L73
*
* @param array $sourced_block Sourced block result.
* @param string $block_name Block name.
* @param array $sourced_block Sourced block result.
* @param string|null $block_name Block name, or null for parsed non-block content.
* @return array
*/
public static function remove_content_array( array $sourced_block, string $block_name ): array {
public static function remove_content_array( array $sourced_block, string|null $block_name ): array {
if ( self::$block_name !== $block_name ) {
return $sourced_block;
}
Expand Down
25 changes: 19 additions & 6 deletions src/parser/content-parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ protected function should_block_be_included( WP_Block $block, array $filter_opti
* Filter out blocks from the blocks output
*
* @param bool $is_block_included True if the block should be included, or false to filter it out.
* @param string $block_name Name of the parsed block, e.g. 'core/paragraph'.
* @param string|null $block_name Name of the parsed block, or null when parse_blocks() returns
* content found outside block delimiters.
* @param array $block Result of parse_blocks() for this block.
* Contains 'blockName', 'attrs', 'innerHTML', and 'innerBlocks' keys.
*/
Expand Down Expand Up @@ -291,7 +292,7 @@ protected function source_block( WP_Block $block, array $filter_options ): array
return null;
}

if ( ! $this->block_registry->is_registered( $block_name ) ) {
if ( null !== $block_name && ! $this->block_registry->is_registered( $block_name ) ) {
$this->add_missing_block_warning( $block_name );
}

Expand All @@ -311,7 +312,7 @@ protected function source_block( WP_Block $block, array $filter_options ): array
* Filters a block's inner blocks before recursive iteration.
*
* @param array $inner_blocks An array of inner block (WP_Block) instances.
* @param string $block_name Name of the parsed block, e.g. 'core/paragraph'.
* @param string|null $block_name Name of the parsed block, or null for non-block content.
* @param int $post_id Post ID associated with the parsed block.
* @param array $parsed_block Result of parse_blocks() for this block.
*/
Expand All @@ -331,7 +332,7 @@ protected function source_block( WP_Block $block, array $filter_options ): array
* Filters a block when parsing is complete.
*
* @param array $sourced_block An associative array of parsed block data with keys 'name' and 'attribute'.
* @param string $block_name Name of the parsed block, e.g. 'core/paragraph'.
* @param string|null $block_name Name of the parsed block, or null for non-block content.
* @param int $post_id Post ID associated with the parsed block.
* @param array $block Result of parse_blocks() for this block. Contains 'blockName', 'attrs', 'innerHTML', and 'innerBlocks' keys.
*/
Expand Down Expand Up @@ -371,6 +372,11 @@ protected function apply_sourced_attributes( WP_Block $block ): array {
if ( isset( $block_attributes[ $block_attribute_name ] ) ) {
// Attribute is already set in the block's delimiter attributes, skip.
continue;
} elseif ( 'core/html' === $block->name && 'content' === $block_attribute_name && 'local' === ( $block_attribute_definition['role'] ?? null ) ) {
// WordPress 6.8 uses the 'raw' source and follows the sourced-attribute path below.
// Newer core/html definitions use role 'local' and keep the value in inner HTML.
$block_attributes[ $block_attribute_name ] = trim( $block->inner_html );
continue;
} elseif ( null !== $attribute_default_value ) {
// Attribute is unset and has a default value, use default value.
$block_attributes[ $block_attribute_name ] = $attribute_default_value;
Expand Down Expand Up @@ -501,6 +507,12 @@ protected function source_block_attribute( $crawler, $block_attribute_definition

if ( $crawler->count() > 0 ) {
$attribute_value = $crawler->attr( $attribute );
if ( 'boolean' === ( $block_attribute_definition['type'] ?? null ) ) {
// HTML boolean attributes are true when present, even if written as
// controls="false". Omit the attribute to mean false; preserve that false
// instead of replacing it with the block's default.
$attribute_value = null !== $attribute_value;
}
}

return $attribute_value;
Expand Down Expand Up @@ -679,8 +691,9 @@ protected function source_block_tag( $crawler, $block_attribute_definition ) {
* @access private
*/
protected function source_block_raw( $crawler ) {
// The only current usage of the 'raw' attribute in Gutenberg core is the 'core/html' block:
// https://github.com/WordPress/gutenberg/blob/6517008/packages/block-library/src/html/block.json#L13
// Older core/html definitions, including WordPress 6.8, use the 'raw' source.
// Newer core/html definitions use role 'local', handled in apply_sourced_attributes().
// core/shortcode, core/freeform, and core/missing also use this raw parser.
// Also see tag attribute parsing in Gutenberg:
// https://github.com/WordPress/gutenberg/blob/6517008/packages/blocks/src/api/parser/get-block-attributes.js#L131

Expand Down
49 changes: 49 additions & 0 deletions tests/parser/blocks/test-html-block.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
/**
* Tests for the core/html block.
*
* @package vip-block-data-api
*/

namespace WPCOMVIP\BlockDataApi;

/**
* Core HTML block tests.
*/
class HtmlBlockTest extends RegistryTestCase {
public function test_parse_registered_core_html_content() {
$parser = new ContentParser( $this->get_block_registry() );
$result = $parser->parse( '<!-- wp:html --><div>Custom HTML</div><!-- /wp:html -->' );

$this->assertIsArray( $result );
$this->assertSame( '<div>Custom HTML</div>', $result['blocks'][0]['attributes']['content'] );
}

public function test_parse_local_content_without_source() {
$registry = $this->get_block_registry();
$original_block = $registry->get_registered( 'core/html' );
$this->assertNotNull( $original_block );

$registry->unregister( 'core/html' );
$registry->register( 'core/html', [
'attributes' => [
'content' => [
'type' => 'string',
'role' => 'local',
],
],
] );

try {
$html = "<!-- wp:html -->\n<div>First</div><div>Second</div>\n<!-- /wp:html -->";
$parser = new ContentParser( $registry );
$result = $parser->parse( $html );

$this->assertIsArray( $result, sprintf( 'Unexpected parser output: %s', wp_json_encode( $result ) ) );
$this->assertSame( '<div>First</div><div>Second</div>', $result['blocks'][0]['attributes']['content'] );
} finally {
$registry->unregister( 'core/html' );
$registry->register( $original_block );
}
}
}
24 changes: 24 additions & 0 deletions tests/parser/blocks/test-video-block.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php
/**
* Tests for the core/video block.
*
* @package vip-block-data-api
*/

namespace WPCOMVIP\BlockDataApi;

/**
* Core video block tests.
*/
class VideoBlockTest extends RegistryTestCase {
public function test_controls_follow_video_markup() {
$parser = new ContentParser( $this->get_block_registry() );
$off = $parser->parse( '<!-- wp:video --><figure class="wp-block-video"><video src="/video.mp4"></video></figure><!-- /wp:video -->' );
$on = $parser->parse( '<!-- wp:video --><figure class="wp-block-video"><video controls src="/video.mp4"></video></figure><!-- /wp:video -->' );

$this->assertIsArray( $off );
$this->assertIsArray( $on );
$this->assertFalse( $off['blocks'][0]['attributes']['controls'] );
$this->assertTrue( $on['blocks'][0]['attributes']['controls'] );
}
}
27 changes: 27 additions & 0 deletions tests/parser/sources/test-source-attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,31 @@ public function test_parse_attribute_source__with_asterisk_selector() {
$this->assertArrayHasKey( 'blocks', $blocks, sprintf( 'Unexpected parser output: %s', wp_json_encode( $blocks ) ) );
$this->assertArraySubset( $expected_blocks, $blocks['blocks'], true );
}

public function test_parse_boolean_attribute_source_with_true_default() {
$this->register_block_with_attributes( 'test/video', [
'controls' => [
'type' => 'boolean',
'source' => 'attribute',
'selector' => 'video',
'attribute' => 'controls',
'default' => true,
],
] );

$content_parser = new ContentParser( $this->get_block_registry() );
$without = $content_parser->parse( '<!-- wp:test/video --><video src="/video.mp4"></video><!-- /wp:test/video -->' );
$with = $content_parser->parse( '<!-- wp:test/video --><video controls src="/video.mp4"></video><!-- /wp:test/video -->' );
$with_false_text = $content_parser->parse( '<!-- wp:test/video --><video controls="false" src="/video.mp4"></video><!-- /wp:test/video -->' );
$missing_video = $content_parser->parse( '<!-- wp:test/video --><p>No video</p><!-- /wp:test/video -->' );

$this->assertIsArray( $without );
$this->assertIsArray( $with );
$this->assertIsArray( $with_false_text );
$this->assertIsArray( $missing_video );
$this->assertFalse( $without['blocks'][0]['attributes']['controls'] );
$this->assertTrue( $with['blocks'][0]['attributes']['controls'] );
$this->assertTrue( $with_false_text['blocks'][0]['attributes']['controls'] );
$this->assertTrue( $missing_video['blocks'][0]['attributes']['controls'] );
}
}
23 changes: 23 additions & 0 deletions tests/parser/test-content-parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,29 @@ public function test_parse_classic_editor_content() {
);
}

public function test_parse_blocks_with_classic_content_between_them() {
$this->register_block_with_attributes( 'test/paragraph', [
'content' => [
'type' => 'string',
'source' => 'html',
'selector' => 'p',
],
] );

$html = '<!-- wp:test/paragraph --><p>First block</p><!-- /wp:test/paragraph -->'
. '<p>Imported classic content</p>'
. '<!-- wp:test/paragraph --><p>Second block</p><!-- /wp:test/paragraph -->';

$content_parser = new ContentParser( $this->get_block_registry() );
$result = $content_parser->parse( $html );

$this->assertIsArray( $result, sprintf( 'Unexpected parser output: %s', wp_json_encode( $result ) ) );
$this->assertSame( [ 'test/paragraph', null, 'test/paragraph' ], array_column( $result['blocks'], 'name' ) );
$this->assertSame( 'First block', $result['blocks'][0]['attributes']['content'] );
$this->assertSame( 'Second block', $result['blocks'][2]['attributes']['content'] );
$this->assertArrayNotHasKey( 'warnings', $result );
}

/* Empty content */

public function test_parse_empty_content() {
Expand Down
4 changes: 2 additions & 2 deletions vendor/composer/platform_check.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

$issues = array();

if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
}

if ($issues) {
Expand Down
Loading
Loading