diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml
index aabb18b..00c7744 100644
--- a/.github/workflows/phpcs.yml
+++ b/.github/workflows/phpcs.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
php-versions:
- - 8.1
+ - 8.2
steps:
- uses: actions/checkout@v7
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index 58747c0..fe1c69d 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -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
diff --git a/AGENTS.md b/AGENTS.md
index 155724d..3bf970e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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
@@ -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
@@ -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
diff --git a/README.md b/README.md
index e833d57..7fac48e 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/composer.json b/composer.json
index 0ba31b2..dc70e34 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
diff --git a/composer.lock b/composer.lock
index 6716b23..fc62e62 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "6c55e96eae0472666466689b8b782260",
+ "content-hash": "028d823335c0bfc1f678f0b27e4f124b",
"packages": [
{
"name": "masterminds/html5",
@@ -3078,7 +3078,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=8.1"
+ "php": ">=8.2"
},
"platform-dev": {},
"plugin-api-version": "2.9.0"
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index f307934..4aab095 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -34,7 +34,7 @@
-
+
/tests
@@ -46,7 +46,7 @@
-
+
diff --git a/src/parser/block-additions/core-block.php b/src/parser/block-additions/core-block.php
index 1b410a5..028a803 100644
--- a/src/parser/block-additions/core-block.php
+++ b/src/parser/block-additions/core-block.php
@@ -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;
}
diff --git a/src/parser/content-parser.php b/src/parser/content-parser.php
index e24f521..44185ec 100644
--- a/src/parser/content-parser.php
+++ b/src/parser/content-parser.php
@@ -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.
*/
@@ -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 );
}
@@ -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.
*/
@@ -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.
*/
@@ -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;
@@ -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;
@@ -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
diff --git a/tests/parser/blocks/test-html-block.php b/tests/parser/blocks/test-html-block.php
new file mode 100644
index 0000000..c121486
--- /dev/null
+++ b/tests/parser/blocks/test-html-block.php
@@ -0,0 +1,49 @@
+get_block_registry() );
+ $result = $parser->parse( 'Custom HTML
' );
+
+ $this->assertIsArray( $result );
+ $this->assertSame( 'Custom HTML
', $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 = "\nFirst
Second
\n";
+ $parser = new ContentParser( $registry );
+ $result = $parser->parse( $html );
+
+ $this->assertIsArray( $result, sprintf( 'Unexpected parser output: %s', wp_json_encode( $result ) ) );
+ $this->assertSame( 'First
Second
', $result['blocks'][0]['attributes']['content'] );
+ } finally {
+ $registry->unregister( 'core/html' );
+ $registry->register( $original_block );
+ }
+ }
+}
diff --git a/tests/parser/blocks/test-video-block.php b/tests/parser/blocks/test-video-block.php
new file mode 100644
index 0000000..a7b8be9
--- /dev/null
+++ b/tests/parser/blocks/test-video-block.php
@@ -0,0 +1,24 @@
+get_block_registry() );
+ $off = $parser->parse( '' );
+ $on = $parser->parse( '' );
+
+ $this->assertIsArray( $off );
+ $this->assertIsArray( $on );
+ $this->assertFalse( $off['blocks'][0]['attributes']['controls'] );
+ $this->assertTrue( $on['blocks'][0]['attributes']['controls'] );
+ }
+}
diff --git a/tests/parser/sources/test-source-attribute.php b/tests/parser/sources/test-source-attribute.php
index 2273127..b539928 100644
--- a/tests/parser/sources/test-source-attribute.php
+++ b/tests/parser/sources/test-source-attribute.php
@@ -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( '' );
+ $with = $content_parser->parse( '' );
+ $with_false_text = $content_parser->parse( '' );
+ $missing_video = $content_parser->parse( 'No 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'] );
+ }
}
diff --git a/tests/parser/test-content-parser.php b/tests/parser/test-content-parser.php
index dc157d6..a75db3a 100644
--- a/tests/parser/test-content-parser.php
+++ b/tests/parser/test-content-parser.php
@@ -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 = 'First block
'
+ . 'Imported classic content
'
+ . 'Second block
';
+
+ $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() {
diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php
index 4c3a5d6..d32d90c 100644
--- a/vendor/composer/platform_check.php
+++ b/vendor/composer/platform_check.php
@@ -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) {
diff --git a/vip-block-data-api.php b/vip-block-data-api.php
index 50ed22c..4ef6e3d 100644
--- a/vip-block-data-api.php
+++ b/vip-block-data-api.php
@@ -6,9 +6,9 @@
* Author: WordPress VIP
* Text Domain: vip-block-data-api
* Version: 1.4.10
- * Requires at least: 6.0
- * Tested up to: 6.8
- * Requires PHP: 8.1
+ * Requires at least: 6.8
+ * Tested up to: 7.1
+ * Requires PHP: 8.2
* License: GPL-3
* License URI: https://www.gnu.org/licenses/gpl-3.0.html
*
@@ -20,13 +20,13 @@
if ( ! defined( 'VIP_BLOCK_DATA_API_LOADED' ) ) {
define( 'VIP_BLOCK_DATA_API_LOADED', true );
- // ToDo: When 6.4 is our min version, switch to wp_admin_notice.
global $wp_version;
- if ( version_compare( phpversion(), '8.1', '<' ) || version_compare( $wp_version, '6.0', '<' ) ) {
+ if ( version_compare( phpversion(), '8.2', '<' ) || version_compare( $wp_version, '6.8', '<' ) ) {
+ // This callback must also work on unsupported WordPress versions before 6.4.
add_action( 'admin_notices', function () {
?>