diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
new file mode 100644
index 00000000..34c463bb
--- /dev/null
+++ b/.github/workflows/playwright.yml
@@ -0,0 +1,25 @@
+name: Playwright Tests
+on: pull_request
+jobs:
+ test:
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 14
+ - name: Install dependencies
+ run: npm ci
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+ - name: Build
+ run: npm run build
+ - name: Run Playwright tests
+ run: npx playwright test
+ - uses: actions/upload-artifact@v3
+ if: always()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index 427fe02a..bf7218b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
node_modules
build
vendor
-composer.lock
\ No newline at end of file
+composer.lock
+/test-results/
+/playwright-report/
+/playwright/.cache/
diff --git a/e2e/tovdom-full.html b/e2e/tovdom-full.html
new file mode 100644
index 00000000..b46b7596
--- /dev/null
+++ b/e2e/tovdom-full.html
@@ -0,0 +1,20 @@
+
+
+
+ toVdom - full
+
+
+
+
+
+
+ This should not be shown because we are in full mode.
+
+
+
+
+
+
+
+
+
diff --git a/e2e/tovdom-full.spec.ts b/e2e/tovdom-full.spec.ts
new file mode 100644
index 00000000..fc5ca7d0
--- /dev/null
+++ b/e2e/tovdom-full.spec.ts
@@ -0,0 +1,13 @@
+import { join } from 'path';
+import { test, expect } from '@playwright/test';
+
+test.describe('toVdom - full', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('file://' + join(__dirname, 'tovdom-full.html'));
+ });
+
+ test('it should stop when it founds wp-ignore', async ({ page }) => {
+ const el = page.getByTestId('inside wp-ignore');
+ await expect(el).toBeVisible();
+ });
+});
diff --git a/e2e/tovdom-islands.html b/e2e/tovdom-islands.html
new file mode 100644
index 00000000..efaadbba
--- /dev/null
+++ b/e2e/tovdom-islands.html
@@ -0,0 +1,70 @@
+
+
+
+ toVdom - islands
+
+
+
+
+
+ This should be shown because it is inside an island.
+
+
+
+
+
+
+ This should not be shown because it is inside an island.
+
+
+
+
+
+
+
+
+ This should be shown because it is inside an inner
+ block of an isolated island.
+
+
+
+
+
+
+
+
+
+ This should not have two template wrappers because
+ that means we hydrated twice.
+
+
+
+
+
+
+
+
+
+
+ This should not be shown because even though it
+ is inside an inner block of an isolated island,
+ it's inside an new island.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/e2e/tovdom-islands.spec.ts b/e2e/tovdom-islands.spec.ts
new file mode 100644
index 00000000..10f530d6
--- /dev/null
+++ b/e2e/tovdom-islands.spec.ts
@@ -0,0 +1,48 @@
+import { join } from 'path';
+import { test, expect } from '@playwright/test';
+
+test.describe('toVdom - isands', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('file://' + join(__dirname, 'tovdom-islands.html'));
+ });
+
+ test('directives that are not inside islands should not be hydrated', async ({
+ page,
+ }) => {
+ const el = page.getByTestId('not inside an island');
+ await expect(el).toBeVisible();
+ });
+
+ test('directives that are inside islands should be hydrated', async ({
+ page,
+ }) => {
+ const el = page.getByTestId('inside an island');
+ await expect(el).toBeHidden();
+ });
+
+ test('directives that are inside inner blocks of isolated islands should not be hydrated', async ({
+ page,
+ }) => {
+ const el = page.getByTestId(
+ 'inside an inner block of an isolated island'
+ );
+ await expect(el).toBeVisible();
+ });
+
+ test('directives inside islands should not be hydrated twice', async ({
+ page,
+ }) => {
+ const el = page.getByTestId('island inside another island');
+ const templates = el.locator('template');
+ expect(await templates.count()).toEqual(1);
+ });
+
+ test('islands inside inner blocks of isolated islands should be hydrated', async ({
+ page,
+ }) => {
+ const el = page.getByTestId(
+ 'island inside inner block of isolated island'
+ );
+ await expect(el).toBeHidden();
+ });
+});
diff --git a/e2e/tovdom.js b/e2e/tovdom.js
new file mode 100644
index 00000000..06e44fd5
--- /dev/null
+++ b/e2e/tovdom.js
@@ -0,0 +1,7 @@
+import wpx from '../src/runtime/wpx';
+
+wpx({
+ state: {
+ falseValue: false,
+ },
+});
diff --git a/package-lock.json b/package-lock.json
index d70f9c5a..1445f016 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2034,6 +2034,24 @@
"fastq": "^1.6.0"
}
},
+ "@playwright/test": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.29.0.tgz",
+ "integrity": "sha512-gp5PVBenxTJsm2bATWDNc2CCnrL5OaA/MXQdJwwkGQtqTjmY+ZOqAdLqo49O9MLTDh2vYh+tHWDnmFsILnWaeA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "playwright-core": "1.29.0"
+ },
+ "dependencies": {
+ "playwright-core": {
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.29.0.tgz",
+ "integrity": "sha512-pboOm1m0RD6z1GtwAbEH60PYRfF87vKdzOSRw2RyO0Y0a7utrMyWN2Au1ojGvQr4umuBMODkKTv607YIRypDSQ==",
+ "dev": true
+ }
+ }
+ },
"@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.8.tgz",
diff --git a/package.json b/package.json
index f02a0ad1..1495863a 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"devDependencies": {
"@babel/core": "^7.17.10",
"@babel/preset-env": "^7.17.10",
+ "@playwright/test": "^1.29.0",
"@prettier/plugin-php": "^0.18.9",
"@types/jest": "^27.5.1",
"@wordpress/env": "^4.4.0",
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..6747b3a6
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,107 @@
+import type { PlaywrightTestConfig } from '@playwright/test';
+import { devices } from '@playwright/test';
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+const config: PlaywrightTestConfig = {
+ testDir: './e2e',
+ /* Maximum time one test can run for. */
+ timeout: 30 * 1000,
+ expect: {
+ /**
+ * Maximum time expect() should wait for the condition to be met.
+ * For example in `await expect(locator).toHaveText();`
+ */
+ timeout: 5000,
+ },
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
+ actionTimeout: 0,
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ // baseURL: 'http://localhost:3000',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: {
+ ...devices['Desktop Chrome'],
+ },
+ },
+
+ {
+ name: 'firefox',
+ use: {
+ ...devices['Desktop Firefox'],
+ },
+ },
+
+ {
+ name: 'webkit',
+ use: {
+ ...devices['Desktop Safari'],
+ },
+ },
+
+ /* Test against mobile viewports. */
+ // {
+ // name: 'Mobile Chrome',
+ // use: {
+ // ...devices['Pixel 5'],
+ // },
+ // },
+ // {
+ // name: 'Mobile Safari',
+ // use: {
+ // ...devices['iPhone 12'],
+ // },
+ // },
+
+ /* Test against branded browsers. */
+ // {
+ // name: 'Microsoft Edge',
+ // use: {
+ // channel: 'msedge',
+ // },
+ // },
+ // {
+ // name: 'Google Chrome',
+ // use: {
+ // channel: 'chrome',
+ // },
+ // },
+ ],
+
+ /* Folder for test artifacts such as screenshots, videos, traces, etc. */
+ // outputDir: 'test-results/',
+
+ /* Run your local dev server before starting the tests */
+ // webServer: {
+ // command: 'npm run start',
+ // port: 3000,
+ // },
+};
+
+export default config;
diff --git a/src/admin/admin-page.php b/src/admin/admin-page.php
index ffdea070..0435cd29 100644
--- a/src/admin/admin-page.php
+++ b/src/admin/admin-page.php
@@ -11,16 +11,16 @@ function wp_directives_register_menu() {
}
add_action( 'admin_menu', 'wp_directives_register_menu' );
-function wp_directives_render_admin_page() {?>
+function wp_directives_render_admin_page() { ?>
-
WP Directives
-
+
+
>
diff --git a/src/runtime/components.js b/src/runtime/components.js
index acc75298..07a6e05f 100644
--- a/src/runtime/components.js
+++ b/src/runtime/components.js
@@ -1,6 +1,7 @@
import { useMemo } from 'preact/hooks';
import { deepSignal } from './deepsignal';
import { component } from './hooks';
+import { getCallback } from './utils';
export default () => {
const WpContext = ({ children, data, context: { Provider } }) => {
@@ -8,4 +9,16 @@ export default () => {
return {children};
};
component('wp-context', WpContext);
+
+ const WpShow = ({ children, when }) => {
+ const cb = getCallback(when);
+ const value =
+ typeof cb === 'function' ? cb({ state: window.wpx.state }) : cb;
+ if (value) {
+ return children;
+ } else {
+ return {children};
+ }
+ };
+ component('wp-show', WpShow);
};
diff --git a/src/runtime/deepsignal.js b/src/runtime/deepsignal.js
index 84901785..08742367 100644
--- a/src/runtime/deepsignal.js
+++ b/src/runtime/deepsignal.js
@@ -1,25 +1,27 @@
import { signal } from '@preact/signals';
-import { knownSymbols, shouldWrap } from './utils';
const proxyToSignals = new WeakMap();
const objToProxy = new WeakMap();
-const returnSignal = /^\$/;
export const deepSignal = (obj) => new Proxy(obj, handlers);
+export const options = { returnSignal: /^\$/ };
const handlers = {
get(target, prop, receiver) {
- if (typeof prop === 'symbol' && knownSymbols.has(prop))
- return Reflect.get(target, prop, receiver);
- const shouldReturnSignal = returnSignal.test(prop);
- const key = shouldReturnSignal ? prop.replace(returnSignal, '') : prop;
+ const returnSignal = options.returnSignal.test(prop);
+ const key = returnSignal
+ ? prop.replace(options.returnSignal, '')
+ : prop;
if (!proxyToSignals.has(receiver))
proxyToSignals.set(receiver, new Map());
const signals = proxyToSignals.get(receiver);
if (!signals.has(key)) {
let val = Reflect.get(target, key, receiver);
- if (typeof val === 'object' && val !== null && shouldWrap(val))
- val = new Proxy(val, handlers);
+ if (typeof val === 'object' && val !== null) {
+ if (!objToProxy.has(val))
+ objToProxy.set(val, new Proxy(val, handlers));
+ val = objToProxy.get(val);
+ }
signals.set(key, signal(val));
}
return returnSignal ? signals.get(key) : signals.get(key).value;
@@ -27,7 +29,7 @@ const handlers = {
set(target, prop, val, receiver) {
let internal = val;
- if (typeof val === 'object' && val !== null && shouldWrap(val)) {
+ if (typeof val === 'object' && val !== null) {
if (!objToProxy.has(val))
objToProxy.set(val, new Proxy(val, handlers));
internal = objToProxy.get(val);
diff --git a/src/runtime/hooks.js b/src/runtime/hooks.js
index 617f6009..080accfd 100644
--- a/src/runtime/hooks.js
+++ b/src/runtime/hooks.js
@@ -43,9 +43,7 @@ options.vnode = (vnode) => {
{ ...vnode.props, context },
vnode.props.children
);
- }
-
- if (wp) {
+ } else if (wp) {
const props = vnode.props;
delete props.wp;
if (!props._wrapped) {
diff --git a/src/runtime/router.js b/src/runtime/router.js
index 94c6f659..3347a813 100644
--- a/src/runtime/router.js
+++ b/src/runtime/router.js
@@ -1,5 +1,5 @@
import { hydrate, render } from 'preact';
-import toVdom from './vdom';
+import { toVdom, hydratedIslands } from './vdom';
import { createRootFragment } from './utils';
// The root to render the vdom (document.body).
@@ -94,13 +94,25 @@ window.addEventListener('popstate', async () => {
// Initialize the router with the initial DOM.
export const init = async () => {
- // Create the root fragment to hydrate everything.
- rootFragment = createRootFragment(document.documentElement, document.body);
- const body = toVdom(document.body);
- hydrate(body, rootFragment);
-
if (hasClientSideTransitions(document.head)) {
+ // Create the root fragment to hydrate everything.
+ rootFragment = createRootFragment(
+ document.documentElement,
+ document.body
+ );
+
+ const body = toVdom(document.body);
+ hydrate(body, rootFragment);
+
const head = await fetchHead(document.head);
pages.set(cleanUrl(window.location), Promise.resolve({ body, head }));
+ } else {
+ document.querySelectorAll('[wp-island]').forEach((node) => {
+ if (!hydratedIslands.has(node)) {
+ const fragment = createRootFragment(node.parentNode, node);
+ const vdom = toVdom(node);
+ hydrate(vdom, fragment);
+ }
+ });
}
};
diff --git a/src/runtime/vdom.js b/src/runtime/vdom.js
index 104b6fda..754146ce 100644
--- a/src/runtime/vdom.js
+++ b/src/runtime/vdom.js
@@ -1,11 +1,15 @@
import { h } from 'preact';
+export const hydratedIslands = new WeakSet();
+
// Recursive function that transfoms a DOM tree into vDOM.
-export default function toVdom(node) {
+export function toVdom(node) {
const props = {};
const { attributes, childNodes } = node;
const wpDirectives = {};
let hasWpDirectives = false;
+ let ignore = false;
+ let island = false;
if (node.nodeType === 3) return node.data;
if (node.nodeType === 4) {
@@ -16,14 +20,20 @@ export default function toVdom(node) {
for (let i = 0; i < attributes.length; i++) {
const n = attributes[i].name;
if (n[0] === 'w' && n[1] === 'p' && n[2] === '-' && n[3]) {
- hasWpDirectives = true;
- let val = attributes[i].value;
- try {
- val = JSON.parse(val);
- } catch (e) {}
- const [, prefix, suffix] = /wp-([^:]+):?(.*)$/.exec(n);
- wpDirectives[prefix] = wpDirectives[prefix] || {};
- wpDirectives[prefix][suffix || 'default'] = val;
+ if (n === 'wp-ignore') {
+ ignore = true;
+ } else if (n === 'wp-island') {
+ island = true;
+ } else {
+ hasWpDirectives = true;
+ let val = attributes[i].value;
+ try {
+ val = JSON.parse(val);
+ } catch (e) {}
+ const [, prefix, suffix] = /wp-([^:]+):?(.*)$/.exec(n);
+ wpDirectives[prefix] = wpDirectives[prefix] || {};
+ wpDirectives[prefix][suffix || 'default'] = val;
+ }
} else if (n === 'ref') {
continue;
} else {
@@ -31,6 +41,12 @@ export default function toVdom(node) {
}
}
+ if (ignore && !island)
+ return h(node.localName, {
+ dangerouslySetInnerHTML: { __html: node.innerHTML },
+ });
+ if (island) hydratedIslands.add(node);
+
if (hasWpDirectives) props.wp = wpDirectives;
const children = [];
diff --git a/src/runtime/wpx.js b/src/runtime/wpx.js
new file mode 100644
index 00000000..6f7c1b47
--- /dev/null
+++ b/src/runtime/wpx.js
@@ -0,0 +1,10 @@
+import { deepSignal } from './deepsignal';
+import { deepMerge } from './utils';
+
+const rawState = {};
+window.wpx = { state: deepSignal(rawState) };
+
+export default ({ state, ...block }) => {
+ deepMerge(window.wpx, block);
+ deepMerge(rawState, state);
+};
diff --git a/webpack.config.js b/webpack.config.js
index c2070f52..d6eecafa 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,54 +1,60 @@
+const defaultConfig = require('@wordpress/scripts/config/webpack.config');
const { resolve } = require('path');
-module.exports = {
- entry: {
- runtime: './src/runtime',
- },
- output: {
- filename: '[name].js',
- path: resolve(process.cwd(), 'build'),
- },
- optimization: {
- runtimeChunk: {
- name: 'vendors',
+module.exports = [
+ defaultConfig,
+ {
+ ...defaultConfig,
+ entry: {
+ runtime: './src/runtime',
+ 'e2e/tovdom': './e2e/tovdom',
+ },
+ output: {
+ filename: '[name].js',
+ path: resolve(process.cwd(), 'build'),
},
- splitChunks: {
- cacheGroups: {
- vendors: {
- test: /[\\/]node_modules[\\/]/,
- name: 'vendors',
- minSize: 0,
- chunks: 'all',
+ optimization: {
+ runtimeChunk: {
+ name: 'vendors',
+ },
+ splitChunks: {
+ cacheGroups: {
+ vendors: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendors',
+ minSize: 0,
+ chunks: 'all',
+ },
},
},
},
- },
- module: {
- rules: [
- {
- test: /\.(j|t)sx?$/,
- exclude: /node_modules/,
- use: [
- {
- loader: require.resolve('babel-loader'),
- options: {
- cacheDirectory:
- process.env.BABEL_CACHE_DIRECTORY || true,
- babelrc: false,
- configFile: false,
- presets: [
- [
- '@babel/preset-react',
- {
- runtime: 'automatic',
- importSource: 'preact',
- },
+ module: {
+ rules: [
+ {
+ test: /\.(j|t)sx?$/,
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: require.resolve('babel-loader'),
+ options: {
+ cacheDirectory:
+ process.env.BABEL_CACHE_DIRECTORY || true,
+ babelrc: false,
+ configFile: false,
+ presets: [
+ [
+ '@babel/preset-react',
+ {
+ runtime: 'automatic',
+ importSource: 'preact',
+ },
+ ],
],
- ],
+ },
},
- },
- ],
- },
- ],
+ ],
+ },
+ ],
+ },
},
-};
+];
diff --git a/wp-directives.php b/wp-directives.php
index 4ec17a65..64e13ac3 100644
--- a/wp-directives.php
+++ b/wp-directives.php
@@ -10,20 +10,27 @@
* Text Domain: wp-directives
*/
- // Check if Gutenberg plugin is active
+// Check if Gutenberg plugin is active.
if ( ! function_exists( 'is_plugin_active' ) ) {
- include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
+ include_once ABSPATH . 'wp-admin/includes/plugin.php';
}
+
if ( ! is_plugin_active( 'gutenberg/gutenberg.php' ) ) {
- // Show an error message
+ // Show an error message.
add_action(
'admin_notices',
- function() {
- echo sprintf( '', __( 'This plugin requires the Gutenberg plugin to be installed and activated.', 'wp-directives' ) );
+ function () {
+ echo sprintf(
+ '',
+ __(
+ 'This plugin requires the Gutenberg plugin to be installed and activated.',
+ 'wp-directives'
+ )
+ );
}
);
- // Deactivate the plugin
+ // Deactivate the plugin.
deactivate_plugins( plugin_basename( __FILE__ ) );
return;
}
@@ -91,8 +98,14 @@ function wp_directives_add_wp_link_attribute( $block_content ) {
$link = parse_url( $w->get_attribute( 'href' ) );
if ( ! isset( $link['host'] ) || $link['host'] === $site_url['host'] ) {
$classes = $w->get_attribute( 'class' );
- if ( str_contains( $classes, 'query-pagination' ) || str_contains( $classes, 'page-numbers' ) ) {
- $w->set_attribute( 'wp-link', '{ "prefetch": true, "scroll": false }' );
+ if (
+ str_contains( $classes, 'query-pagination' ) ||
+ str_contains( $classes, 'page-numbers' )
+ ) {
+ $w->set_attribute(
+ 'wp-link',
+ '{ "prefetch": true, "scroll": false }'
+ );
} else {
$w->set_attribute( 'wp-link', '{ "prefetch": true }' );
}
@@ -101,22 +114,85 @@ function wp_directives_add_wp_link_attribute( $block_content ) {
return (string) $w;
}
// We go only through the Query Loops and the template parts until we find a better solution.
-add_filter( 'render_block_core/query', 'wp_directives_add_wp_link_attribute', 10, 1 );
-add_filter( 'render_block_core/template-part', 'wp_directives_add_wp_link_attribute', 10, 1 );
+add_filter(
+ 'render_block_core/query',
+ 'wp_directives_add_wp_link_attribute',
+ 10,
+ 1
+);
+add_filter(
+ 'render_block_core/template-part',
+ 'wp_directives_add_wp_link_attribute',
+ 10,
+ 1
+);
+
-function wp_directives_client_site_transitions_meta_tag() {
- if ( apply_filters( 'client_side_transitions', false ) ) {
+function wp_directives_get_client_side_transitions() {
+ static $client_side_transitions = null;
+ if ( is_null( $client_side_transitions ) ) {
+ $client_side_transitions = apply_filters( 'client_side_transitions', false );
+ }
+ return $client_side_transitions;
+}
+
+function wp_directives_add_client_side_transitions_meta_tag() {
+ if ( wp_directives_get_client_side_transitions() ) {
echo '';
}
}
-add_action( 'wp_head', 'wp_directives_client_site_transitions_meta_tag', 10, 0 );
+add_action( 'wp_head', 'wp_directives_add_client_side_transitions_meta_tag' );
-/* User code */
function wp_directives_client_site_transitions_option() {
$options = get_option( 'wp_directives_plugin_settings' );
return $options['client_side_transitions'];
}
add_filter(
'client_side_transitions',
- 'wp_directives_client_site_transitions_option'
+ 'wp_directives_client_site_transitions_option',
+ 9
);
+
+function wp_directives_mark_interactive_blocks( $block_content, $block, $instance ) {
+ if ( wp_directives_get_client_side_transitions() ) {
+ return $block_content;
+ }
+
+ // Append the `wp-ignore` attribute for inner blocks of interactive blocks.
+ if ( isset( $instance->parsed_block['isolated'] ) ) {
+ $w = new WP_HTML_Tag_Processor( $block_content );
+ $w->next_tag();
+ $w->set_attribute( 'wp-ignore', '' );
+ $block_content = (string) $w;
+ }
+
+ // Return if it's not interactive.
+ if ( ! block_has_support( $instance->block_type, array( 'interactivity' ) ) ) {
+ return $block_content;
+ }
+
+ // Add the `wp-island` attribute if it's interactive.
+ $w = new WP_HTML_Tag_Processor( $block_content );
+ $w->next_tag();
+ $w->set_attribute( 'wp-island', '' );
+
+ return (string) $w;
+}
+add_filter( 'render_block', 'wp_directives_mark_interactive_blocks', 10, 3 );
+
+/**
+ * Add a flag to mark inner blocks of isolated interactive blocks.
+ */
+function bhe_inner_blocks( $parsed_block, $source_block, $parent_block ) {
+ if (
+ isset( $parent_block ) &&
+ block_has_support(
+ $parent_block->block_type,
+ array( 'interactivity', 'isolated' )
+ )
+ ) {
+ $parsed_block['isolated'] = true;
+ }
+ return $parsed_block;
+}
+add_filter( 'render_block_data', 'bhe_inner_blocks', 10, 3 );