From 8829b45ebe91539dd9dffe0a870c0fa5585ed29c Mon Sep 17 00:00:00 2001
From: Stuart McAlpine
Date: Fri, 7 Nov 2025 16:57:57 +0000
Subject: [PATCH 1/7] Assign markers in PHP to fix editor JS issues
---
composer.json | 3 +-
composer.lock | 159 ++++++++++++-
.../Post/BlockAttributes/BlockAttributes.php | 221 +++++++++++++++++-
.../Post/BlockAttributes/addAttributes.js | 78 ++++---
.../Post/BlockAttributes/addControls.js | 174 ++++++++++----
.../helpers/getBlockMarkerAttribute.js | 88 -------
src/Component/Post/BlockAttributes/index.js | 1 +
.../Post/BlockAttributes/refreshAfterSave.js | 165 +++++++++++++
.../e2e/block-editor/block-inserter.cy.js | 161 +++++++++++++
9 files changed, 881 insertions(+), 169 deletions(-)
delete mode 100644 src/Component/Post/BlockAttributes/helpers/getBlockMarkerAttribute.js
create mode 100644 src/Component/Post/BlockAttributes/refreshAfterSave.js
create mode 100644 tests/cypress/e2e/block-editor/block-inserter.cy.js
diff --git a/composer.json b/composer.json
index 3ad97919..2aa7d05e 100644
--- a/composer.json
+++ b/composer.json
@@ -7,7 +7,8 @@
"require": {
"php": ">=8.0",
"symfony/dom-crawler": "^5.4",
- "symfony/property-access": "^5.4"
+ "symfony/property-access": "^5.4",
+ "symfony/uid": "^7.3"
},
"require-dev": {
"automattic/vipwpcs": "^3.0.1",
diff --git a/composer.lock b/composer.lock
index 414c570f..ca087322 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": "871f668f3d5222a0e58403030261d782",
+ "content-hash": "cf0482212addeb396b0cced4fb5170fc",
"packages": [
{
"name": "symfony/deprecation-contracts",
@@ -567,6 +567,89 @@
],
"time": "2025-01-02T08:10:11+00:00"
},
+ {
+ "name": "symfony/polyfill-uuid",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-uuid.git",
+ "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
+ "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-uuid": "*"
+ },
+ "suggest": {
+ "ext-uuid": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Uuid\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for uuid functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-09T11:45:10+00:00"
+ },
{
"name": "symfony/property-access",
"version": "v5.4.45",
@@ -827,6 +910,80 @@
}
],
"time": "2025-09-11T14:36:48+00:00"
+ },
+ {
+ "name": "symfony/uid",
+ "version": "v7.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/uid.git",
+ "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb",
+ "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "symfony/polyfill-uuid": "^1.15"
+ },
+ "require-dev": {
+ "symfony/console": "^6.4|^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Uid\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to generate and represent UIDs",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "UID",
+ "ulid",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/uid/tree/v7.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-06-27T19:55:54+00:00"
}
],
"packages-dev": [
diff --git a/src/Component/Post/BlockAttributes/BlockAttributes.php b/src/Component/Post/BlockAttributes/BlockAttributes.php
index 2c760cec..bc9d8a80 100644
--- a/src/Component/Post/BlockAttributes/BlockAttributes.php
+++ b/src/Component/Post/BlockAttributes/BlockAttributes.php
@@ -14,8 +14,8 @@
namespace Beyondwords\Wordpress\Component\Post\BlockAttributes;
use Beyondwords\Wordpress\Component\Post\PostContentUtils;
-use Beyondwords\Wordpress\Component\Post\PostMetaUtils;
use Beyondwords\Wordpress\Component\Settings\Fields\PlayerUI\PlayerUI;
+use Symfony\Component\Uid\Uuid;
/**
* BlockAttributes
@@ -30,12 +30,80 @@ class BlockAttributes
*
* @since 4.0.0
* @since 6.0.0 Make static.
+ * @since 6.0.1 Add REST API hooks for marker initialization.
*/
public static function init()
{
add_filter('register_block_type_args', [self::class, 'registerAudioAttribute']);
add_filter('register_block_type_args', [self::class, 'registerMarkerAttribute']);
add_filter('render_block', [self::class, 'renderBlock'], 10, 2);
+
+ // Register hooks for marker initialization
+ add_filter('wp_insert_post_data', [self::class, 'initializeBlockMarkersBeforeSave'], 10, 2);
+ }
+
+ /**
+ * Initialize block markers before post data is saved to database.
+ *
+ * This function runs right before post data is inserted/updated in the database
+ * and ensures all blocks have unique markers.
+ *
+ * @since 6.0.1
+ *
+ * @param array $data An array of slashed, sanitized, and processed post data.
+ * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data.
+ *
+ * @return array Modified post data.
+ */
+ public static function initializeBlockMarkersBeforeSave($data, $postarr)
+ {
+ // Skip if no content
+ if (empty($data['post_content'])) {
+ return $data;
+ }
+
+ // Skip autosaves and revisions
+ if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
+ return $data;
+ }
+
+ if (wp_is_post_revision($postarr['ID'] ?? 0)) {
+ return $data;
+ }
+
+ // Skip if post type doesn't support custom fields
+ if (! empty($data['post_type']) && ! post_type_supports($data['post_type'], 'custom-fields')) {
+ return $data;
+ }
+
+ // Parse blocks
+ $blocks = parse_blocks($data['post_content']);
+
+ if (empty($blocks)) {
+ return $data;
+ }
+
+ // Track existing markers to detect duplicates
+ $existingMarkers = [];
+ $needsUpdate = false;
+
+ // Process blocks recursively
+ $updatedBlocks = self::processBlocksForMarkers($blocks, $existingMarkers, $needsUpdate);
+
+ // Only update content if changes were made
+ if ($needsUpdate) {
+ // Serialize blocks back to content
+ $data['post_content'] = serialize_blocks($updatedBlocks);
+
+ // Debug logging
+ error_log(sprintf(
+ 'BeyondWords: Generated markers for post %d, found %d existing markers',
+ $postarr['ID'] ?? 0,
+ count($existingMarkers)
+ ));
+ }
+
+ return $data;
}
/**
@@ -75,7 +143,6 @@ public static function registerMarkerAttribute($args)
if (! array_key_exists('beyondwordsMarker', $args['attributes'])) {
$args['attributes']['beyondwordsMarker'] = [
'type' => 'string',
- 'default' => '',
];
}
@@ -86,7 +153,7 @@ public static function registerMarkerAttribute($args)
* Render block as HTML.
*
* Performs some checks and then attempts to add data-beyondwords-marker
- * attribute to the root element of Gutenberg blocks.
+ * attribute to the root element of Gutenberg block.
*
* @since 4.0.0
* @since 4.2.2 Rename method to renderBlock.
@@ -110,11 +177,6 @@ public static function renderBlock($blockContent, $block)
return $blockContent;
}
- // Skip adding marker if no content exists
- if (! PostMetaUtils::hasContent($postId)) {
- return $blockContent;
- }
-
$marker = $block['attrs']['beyondwordsMarker'] ?? '';
return PostContentUtils::addMarkerAttribute(
@@ -122,4 +184,147 @@ public static function renderBlock($blockContent, $block)
$marker
);
}
+
+ /**
+ * Recursively process blocks to initialize and deduplicate markers.
+ *
+ * @since 6.0.1
+ *
+ * @param array $blocks Array of block arrays.
+ * @param array $existingMarkers Reference to array tracking existing markers.
+ * @param bool $needsUpdate Reference to flag indicating if update is needed.
+ *
+ * @return array Updated blocks.
+ */
+ private static function processBlocksForMarkers($blocks, &$existingMarkers, &$needsUpdate)
+ {
+ $updatedBlocks = [];
+
+ foreach ($blocks as $block) {
+ // Skip blocks that shouldn't have markers
+ if (! self::shouldHaveBeyondWordsMarker($block['blockName'])) {
+ $updatedBlocks[] = $block;
+ continue;
+ }
+
+ // Check if block has beyondwordsAudio attribute
+ $hasAudio = $block['attrs']['beyondwordsAudio'] ?? true;
+
+ // Only process blocks with audio enabled
+ if ($hasAudio) {
+ $currentMarker = $block['attrs']['beyondwordsMarker'] ?? '';
+
+ // Check if marker is missing or duplicate
+ if (empty($currentMarker) || in_array($currentMarker, $existingMarkers, true)) {
+ // Generate new unique marker
+ $newMarker = self::generateUniqueUuid($existingMarkers);
+
+ error_log(sprintf(
+ 'BeyondWords: Generating marker for block %s (had: %s, generated: %s)',
+ $block['blockName'] ?? 'unknown',
+ $currentMarker ?: 'none',
+ $newMarker
+ ));
+
+ $block['attrs']['beyondwordsMarker'] = $newMarker;
+ $existingMarkers[] = $newMarker;
+ $needsUpdate = true;
+ } else {
+ // Track existing marker
+ $existingMarkers[] = $currentMarker;
+ }
+ }
+
+ // Process inner blocks recursively
+ if (! empty($block['innerBlocks'])) {
+ $block['innerBlocks'] = self::processBlocksForMarkers(
+ $block['innerBlocks'],
+ $existingMarkers,
+ $needsUpdate
+ );
+ }
+
+ $updatedBlocks[] = $block;
+ }
+
+ return $updatedBlocks;
+ }
+
+ /**
+ * Check if a block should have BeyondWords marker.
+ *
+ * @since 6.0.1
+ *
+ * @param string $blockName Block name.
+ *
+ * @return bool Whether the block should have a marker.
+ */
+ private static function shouldHaveBeyondWordsMarker($blockName)
+ {
+ // Skip blocks without a name
+ if (empty($blockName)) {
+ return false;
+ }
+
+ // Skip internal/UI blocks
+ if (strpos($blockName, '__') === 0) {
+ return false;
+ }
+
+ // Skip reusable blocks and template parts (these are containers)
+ if (
+ strpos($blockName, 'core/block') === 0 ||
+ strpos($blockName, 'core/template') === 0
+ ) {
+ return false;
+ }
+
+ // Skip editor UI blocks
+ $excludedBlocks = [
+ 'core/freeform', // Classic editor
+ 'core/legacy-widget',
+ 'core/widget-area',
+ 'core/navigation',
+ 'core/navigation-link',
+ 'core/navigation-submenu',
+ 'core/site-logo',
+ 'core/site-title',
+ 'core/site-tagline',
+ ];
+
+ if (in_array($blockName, $excludedBlocks, true)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Generate a unique UUID v4 that doesn't exist in the given array.
+ *
+ * @since 6.0.1
+ *
+ * @param array $existingMarkers Array of existing markers to check against.
+ *
+ * @return string UUID v4.
+ */
+ private static function generateUniqueUuid($existingMarkers)
+ {
+ $maxAttempts = 100;
+ $attempts = 0;
+
+ do {
+ $uuid = Uuid::v4()->toRfc4122();
+ $attempts++;
+
+ // Ensure uniqueness
+ if (! in_array($uuid, $existingMarkers, true)) {
+ return $uuid;
+ }
+ } while ($attempts < $maxAttempts);
+
+ // Fallback: append timestamp if somehow we can't generate unique UUID
+ // This should never happen with proper UUIDs but provides safety
+ return Uuid::v4()->toRfc4122() . '-' . time();
+ }
}
diff --git a/src/Component/Post/BlockAttributes/addAttributes.js b/src/Component/Post/BlockAttributes/addAttributes.js
index e79a4960..88ecc9b8 100644
--- a/src/Component/Post/BlockAttributes/addAttributes.js
+++ b/src/Component/Post/BlockAttributes/addAttributes.js
@@ -4,20 +4,68 @@
import { addFilter } from '@wordpress/hooks';
/**
- * External dependencies
+ * Check if a block should have BeyondWords attributes.
+ * Only content blocks that can be read aloud should have these attributes.
+ *
+ * @param {string} name Block name.
+ * @return {boolean} Whether the block should have BeyondWords attributes.
*/
-import getBlockMarkerAttribute from './helpers/getBlockMarkerAttribute';
+function shouldHaveBeyondWordsAttributes( name ) {
+ // Skip blocks without a name
+ if ( ! name ) {
+ return false;
+ }
+
+ // Skip internal/UI blocks
+ if ( name.startsWith( '__' ) ) {
+ return false;
+ }
+
+ // Skip reusable blocks and template parts (these are containers)
+ if (
+ name.startsWith( 'core/block' ) ||
+ name.startsWith( 'core/template' )
+ ) {
+ return false;
+ }
+
+ // Skip editor UI blocks
+ const excludedBlocks = [
+ 'core/freeform', // Classic editor
+ 'core/legacy-widget',
+ 'core/widget-area',
+ 'core/navigation',
+ 'core/navigation-link',
+ 'core/navigation-submenu',
+ 'core/site-logo',
+ 'core/site-title',
+ 'core/site-tagline',
+ ];
+
+ if ( excludedBlocks.includes( name ) ) {
+ return false;
+ }
+
+ return true;
+}
/**
* Register custom block attributes for BeyondWords.
*
* @since 4.0.4 Remove settings.attributes undefined check, to match official docs.
+ * @since 6.0.1 Skip internal/UI blocks to prevent breaking the block inserter.
*
* @param {Object} settings Settings for the block.
+ * @param {string} name Block name.
*
* @return {Object} settings Modified settings.
*/
-function addAttributes( settings ) {
+function addAttributes( settings, name ) {
+ // Only add attributes to content blocks
+ if ( ! shouldHaveBeyondWordsAttributes( name ) ) {
+ return settings;
+ }
+
return {
...settings,
attributes: {
@@ -39,27 +87,3 @@ addFilter(
'beyondwords/beyondwords-block-attributes',
addAttributes
);
-
-/**
- * Set a unique BeyondWords marker for each block that doesn't already have one.
- *
- * @param {Object} attributes Attributes for the block.
- *
- * @return {Object} attributes Modified attributes.
- */
-function setMarkerAttribute( attributes ) {
- const marker = getBlockMarkerAttribute( attributes );
-
- attributes = {
- ...attributes,
- beyondwordsMarker: marker,
- };
-
- return attributes;
-}
-
-addFilter(
- 'blocks.getBlockAttributes',
- 'beyondwords/set-marker-attribute',
- setMarkerAttribute
-);
diff --git a/src/Component/Post/BlockAttributes/addControls.js b/src/Component/Post/BlockAttributes/addControls.js
index c277ffce..65d82c09 100644
--- a/src/Component/Post/BlockAttributes/addControls.js
+++ b/src/Component/Post/BlockAttributes/addControls.js
@@ -12,22 +12,63 @@ import {
ToolbarGroup,
} from '@wordpress/components';
import { createHigherOrderComponent } from '@wordpress/compose';
-import { useEffect } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
/**
* External dependencies
*/
-import getBlockMarkerAttribute from './helpers/getBlockMarkerAttribute';
+import { v4 as uuidv4 } from 'uuid';
/**
- * Internal dependencies
+ * Check if a block should have BeyondWords controls.
+ *
+ * @param {string} name Block name.
+ * @return {boolean} Whether the block should have controls.
*/
-import BlockAttributesCheck from './check';
+function shouldHaveBeyondWordsControls( name ) {
+ // Skip blocks without a name
+ if ( ! name ) {
+ return false;
+ }
+
+ // Skip internal/UI blocks
+ if ( name.startsWith( '__' ) ) {
+ return false;
+ }
+
+ // Skip reusable blocks and template parts (these are containers)
+ if (
+ name.startsWith( 'core/block' ) ||
+ name.startsWith( 'core/template' )
+ ) {
+ return false;
+ }
+
+ // Skip editor UI blocks
+ const excludedBlocks = [
+ 'core/freeform', // Classic editor
+ 'core/legacy-widget',
+ 'core/widget-area',
+ 'core/navigation',
+ 'core/navigation-link',
+ 'core/navigation-submenu',
+ 'core/site-logo',
+ 'core/site-title',
+ 'core/site-tagline',
+ ];
+
+ if ( excludedBlocks.includes( name ) ) {
+ return false;
+ }
+
+ return true;
+}
/**
* Add BeyondWords controls to Gutenberg Blocks.
*
+ * @since 6.0.1 Skip internal/UI blocks to prevent breaking the block inserter.
+ *
* @param {Function} BlockEdit Block edit component.
*
* @return {Function} BlockEdit Modified block edit component.
@@ -35,14 +76,15 @@ import BlockAttributesCheck from './check';
const withBeyondwordsBlockControls = createHigherOrderComponent(
( BlockEdit ) => {
return ( props ) => {
- const { attributes, setAttributes } = props;
+ const { name } = props;
- useEffect( () => {
- setAttributes( {
- beyondwordsMarker: getBlockMarkerAttribute( attributes ),
- } );
- }, [] );
+ // Skip blocks that shouldn't have controls
+ // Do this check BEFORE accessing attributes to avoid unnecessary processing
+ if ( ! shouldHaveBeyondWordsControls( name ) ) {
+ return ;
+ }
+ const { attributes, setAttributes } = props;
const { beyondwordsAudio, beyondwordsMarker } = attributes;
const icon = !! beyondwordsAudio
@@ -57,30 +99,39 @@ const withBeyondwordsBlockControls = createHigherOrderComponent(
? __( 'Audio processing enabled', 'speechkit' )
: __( 'Audio processing disabled', 'speechkit' );
- const toggleBeyondwordsAudio = () =>
- setAttributes( { beyondwordsAudio: ! beyondwordsAudio } );
+ const toggleBeyondwordsAudio = () => {
+ const newAudioValue = ! beyondwordsAudio;
+ const updates = { beyondwordsAudio: newAudioValue };
+
+ // Only set marker when enabling audio and marker doesn't exist
+ if ( newAudioValue && ! beyondwordsMarker ) {
+ updates.beyondwordsMarker = uuidv4();
+ }
+
+ setAttributes( updates );
+ };
return (
<>
-
-
-
+
+
+
+
+
+ { !! beyondwordsAudio && (
-
-
- { !! beyondwordsAudio && (
-
+ { beyondwordsMarker ? (
-
- ) }
-
-
-
-
-
-
-
-
-
+ ) : (
+
+
+ { __(
+ 'Segment marker',
+ 'speechkit'
+ ) }
+
+
+ { __(
+ 'Generated on save',
+ 'speechkit'
+ ) }
+
+
+ ) }
+
+ ) }
+
+
+
+
+
+
+
+
>
);
};
diff --git a/src/Component/Post/BlockAttributes/helpers/getBlockMarkerAttribute.js b/src/Component/Post/BlockAttributes/helpers/getBlockMarkerAttribute.js
deleted file mode 100644
index 85195750..00000000
--- a/src/Component/Post/BlockAttributes/helpers/getBlockMarkerAttribute.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/**
- * WordPress Dependencies
- */
-import { select } from '@wordpress/data';
-
-/**
- * External dependencies
- */
-import { v4 as uuidv4 } from 'uuid';
-
-/**
- * Get a beyondwordsMarker attribute for a block.
- *
- * Using the "Duplicate" button in the Block toolbar duplicates the marker
- * attribute too, so we attempt to handle this by getting all the markers in the
- * current Post and assinging new UUIDs to markers that already exist.
- *
- * @since 4.0.0
- *
- * @param {Object} attributes Attributes for the block.
- *
- * @return {string} marker The block marker (segment marker in BeyondWords API).
- */
-const getBlockMarkerAttribute = ( attributes ) => {
- const { beyondwordsMarker } = attributes;
-
- if ( ! beyondwordsMarker ) {
- return uuidv4();
- }
-
- const existingMarkers = getExistingBlockMarkers();
-
- if ( countInArray( existingMarkers, beyondwordsMarker ) > 1 ) {
- // Return a new UUID if this marker is a duplicate
- return uuidv4();
- }
-
- // Return the existing marker only if it is not a duplicate
- return beyondwordsMarker;
-};
-
-/**
- * Get all existing Block markers for the currently-edited post.
- *
- * If using `getBlocks()` proves to be too respource-intensive then further work
- * will be required to optimise this.
- *
- * @since 4.0.0
- *
- * @return {string[]} markers The block markers for the current Post.
- */
-const getExistingBlockMarkers = () => {
- // Get all Blocks in current Post
- const blocks = select( 'core/block-editor' ).getBlocks();
-
- // Return all non-empty markers of the Blocks
- return blocks
- .map( ( block ) => block?.attributes?.beyondwordsMarker )
- .filter( ( marker ) => marker );
-};
-
-/**
- * Count the number of times an item is in an array.
- *
- * @param array
- * @param item
- * @since 4.0.0
- * @since 4.4.0 Ensure param is array
- *
- * @return {number} count The number of times the item occurs.
- */
-function countInArray( array, item ) {
- if ( ! Array.isArray( array ) ) {
- return 0;
- }
-
- let count = 0;
-
- for ( let i = 0; i < array.length; i++ ) {
- if ( array[ i ] === item ) {
- count++;
- }
- }
-
- return count;
-}
-
-export default getBlockMarkerAttribute;
diff --git a/src/Component/Post/BlockAttributes/index.js b/src/Component/Post/BlockAttributes/index.js
index 7aa1d532..4d2f6b6b 100644
--- a/src/Component/Post/BlockAttributes/index.js
+++ b/src/Component/Post/BlockAttributes/index.js
@@ -1,2 +1,3 @@
require( './addAttributes' );
require( './addControls' );
+require( './refreshAfterSave' );
diff --git a/src/Component/Post/BlockAttributes/refreshAfterSave.js b/src/Component/Post/BlockAttributes/refreshAfterSave.js
new file mode 100644
index 00000000..3ff9b03f
--- /dev/null
+++ b/src/Component/Post/BlockAttributes/refreshAfterSave.js
@@ -0,0 +1,165 @@
+/**
+ * WordPress dependencies
+ */
+import { subscribe, select, dispatch } from '@wordpress/data';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Refresh block attributes after save to get server-generated markers.
+ *
+ * When a post is saved, the server generates markers for blocks that don't have them.
+ * This code ensures those markers appear in the editor without requiring a page refresh.
+ *
+ * @since 6.0.1
+ */
+let isSaving = false;
+let isRefreshing = false;
+
+subscribe( () => {
+ const editor = select( 'core/editor' );
+
+ if ( ! editor || isRefreshing ) {
+ return;
+ }
+
+ const isAutosaving = editor.isAutosavingPost();
+
+ // Skip autosaves
+ if ( isAutosaving ) {
+ return;
+ }
+
+ const currentlySaving = editor.isSavingPost();
+
+ // Detect when save finishes
+ if ( isSaving && ! currentlySaving && ! editor.isEditedPostDirty() ) {
+ // Save just finished and post is clean (saved successfully)
+ const postId = editor.getCurrentPostId();
+ const postType = editor.getCurrentPostType();
+
+ if ( postId && postType ) {
+ // Refresh the post from the server to get updated markers
+ refreshPostFromServer( postId, postType );
+ }
+ }
+
+ isSaving = currentlySaving;
+} );
+
+/**
+ * Refresh post data from server after save.
+ *
+ * @param {number} postId The post ID.
+ * @param {string} postType The post type.
+ */
+async function refreshPostFromServer( postId, postType ) {
+ isRefreshing = true;
+
+ try {
+ // Get the REST base for this post type
+ const postTypeObject = select( 'core' ).getPostType( postType );
+ const restBase = postTypeObject?.rest_base || postType;
+
+ // Fetch the updated post from the server
+ const updatedPost = await apiFetch( {
+ path: `/wp/v2/${ restBase }/${ postId }?context=edit`,
+ } );
+
+ if ( updatedPost && updatedPost.content && updatedPost.content.raw ) {
+ const { updateBlockAttributes, resetBlocks } =
+ dispatch( 'core/block-editor' );
+ const blockEditor = select( 'core/block-editor' );
+
+ // Parse the server blocks to get updated markers
+ const serverBlocks = wp.blocks.parse( updatedPost.content.raw );
+ const editorBlocks = blockEditor.getBlocks();
+
+ // Update only the marker attributes
+ const count = updateBlockMarkers(
+ serverBlocks,
+ editorBlocks,
+ updateBlockAttributes
+ );
+
+ if ( count > 0 ) {
+ // Force re-serialization by resetting blocks
+ // This ensures updated markers are written to block comments
+ const updatedBlocks = blockEditor.getBlocks();
+ resetBlocks( updatedBlocks );
+
+ // eslint-disable-next-line no-console
+ console.log(
+ `BeyondWords: Updated ${ count } block markers and re-serialized`
+ );
+ }
+ }
+ } catch ( error ) {
+ // Log error for debugging
+ console.error( 'BeyondWords: Failed to refresh block markers:', error );
+ } finally {
+ isRefreshing = false;
+ }
+}
+
+/**
+ * Recursively update block markers from server response.
+ *
+ * @param {Array} serverBlocks Blocks from server with updated markers.
+ * @param {Array} editorBlocks Blocks currently in the editor.
+ * @param {Function} updateBlockAttributes Function to update block attributes.
+ *
+ * @return {number} Count of blocks that were updated.
+ */
+function updateBlockMarkers(
+ serverBlocks,
+ editorBlocks,
+ updateBlockAttributes
+) {
+ if (
+ ! serverBlocks ||
+ ! editorBlocks ||
+ serverBlocks.length !== editorBlocks.length
+ ) {
+ return 0;
+ }
+
+ let updatedCount = 0;
+
+ for ( let i = 0; i < serverBlocks.length; i++ ) {
+ const serverBlock = serverBlocks[ i ];
+ const editorBlock = editorBlocks[ i ];
+
+ // Skip if blocks don't match
+ if (
+ ! serverBlock ||
+ ! editorBlock ||
+ serverBlock.name !== editorBlock.name
+ ) {
+ continue;
+ }
+
+ // Check if server block has a marker that editor block doesn't
+ const serverMarker = serverBlock.attributes?.beyondwordsMarker;
+ const editorMarker = editorBlock.attributes?.beyondwordsMarker;
+
+ if ( serverMarker && serverMarker !== editorMarker ) {
+ // Update the block attributes with server-generated marker
+ updateBlockAttributes( editorBlock.clientId, {
+ beyondwordsMarker: serverMarker,
+ } );
+
+ updatedCount++;
+ }
+
+ // Recursively process inner blocks
+ if ( serverBlock.innerBlocks && editorBlock.innerBlocks ) {
+ updatedCount += updateBlockMarkers(
+ serverBlock.innerBlocks,
+ editorBlock.innerBlocks,
+ updateBlockAttributes
+ );
+ }
+ }
+
+ return updatedCount;
+}
diff --git a/tests/cypress/e2e/block-editor/block-inserter.cy.js b/tests/cypress/e2e/block-editor/block-inserter.cy.js
new file mode 100644
index 00000000..c04a0c73
--- /dev/null
+++ b/tests/cypress/e2e/block-editor/block-inserter.cy.js
@@ -0,0 +1,161 @@
+/* global cy, beforeEach, context, it */
+
+context( 'Block Editor: Block Inserter', () => {
+ beforeEach( () => {
+ cy.login();
+ } );
+
+ const postTypes = require( '../../../../tests/fixtures/post-types.json' );
+
+ postTypes
+ .filter( ( x ) => x.supported )
+ .forEach( ( postType ) => {
+ it( `block inserter button appears correctly for ${ postType.name }`, () => {
+ cy.visitPostEditor( postType.slug );
+
+ // Add a title to make the editor active
+ cy.get( '.editor-post-title__input' ).type(
+ 'Test block inserter'
+ );
+
+ // Click after the title to focus the editor body
+ cy.get( '.editor-post-title__input' ).type( '{enter}' );
+
+ // Wait for the editor to be ready
+ cy.wait( 500 );
+
+ // The block inserter button ([+]) should be visible
+ // This is the button that appears when you're in an empty block
+ cy.get( 'button[aria-label="Add block"]' ).should(
+ 'be.visible'
+ );
+
+ // Click the inserter button to open the block picker
+ cy.get( 'button[aria-label="Add block"]' ).first().click();
+
+ // The block picker popover should appear
+ cy.get( '.block-editor-inserter__menu' ).should(
+ 'be.visible'
+ );
+
+ // Should show "Browse all" option
+ cy.contains( 'Browse all' ).should( 'be.visible' );
+
+ // Close the inserter
+ cy.get( 'body' ).type( '{esc}' );
+ } );
+
+ it( `can insert multiple blocks sequentially for ${ postType.name }`, () => {
+ cy.visitPostEditor( postType.slug );
+
+ // Add a title
+ cy.get( '.editor-post-title__input' ).type(
+ 'Test multiple blocks'
+ );
+ cy.get( '.editor-post-title__input' ).type( '{enter}' );
+
+ // Wait for the editor to be ready
+ cy.wait( 500 );
+
+ // Type some text in the first block
+ cy.get( '.block-editor-block-list__layout' )
+ .first()
+ .type( 'First paragraph' );
+
+ // Press enter to create a new block
+ cy.get( '.block-editor-block-list__layout' )
+ .first()
+ .type( '{enter}' );
+
+ // The inserter button should still appear for the new block
+ cy.get( 'button[aria-label="Add block"]' ).should(
+ 'be.visible'
+ );
+
+ // Type in the second block
+ cy.get( '.block-editor-block-list__layout' )
+ .first()
+ .type( 'Second paragraph{enter}' );
+
+ // Type in the third block
+ cy.get( '.block-editor-block-list__layout' )
+ .first()
+ .type( 'Third paragraph' );
+
+ // Verify we have 3 paragraph blocks
+ cy.get( '.wp-block-paragraph' ).should( 'have.length', 3 );
+
+ // Verify the content
+ cy.contains( '.wp-block-paragraph', 'First paragraph' );
+ cy.contains( '.wp-block-paragraph', 'Second paragraph' );
+ cy.contains( '.wp-block-paragraph', 'Third paragraph' );
+ } );
+
+ it( `duplicated blocks get unique markers for ${ postType.name }`, () => {
+ cy.visitPostEditor( postType.slug );
+
+ // Add a title
+ cy.get( '.editor-post-title__input' ).type(
+ 'Test duplicate markers'
+ );
+ cy.get( '.editor-post-title__input' ).type( '{enter}' );
+
+ // Wait for the editor to be ready
+ cy.wait( 500 );
+
+ // Type some text in the first block
+ cy.get( '.block-editor-block-list__layout' )
+ .first()
+ .type( 'Original paragraph' );
+
+ // Wait for the block to be created
+ cy.wait( 500 );
+
+ // Select the block by clicking on it
+ cy.contains( '.wp-block-paragraph', 'Original paragraph' ).click();
+
+ // Open the block options menu (three dots)
+ cy.get( '.block-editor-block-toolbar' )
+ .find( 'button[aria-label="Options"]' )
+ .click();
+
+ // Click "Duplicate" in the dropdown menu
+ cy.contains( 'button', 'Duplicate' ).click();
+
+ // Wait for duplication to complete
+ cy.wait( 500 );
+
+ // Verify we have 2 paragraph blocks with the same content
+ cy.get( '.wp-block-paragraph' ).should( 'have.length', 2 );
+
+ // Get the beyondwordsMarker attributes for both blocks
+ cy.window().then( ( win ) => {
+ const blocks = win.wp.data
+ .select( 'core/block-editor' )
+ .getBlocks();
+
+ // Find the two paragraph blocks
+ const paragraphBlocks = blocks.filter(
+ ( block ) => block.name === 'core/paragraph'
+ );
+
+ expect( paragraphBlocks ).to.have.length( 2 );
+
+ // Extract markers
+ const marker1 =
+ paragraphBlocks[ 0 ].attributes.beyondwordsMarker;
+ const marker2 =
+ paragraphBlocks[ 1 ].attributes.beyondwordsMarker;
+
+ // Both should have markers
+ expect( marker1 ).to.be.a( 'string' );
+ expect( marker2 ).to.be.a( 'string' );
+ expect( marker1 ).to.have.length.greaterThan( 0 );
+ expect( marker2 ).to.have.length.greaterThan( 0 );
+
+ // Markers should be different (not duplicates)
+ expect( marker1 ).to.not.equal( marker2 );
+ } );
+ } );
+ } );
+} );
From 3a223c0301b79d689678cef54e4b0e06e2f748b8 Mon Sep 17 00:00:00 2001
From: Stuart McAlpine
Date: Mon, 10 Nov 2025 11:51:52 +0000
Subject: [PATCH 2/7] Stop setting segment markers
---
.../Post/BlockAttributes/BlockAttributes.php | 156 ++++++++--------
.../Post/BlockAttributes/addControls.js | 83 ++-------
.../Post/BlockAttributes/refreshAfterSave.js | 165 ----------------
src/Component/Post/PostContentUtils.php | 176 ++++++------------
.../e2e/block-editor/block-inserter.cy.js | 2 +-
.../e2e/block-editor/segment-markers.cy.js | 95 +++++++++-
.../BlockAttributes/BlockAttributesTest.php | 116 ------------
tests/phpunit/Core/PostContentUtilsTest.php | 170 ++++++++---------
8 files changed, 335 insertions(+), 628 deletions(-)
delete mode 100644 src/Component/Post/BlockAttributes/refreshAfterSave.js
diff --git a/src/Component/Post/BlockAttributes/BlockAttributes.php b/src/Component/Post/BlockAttributes/BlockAttributes.php
index bc9d8a80..02275668 100644
--- a/src/Component/Post/BlockAttributes/BlockAttributes.php
+++ b/src/Component/Post/BlockAttributes/BlockAttributes.php
@@ -36,10 +36,10 @@ public static function init()
{
add_filter('register_block_type_args', [self::class, 'registerAudioAttribute']);
add_filter('register_block_type_args', [self::class, 'registerMarkerAttribute']);
- add_filter('render_block', [self::class, 'renderBlock'], 10, 2);
+ // add_filter('render_block', [self::class, 'renderBlock'], 10, 2);
// Register hooks for marker initialization
- add_filter('wp_insert_post_data', [self::class, 'initializeBlockMarkersBeforeSave'], 10, 2);
+ // add_filter('wp_insert_post_data', [self::class, 'initializeBlockMarkersBeforeSave'], 10, 2);
}
/**
@@ -95,12 +95,12 @@ public static function initializeBlockMarkersBeforeSave($data, $postarr)
// Serialize blocks back to content
$data['post_content'] = serialize_blocks($updatedBlocks);
- // Debug logging
- error_log(sprintf(
- 'BeyondWords: Generated markers for post %d, found %d existing markers',
- $postarr['ID'] ?? 0,
- count($existingMarkers)
- ));
+ // Debug: Log marker summary
+ // error_log(sprintf(
+ // 'BeyondWords: Generated markers for post %d, found %d existing markers',
+ // $postarr['ID'] ?? 0,
+ // count($existingMarkers)
+ // ));
}
return $data;
@@ -143,6 +143,7 @@ public static function registerMarkerAttribute($args)
if (! array_key_exists('beyondwordsMarker', $args['attributes'])) {
$args['attributes']['beyondwordsMarker'] = [
'type' => 'string',
+ 'default' => '',
];
}
@@ -164,26 +165,26 @@ public static function registerMarkerAttribute($args)
*
* @return string Block Content (HTML).
*/
- public static function renderBlock($blockContent, $block)
- {
- // Skip adding marker if player UI is disabled
- if (get_option(PlayerUI::OPTION_NAME) === PlayerUI::DISABLED) {
- return $blockContent;
- }
+ // public static function renderBlock($blockContent, $block)
+ // {
+ // // Skip adding marker if player UI is disabled
+ // if (get_option(PlayerUI::OPTION_NAME) === PlayerUI::DISABLED) {
+ // return $blockContent;
+ // }
- $postId = get_the_ID();
+ // $postId = get_the_ID();
- if (! $postId) {
- return $blockContent;
- }
+ // if (! $postId) {
+ // return $blockContent;
+ // }
- $marker = $block['attrs']['beyondwordsMarker'] ?? '';
+ // $marker = $block['attrs']['beyondwordsMarker'] ?? '';
- return PostContentUtils::addMarkerAttribute(
- $blockContent,
- $marker
- );
- }
+ // return PostContentUtils::addMarkerAttribute(
+ // $blockContent,
+ // $marker
+ // );
+ // }
/**
* Recursively process blocks to initialize and deduplicate markers.
@@ -196,59 +197,60 @@ public static function renderBlock($blockContent, $block)
*
* @return array Updated blocks.
*/
- private static function processBlocksForMarkers($blocks, &$existingMarkers, &$needsUpdate)
- {
- $updatedBlocks = [];
-
- foreach ($blocks as $block) {
- // Skip blocks that shouldn't have markers
- if (! self::shouldHaveBeyondWordsMarker($block['blockName'])) {
- $updatedBlocks[] = $block;
- continue;
- }
-
- // Check if block has beyondwordsAudio attribute
- $hasAudio = $block['attrs']['beyondwordsAudio'] ?? true;
-
- // Only process blocks with audio enabled
- if ($hasAudio) {
- $currentMarker = $block['attrs']['beyondwordsMarker'] ?? '';
-
- // Check if marker is missing or duplicate
- if (empty($currentMarker) || in_array($currentMarker, $existingMarkers, true)) {
- // Generate new unique marker
- $newMarker = self::generateUniqueUuid($existingMarkers);
-
- error_log(sprintf(
- 'BeyondWords: Generating marker for block %s (had: %s, generated: %s)',
- $block['blockName'] ?? 'unknown',
- $currentMarker ?: 'none',
- $newMarker
- ));
-
- $block['attrs']['beyondwordsMarker'] = $newMarker;
- $existingMarkers[] = $newMarker;
- $needsUpdate = true;
- } else {
- // Track existing marker
- $existingMarkers[] = $currentMarker;
- }
- }
-
- // Process inner blocks recursively
- if (! empty($block['innerBlocks'])) {
- $block['innerBlocks'] = self::processBlocksForMarkers(
- $block['innerBlocks'],
- $existingMarkers,
- $needsUpdate
- );
- }
-
- $updatedBlocks[] = $block;
- }
-
- return $updatedBlocks;
- }
+ // private static function processBlocksForMarkers($blocks, &$existingMarkers, &$needsUpdate)
+ // {
+ // $updatedBlocks = [];
+
+ // foreach ($blocks as $block) {
+ // // Skip blocks that shouldn't have markers
+ // if (! self::shouldHaveBeyondWordsMarker($block['blockName'])) {
+ // $updatedBlocks[] = $block;
+ // continue;
+ // }
+
+ // // Check if block has beyondwordsAudio attribute
+ // $hasAudio = $block['attrs']['beyondwordsAudio'] ?? true;
+
+ // // Only process blocks with audio enabled
+ // if ($hasAudio) {
+ // $currentMarker = $block['attrs']['beyondwordsMarker'] ?? '';
+
+ // // Check if marker is missing or duplicate
+ // if (empty($currentMarker) || in_array($currentMarker, $existingMarkers, true)) {
+ // // Generate new unique marker
+ // $newMarker = self::generateUniqueUuid($existingMarkers);
+
+ // // Debug: Log marker generation
+ // // error_log(sprintf(
+ // // 'BeyondWords: Generating marker for block %s (had: %s, generated: %s)',
+ // // $block['blockName'] ?? 'unknown',
+ // // $currentMarker ?: 'none',
+ // // $newMarker
+ // // ));
+
+ // $block['attrs']['beyondwordsMarker'] = $newMarker;
+ // $existingMarkers[] = $newMarker;
+ // $needsUpdate = true;
+ // } else {
+ // // Track existing marker
+ // $existingMarkers[] = $currentMarker;
+ // }
+ // }
+
+ // // Process inner blocks recursively
+ // if (! empty($block['innerBlocks'])) {
+ // $block['innerBlocks'] = self::processBlocksForMarkers(
+ // $block['innerBlocks'],
+ // $existingMarkers,
+ // $needsUpdate
+ // );
+ // }
+
+ // $updatedBlocks[] = $block;
+ // }
+
+ // return $updatedBlocks;
+ // }
/**
* Check if a block should have BeyondWords marker.
diff --git a/src/Component/Post/BlockAttributes/addControls.js b/src/Component/Post/BlockAttributes/addControls.js
index 65d82c09..15af8aa7 100644
--- a/src/Component/Post/BlockAttributes/addControls.js
+++ b/src/Component/Post/BlockAttributes/addControls.js
@@ -14,11 +14,6 @@ import {
import { createHigherOrderComponent } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
-/**
- * External dependencies
- */
-import { v4 as uuidv4 } from 'uuid';
-
/**
* Check if a block should have BeyondWords controls.
*
@@ -85,7 +80,8 @@ const withBeyondwordsBlockControls = createHigherOrderComponent(
}
const { attributes, setAttributes } = props;
- const { beyondwordsAudio, beyondwordsMarker } = attributes;
+ // const { beyondwordsAudio, beyondwordsMarker } = attributes;
+ const { beyondwordsAudio } = attributes;
const icon = !! beyondwordsAudio
? 'controls-volumeon'
@@ -100,15 +96,7 @@ const withBeyondwordsBlockControls = createHigherOrderComponent(
: __( 'Audio processing disabled', 'speechkit' );
const toggleBeyondwordsAudio = () => {
- const newAudioValue = ! beyondwordsAudio;
- const updates = { beyondwordsAudio: newAudioValue };
-
- // Only set marker when enabling audio and marker doesn't exist
- if ( newAudioValue && ! beyondwordsMarker ) {
- updates.beyondwordsMarker = uuidv4();
- }
-
- setAttributes( updates );
+ setAttributes( { beyondwordsAudio: ! beyondwordsAudio } );
};
return (
@@ -129,57 +117,24 @@ const withBeyondwordsBlockControls = createHigherOrderComponent(
__nextHasNoMarginBottom
/>
- { !! beyondwordsAudio && (
+ { /* { !! beyondwordsAudio && (
- { beyondwordsMarker ? (
-
- ) : (
-
-
- { __(
- 'Segment marker',
- 'speechkit'
- ) }
-
-
- { __(
- 'Generated on save',
- 'speechkit'
- ) }
-
-
- ) }
+
- ) }
+ ) } */ }
diff --git a/src/Component/Post/BlockAttributes/refreshAfterSave.js b/src/Component/Post/BlockAttributes/refreshAfterSave.js
deleted file mode 100644
index 3ff9b03f..00000000
--- a/src/Component/Post/BlockAttributes/refreshAfterSave.js
+++ /dev/null
@@ -1,165 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { subscribe, select, dispatch } from '@wordpress/data';
-import apiFetch from '@wordpress/api-fetch';
-
-/**
- * Refresh block attributes after save to get server-generated markers.
- *
- * When a post is saved, the server generates markers for blocks that don't have them.
- * This code ensures those markers appear in the editor without requiring a page refresh.
- *
- * @since 6.0.1
- */
-let isSaving = false;
-let isRefreshing = false;
-
-subscribe( () => {
- const editor = select( 'core/editor' );
-
- if ( ! editor || isRefreshing ) {
- return;
- }
-
- const isAutosaving = editor.isAutosavingPost();
-
- // Skip autosaves
- if ( isAutosaving ) {
- return;
- }
-
- const currentlySaving = editor.isSavingPost();
-
- // Detect when save finishes
- if ( isSaving && ! currentlySaving && ! editor.isEditedPostDirty() ) {
- // Save just finished and post is clean (saved successfully)
- const postId = editor.getCurrentPostId();
- const postType = editor.getCurrentPostType();
-
- if ( postId && postType ) {
- // Refresh the post from the server to get updated markers
- refreshPostFromServer( postId, postType );
- }
- }
-
- isSaving = currentlySaving;
-} );
-
-/**
- * Refresh post data from server after save.
- *
- * @param {number} postId The post ID.
- * @param {string} postType The post type.
- */
-async function refreshPostFromServer( postId, postType ) {
- isRefreshing = true;
-
- try {
- // Get the REST base for this post type
- const postTypeObject = select( 'core' ).getPostType( postType );
- const restBase = postTypeObject?.rest_base || postType;
-
- // Fetch the updated post from the server
- const updatedPost = await apiFetch( {
- path: `/wp/v2/${ restBase }/${ postId }?context=edit`,
- } );
-
- if ( updatedPost && updatedPost.content && updatedPost.content.raw ) {
- const { updateBlockAttributes, resetBlocks } =
- dispatch( 'core/block-editor' );
- const blockEditor = select( 'core/block-editor' );
-
- // Parse the server blocks to get updated markers
- const serverBlocks = wp.blocks.parse( updatedPost.content.raw );
- const editorBlocks = blockEditor.getBlocks();
-
- // Update only the marker attributes
- const count = updateBlockMarkers(
- serverBlocks,
- editorBlocks,
- updateBlockAttributes
- );
-
- if ( count > 0 ) {
- // Force re-serialization by resetting blocks
- // This ensures updated markers are written to block comments
- const updatedBlocks = blockEditor.getBlocks();
- resetBlocks( updatedBlocks );
-
- // eslint-disable-next-line no-console
- console.log(
- `BeyondWords: Updated ${ count } block markers and re-serialized`
- );
- }
- }
- } catch ( error ) {
- // Log error for debugging
- console.error( 'BeyondWords: Failed to refresh block markers:', error );
- } finally {
- isRefreshing = false;
- }
-}
-
-/**
- * Recursively update block markers from server response.
- *
- * @param {Array} serverBlocks Blocks from server with updated markers.
- * @param {Array} editorBlocks Blocks currently in the editor.
- * @param {Function} updateBlockAttributes Function to update block attributes.
- *
- * @return {number} Count of blocks that were updated.
- */
-function updateBlockMarkers(
- serverBlocks,
- editorBlocks,
- updateBlockAttributes
-) {
- if (
- ! serverBlocks ||
- ! editorBlocks ||
- serverBlocks.length !== editorBlocks.length
- ) {
- return 0;
- }
-
- let updatedCount = 0;
-
- for ( let i = 0; i < serverBlocks.length; i++ ) {
- const serverBlock = serverBlocks[ i ];
- const editorBlock = editorBlocks[ i ];
-
- // Skip if blocks don't match
- if (
- ! serverBlock ||
- ! editorBlock ||
- serverBlock.name !== editorBlock.name
- ) {
- continue;
- }
-
- // Check if server block has a marker that editor block doesn't
- const serverMarker = serverBlock.attributes?.beyondwordsMarker;
- const editorMarker = editorBlock.attributes?.beyondwordsMarker;
-
- if ( serverMarker && serverMarker !== editorMarker ) {
- // Update the block attributes with server-generated marker
- updateBlockAttributes( editorBlock.clientId, {
- beyondwordsMarker: serverMarker,
- } );
-
- updatedCount++;
- }
-
- // Recursively process inner blocks
- if ( serverBlock.innerBlocks && editorBlock.innerBlocks ) {
- updatedCount += updateBlockMarkers(
- serverBlock.innerBlocks,
- editorBlock.innerBlocks,
- updateBlockAttributes
- );
- }
- }
-
- return updatedCount;
-}
diff --git a/src/Component/Post/PostContentUtils.php b/src/Component/Post/PostContentUtils.php
index 26074503..cf1bc1c5 100755
--- a/src/Component/Post/PostContentUtils.php
+++ b/src/Component/Post/PostContentUtils.php
@@ -154,59 +154,6 @@ public static function getPostSummary(int|\WP_Post $post): string|null
return $summary;
}
- /**
- * Get the segments for the audio content, ready to be sent to the BeyondWords API.
- *
- * @codeCoverageIgnore
- * THIS METHOD IS CURRENTLY NOT IN USE. Segments cannot currently include HTML
- * formatting tags such as and so we do not pass segments, we pass
- * a HTML string as the body param instead.
- *
- * @param int|\WP_Post $post The WordPress post ID, or post object.
- *
- * @since 4.0.0
- */
- public static function getSegments(int|\WP_Post $post): array
- {
- if (! has_blocks($post)) {
- return [];
- }
-
- $titleSegment = (object) [
- 'section' => 'title',
- 'text' => get_the_title($post),
- ];
-
- $summarySegment = (object) [
- 'section' => 'summary',
- 'text' => PostContentUtils::getPostSummary($post),
- ];
-
- $blocks = PostContentUtils::getAudioEnabledBlocks($post);
-
- $bodySegments = array_map(function ($block) {
- $marker = null;
-
- if (isset($block['attrs']) && isset($block['attrs']['beyondwordsMarker'])) {
- $marker = $block['attrs']['beyondwordsMarker'];
- }
-
- return (object) [
- 'section' => 'body',
- 'marker' => $marker,
- 'text' => trim(render_block($block)),
- ];
- }, $blocks);
-
- // Merge title, summary and body segments
- $segments = array_values(array_merge([$titleSegment], [$summarySegment], $bodySegments));
-
- // Remove any segments with empty text
- $segments = array_values(array_filter($segments, fn($segment) => ! empty($segment::text)));
-
- return $segments;
- }
-
/**
* Get the post content without blocks which have been filtered.
*
@@ -234,12 +181,13 @@ public static function getContentWithoutExcludedBlocks(int|\WP_Post $post): stri
$blocks = PostContentUtils::getAudioEnabledBlocks($post);
foreach ($blocks as $block) {
- $marker = $block['attrs']['beyondwordsMarker'] ?? '';
+ // $marker = $block['attrs']['beyondwordsMarker'] ?? '';
- $output .= PostContentUtils::addMarkerAttribute(
- render_block($block),
- $marker
- );
+ // $output .= PostContentUtils::addMarkerAttribute(
+ // render_block($block),
+ // $marker
+ // );
+ $output .= render_block($block);
}
return $output;
@@ -467,19 +415,19 @@ public static function getAuthorName(int $postId): string
*
* @return string HTML.
*/
- public static function addMarkerAttribute(string $html, string $marker): string
- {
- if (! $marker) {
- return $html;
- }
-
- // Prefer WP_HTML_Tag_Processor, introduced in WordPress 6.2
- if (class_exists('WP_HTML_Tag_Processor')) {
- return PostContentUtils::addMarkerAttributeWithHTMLTagProcessor($html, $marker);
- } else {
- return PostContentUtils::addMarkerAttributeWithDOMDocument($html, $marker);
- }
- }
+ // public static function addMarkerAttribute(string $html, string $marker): string
+ // {
+ // if (! $marker) {
+ // return $html;
+ // }
+
+ // // Prefer WP_HTML_Tag_Processor, introduced in WordPress 6.2
+ // if (class_exists('WP_HTML_Tag_Processor')) {
+ // return PostContentUtils::addMarkerAttributeWithHTMLTagProcessor($html, $marker);
+ // } else {
+ // return PostContentUtils::addMarkerAttributeWithDOMDocument($html, $marker);
+ // }
+ // }
/**
* Add data-beyondwords-marker attribute to the root elements in a HTML
@@ -495,21 +443,21 @@ public static function addMarkerAttribute(string $html, string $marker): string
*
* @return string HTML.
*/
- public static function addMarkerAttributeWithHTMLTagProcessor(string $html, string $marker): string
- {
- if (! $marker) {
- return $html;
- }
+ // public static function addMarkerAttributeWithHTMLTagProcessor(string $html, string $marker): string
+ // {
+ // if (! $marker) {
+ // return $html;
+ // }
- // https://github.com/WordPress/gutenberg/pull/42485
- $tags = new \WP_HTML_Tag_Processor($html);
+ // // https://github.com/WordPress/gutenberg/pull/42485
+ // $tags = new \WP_HTML_Tag_Processor($html);
- if ($tags->next_tag()) {
- $tags->set_attribute('data-beyondwords-marker', $marker);
- }
+ // if ($tags->next_tag()) {
+ // $tags->set_attribute('data-beyondwords-marker', $marker);
+ // }
- return strval($tags);
- }
+ // return strval($tags);
+ // }
/**
* Add data-beyondwords-marker attribute to the root elements in a HTML
@@ -537,46 +485,46 @@ public static function addMarkerAttributeWithHTMLTagProcessor(string $html, stri
*
* @return string HTML.
*/
- public static function addMarkerAttributeWithDOMDocument(string $html, string $marker): string
- {
- if (! $marker) {
- return $html;
- }
+ // public static function addMarkerAttributeWithDOMDocument(string $html, string $marker): string
+ // {
+ // if (! $marker) {
+ // return $html;
+ // }
- $dom = new \DOMDocument('1.0', 'utf-8');
+ // $dom = new \DOMDocument('1.0', 'utf-8');
- $wrappedHtml =
- ''
- . $html
- . '';
+ // $wrappedHtml =
+ // ''
+ // . $html
+ // . '';
- $success = $dom->loadHTML($wrappedHtml, LIBXML_HTML_NODEFDTD | LIBXML_COMPACT);
+ // $success = $dom->loadHTML($wrappedHtml, LIBXML_HTML_NODEFDTD | LIBXML_COMPACT);
- if (! $success) {
- return $html;
- }
+ // if (! $success) {
+ // return $html;
+ // }
- // Structure is like ``, so body is the `lastChild` of our document.
- $bodyElement = $dom->documentElement->lastChild;
+ // // Structure is like ``, so body is the `lastChild` of our document.
+ // $bodyElement = $dom->documentElement->lastChild;
- $xpath = new \DOMXPath($dom);
- $blockRoot = $xpath->query('./*', $bodyElement)[0];
+ // $xpath = new \DOMXPath($dom);
+ // $blockRoot = $xpath->query('./*', $bodyElement)[0];
- if (empty($blockRoot)) {
- return $html;
- }
+ // if (empty($blockRoot)) {
+ // return $html;
+ // }
- $blockRoot->setAttribute('data-beyondwords-marker', $marker);
+ // $blockRoot->setAttribute('data-beyondwords-marker', $marker);
- // Avoid using `$dom->saveHtml( $node )` because the node results may not produce consistent
- // whitespace. Saving the root HTML `$dom->saveHtml()` prevents this behavior.
- $fullHtml = $dom->saveHtml();
+ // // Avoid using `$dom->saveHtml( $node )` because the node results may not produce consistent
+ // // whitespace. Saving the root HTML `$dom->saveHtml()` prevents this behavior.
+ // $fullHtml = $dom->saveHtml();
- // Find the
open/close tags. The open tag needs to be adjusted so we get inside the tag
- // and not the tag itself.
- $start = strpos($fullHtml, '', 0) + strlen('');
- $end = strpos($fullHtml, '', $start);
+ // // Find the open/close tags. The open tag needs to be adjusted so we get inside the tag
+ // // and not the tag itself.
+ // $start = strpos($fullHtml, '', 0) + strlen('');
+ // $end = strpos($fullHtml, '', $start);
- return trim(substr($fullHtml, $start, $end - $start));
- }
+ // return trim(substr($fullHtml, $start, $end - $start));
+ // }
}
diff --git a/tests/cypress/e2e/block-editor/block-inserter.cy.js b/tests/cypress/e2e/block-editor/block-inserter.cy.js
index c04a0c73..2fdcac1f 100644
--- a/tests/cypress/e2e/block-editor/block-inserter.cy.js
+++ b/tests/cypress/e2e/block-editor/block-inserter.cy.js
@@ -91,7 +91,7 @@ context( 'Block Editor: Block Inserter', () => {
cy.contains( '.wp-block-paragraph', 'Third paragraph' );
} );
- it( `duplicated blocks get unique markers for ${ postType.name }`, () => {
+ it.skip( `duplicated blocks get unique markers for ${ postType.name }`, () => {
cy.visitPostEditor( postType.slug );
// Add a title
diff --git a/tests/cypress/e2e/block-editor/segment-markers.cy.js b/tests/cypress/e2e/block-editor/segment-markers.cy.js
index 61bc0489..8991ffb0 100644
--- a/tests/cypress/e2e/block-editor/segment-markers.cy.js
+++ b/tests/cypress/e2e/block-editor/segment-markers.cy.js
@@ -29,7 +29,7 @@ context( 'Block Editor: Segment markers', () => {
postTypes
.filter( ( x ) => x.priority )
.forEach( ( postType ) => {
- it( `A ${ postType.name } without audio should not have segment markers`, () => {
+ it.skip( `A ${ postType.name } without audio should not have segment markers`, () => {
cy.createPost( {
postType,
title: `I can add a ${ postType.name } without segment markers`,
@@ -62,7 +62,7 @@ context( 'Block Editor: Segment markers', () => {
);
} );
- it( `can add a ${ postType.name } with segment markers`, () => {
+ it.skip( `can add a ${ postType.name } with segment markers`, () => {
cy.createPost( {
postType,
title: `I can add a ${ postType.name } with segment markers`,
@@ -140,7 +140,7 @@ context( 'Block Editor: Segment markers', () => {
cy.task( 'activatePlugin', 'speechkit' );
} );
- it( `assigns unique markers for duplicated blocks in a ${ postType.name }`, () => {
+ it.skip( `assigns unique markers for duplicated blocks in a ${ postType.name }`, () => {
cy.createPost( {
postType,
title: `I see unique markers for duplicated blocks in a ${ postType.name }`,
@@ -199,7 +199,7 @@ context( 'Block Editor: Segment markers', () => {
} );
} );
- it( 'assigns markers when blocks are added programatically', () => {
+ it.skip( 'assigns markers when blocks are added programatically', () => {
cy.createPost( {
title: `I see markers when blocks are added programatically`,
} );
@@ -248,7 +248,7 @@ context( 'Block Editor: Segment markers', () => {
} );
// So far unable to write tests for pasted content, all attempts have failed :(
- it( 'assigns markers when content is pasted', () => {
+ it.skip( 'assigns markers when content is pasted', () => {
cy.createPost( {
title: `I see markers for pasted content`,
} );
@@ -294,7 +294,7 @@ context( 'Block Editor: Segment markers', () => {
} );
} );
- it( `makes existing duplicate segment markers unique`, () => {
+ it.skip( `makes existing duplicate segment markers unique`, () => {
cy.createPost( {
title: `I see existing duplicate markers are replaced with unique markers`,
} );
@@ -352,4 +352,87 @@ context( 'Block Editor: Segment markers', () => {
expect( markers[ 2 ] ).to.match( markerRegex );
} );
} );
+
+ it.skip( 'preserves markers when resaving a post', () => {
+ cy.createPost( {
+ title: 'Markers should not change on resave',
+ } );
+
+ // cy.closeWelcomeToBlockEditorTips()
+ cy.openBeyondwordsEditorPanel();
+
+ cy.getBlockEditorCheckbox( 'Generate audio' ).check();
+
+ // Add two paragraphs
+ cy.addParagraphBlock( 'One' );
+ cy.addParagraphBlock( 'Two' );
+
+ // Click on first paragraph and check placeholder is shown (no marker yet)
+ cy.contains( 'p.wp-block-paragraph', 'One' ).click();
+ cy.contains( 'label', 'Segment marker' )
+ .siblings( 'input' )
+ .first()
+ .should( 'have.attr', 'placeholder', 'Generated on save' );
+
+ // Publish the post
+ cy.publishWithConfirmation();
+
+ // Wait for the post to be published and markers to be refreshed
+ cy.wait( 2000 );
+
+ // Click on first paragraph and grab its marker
+ cy.contains( 'p.wp-block-paragraph', 'One' ).click();
+ cy.contains( 'label', 'Segment marker' )
+ .siblings( 'input' )
+ .first()
+ .invoke( 'val' )
+ .should( 'match', markerRegex )
+ .as( 'markerOne1' );
+
+ // Click on second paragraph and grab its marker
+ cy.contains( 'p.wp-block-paragraph', 'Two' ).click();
+ cy.contains( 'label', 'Segment marker' )
+ .siblings( 'input' )
+ .first()
+ .invoke( 'val' )
+ .should( 'match', markerRegex )
+ .as( 'markerTwo1' );
+
+ // Add a third paragraph
+ cy.addParagraphBlock( 'Three' );
+
+ // Update the post (resave)
+ cy.get( '.editor-post-publish-button' ).click();
+
+ // Wait for the post to be updated and markers to be refreshed
+ cy.wait( 2000 );
+
+ // Verify first paragraph still has the same marker
+ cy.contains( 'p.wp-block-paragraph', 'One' ).click();
+ cy.get( '@markerOne1' ).then( ( originalMarker ) => {
+ cy.contains( 'label', 'Segment marker' )
+ .siblings( 'input' )
+ .first()
+ .invoke( 'val' )
+ .should( 'equal', originalMarker );
+ } );
+
+ // Verify second paragraph still has the same marker
+ cy.contains( 'p.wp-block-paragraph', 'Two' ).click();
+ cy.get( '@markerTwo1' ).then( ( originalMarker ) => {
+ cy.contains( 'label', 'Segment marker' )
+ .siblings( 'input' )
+ .first()
+ .invoke( 'val' )
+ .should( 'equal', originalMarker );
+ } );
+
+ // Verify third paragraph has a new marker
+ cy.contains( 'p.wp-block-paragraph', 'Three' ).click();
+ cy.contains( 'label', 'Segment marker' )
+ .siblings( 'input' )
+ .first()
+ .invoke( 'val' )
+ .should( 'match', markerRegex );
+ } );
} );
diff --git a/tests/phpunit/Component/Post/BlockAttributes/BlockAttributesTest.php b/tests/phpunit/Component/Post/BlockAttributes/BlockAttributesTest.php
index 1870959b..bdea5ced 100644
--- a/tests/phpunit/Component/Post/BlockAttributes/BlockAttributesTest.php
+++ b/tests/phpunit/Component/Post/BlockAttributes/BlockAttributesTest.php
@@ -1,15 +1,9 @@
assertEquals(10, has_action('register_block_type_args', array(BlockAttributes::class, 'registerAudioAttribute')));
$this->assertEquals(10, has_action('register_block_type_args', array(BlockAttributes::class, 'registerMarkerAttribute')));
- $this->assertEquals(10, has_action('render_block', array(BlockAttributes::class, 'renderBlock')));
}
/**
@@ -185,113 +178,4 @@ public function registerMarkerAttributeProvider($args) {
],
];
}
-
- /**
- * @test
- */
- public function renderBlockWithUiDisabled()
- {
- update_option(PlayerUI::OPTION_NAME, PlayerUI::DISABLED);
-
- $this->assertSame(
- 'Test
',
- BlockAttributes::renderBlock('Test
', [
- 'attrs' => [
- 'beyondwordsMarker' => 'foo',
- ]
- ])
- );
-
- delete_option(PlayerUI::OPTION_NAME);
- }
-
- /**
- * @test
- */
- public function renderBlockWithoutCustomFields()
- {
- $postId = self::factory()->post->create([
- 'post_title' => 'BlockAttributesTest::renderBlockWithoutCustomFields',
- 'post_type' => 'post',
- ]);
-
- $this->go_to(get_permalink($postId));
- global $post;
- setup_postdata($post);
-
- $this->assertSame(
- 'Test
',
- BlockAttributes::renderBlock('Test
', [
- 'attrs' => [
- 'beyondwordsMarker' => 'foo',
- ]
- ])
- );
-
- wp_reset_postdata();
-
- wp_delete_post($postId, true);
- }
-
- /**
- * @test
- */
- public function renderBlockWithoutMarkerAttribute()
- {
- $postId = self::factory()->post->create([
- 'post_title' => 'BlockAttributesTest::renderBlockWithoutMarkerAttribute',
- 'meta_input' => [
- 'beyondwords_project_id' => BEYONDWORDS_TESTS_PROJECT_ID,
- 'beyondwords_content_id' => BEYONDWORDS_TESTS_CONTENT_ID,
- ],
- ]);
-
- $this->go_to(get_permalink($postId));
- global $post;
- setup_postdata($post);
-
- $this->assertSame(
- 'Test
',
- BlockAttributes::renderBlock('Test
', [
- 'attrs' => [
- 'foo' => 'bar',
- ]
- ])
- );
-
- wp_reset_postdata();
-
- wp_delete_post($postId, true);
- }
-
- /**
- * @test
- */
- public function renderBlockWithMarkerAttribute()
- {
- $postId = self::factory()->post->create([
- 'post_title' => 'BlockAttributesTest::renderBlockWithMarkerAttribute',
- 'meta_input' => [
- 'beyondwords_project_id' => BEYONDWORDS_TESTS_PROJECT_ID,
- 'beyondwords_content_id' => BEYONDWORDS_TESTS_CONTENT_ID,
- ],
- ]);
-
- $this->go_to(get_permalink($postId));
- global $post;
- setup_postdata($post);
-
- $this->assertSame(
- 'Test
',
- BlockAttributes::renderBlock('Test
', [
- 'attrs' => [
- 'beyondwordsMarker' => 'baz',
- ]
- ])
- );
-
- wp_reset_postdata();
-
- wp_delete_post($postId, true);
- }
}
diff --git a/tests/phpunit/Core/PostContentUtilsTest.php b/tests/phpunit/Core/PostContentUtilsTest.php
index 98d8939c..ab0bfc20 100644
--- a/tests/phpunit/Core/PostContentUtilsTest.php
+++ b/tests/phpunit/Core/PostContentUtilsTest.php
@@ -140,14 +140,14 @@ public function getContentWithoutExcludedBlocksProvider()
'Previous two paragraphs were empty.
';
$withBlocksExpect = 'No marker.
' .
- 'Has marker.
' .
+ 'Has marker.
' .
+ '' .
'' .
- '' .
'Previous two paragraphs were empty.
';
- $withoutBlocks = "One
\n\n\n\nThree
\n\n";
+ $withoutBlocks = "One
\n\n\n\nThree
\n\n";
- $withoutBlocksExpect = "One
\n\n\n\nThree
";
+ $withoutBlocksExpect = "One
\n\n\n\nThree
";
return [
'Content with blocks' => [ $withBlocks, $withBlocksExpect ],
@@ -619,90 +619,90 @@ public function getAuthorName()
* @test
* @dataProvider addMarkerAttributeWithHTMLTagProcessorProvider
*/
- public function addMarkerAttributeWithHTMLTagProcessor($html, $marker, $expect) {
- $result = PostContentUtils::addMarkerAttributeWithHTMLTagProcessor($html, $marker);
-
- $this->assertSame($expect, trim($result));
- }
-
- public function addMarkerAttributeWithHTMLTagProcessorProvider($args) {
- return [
- 'No HTML' => [
- 'html' => '',
- 'marker' => 'foo',
- 'expect' => '',
- ],
- 'No marker' => [
- 'html' => 'Text
',
- 'marker' => '',
- 'expect' => 'Text
',
- ],
- 'Paragraph' => [
- 'html' => 'Text
',
- 'marker' => 'foo',
- 'expect' => 'Text
',
- ],
- 'Empty paragraph' => [
- 'html' => '',
- 'marker' => 'foo',
- 'expect' => '',
- ],
- 'Existing attributes' => [
- 'html' => 'Text
',
- 'marker' => 'foo',
- 'expect' => 'Text
',
- ],
- 'Multiple root elements' => [
- 'html' => "One
\nTwo
",
- 'marker' => 'foo',
- 'expect' => "One
\nTwo
",
- ],
- ];
- }
+ // public function addMarkerAttributeWithHTMLTagProcessor($html, $marker, $expect) {
+ // $result = PostContentUtils::addMarkerAttributeWithHTMLTagProcessor($html, $marker);
+
+ // $this->assertSame($expect, trim($result));
+ // }
+
+ // public function addMarkerAttributeWithHTMLTagProcessorProvider($args) {
+ // return [
+ // 'No HTML' => [
+ // 'html' => '',
+ // 'marker' => 'foo',
+ // 'expect' => '',
+ // ],
+ // 'No marker' => [
+ // 'html' => 'Text
',
+ // 'marker' => '',
+ // 'expect' => 'Text
',
+ // ],
+ // 'Paragraph' => [
+ // 'html' => 'Text
',
+ // 'marker' => 'foo',
+ // 'expect' => 'Text
',
+ // ],
+ // 'Empty paragraph' => [
+ // 'html' => '',
+ // 'marker' => 'foo',
+ // 'expect' => '',
+ // ],
+ // 'Existing attributes' => [
+ // 'html' => 'Text
',
+ // 'marker' => 'foo',
+ // 'expect' => 'Text
',
+ // ],
+ // 'Multiple root elements' => [
+ // 'html' => "One
\nTwo
",
+ // 'marker' => 'foo',
+ // 'expect' => "One
\nTwo
",
+ // ],
+ // ];
+ // }
/**
* @test
* @dataProvider addMarkerAttributeWithDOMDocumentProvider
*/
- public function addMarkerAttributeWithDOMDocument($html, $marker, $expect)
- {
- $result = PostContentUtils::addMarkerAttributeWithDOMDocument($html, $marker);
-
- $this->assertSame($expect, trim($result));
- }
-
- public function addMarkerAttributeWithDOMDocumentProvider($args) {
- return [
- 'No HTML' => [
- 'html' => '',
- 'marker' => 'foo',
- 'expect' => '',
- ],
- 'No marker' => [
- 'html' => 'Text
',
- 'marker' => '',
- 'expect' => 'Text
',
- ],
- 'Paragraph' => [
- 'html' => 'Text
',
- 'marker' => 'foo',
- 'expect' => 'Text
',
- ],
- 'Empty paragraph' => [
- 'html' => '',
- 'marker' => 'foo',
- 'expect' => '',
- ],
- 'Existing attributes' => [
- 'html' => 'Text
',
- 'marker' => 'foo',
- 'expect' => 'Text
',
- ],
- 'Multiple root elements' => [
- 'html' => "One
\nTwo
",
- 'marker' => 'foo',
- 'expect' => "One
\nTwo
",
- ],
- ];
- }
+ // public function addMarkerAttributeWithDOMDocument($html, $marker, $expect)
+ // {
+ // $result = PostContentUtils::addMarkerAttributeWithDOMDocument($html, $marker);
+
+ // $this->assertSame($expect, trim($result));
+ // }
+
+ // public function addMarkerAttributeWithDOMDocumentProvider($args) {
+ // return [
+ // 'No HTML' => [
+ // 'html' => '',
+ // 'marker' => 'foo',
+ // 'expect' => '',
+ // ],
+ // 'No marker' => [
+ // 'html' => 'Text
',
+ // 'marker' => '',
+ // 'expect' => 'Text
',
+ // ],
+ // 'Paragraph' => [
+ // 'html' => 'Text
',
+ // 'marker' => 'foo',
+ // 'expect' => 'Text
',
+ // ],
+ // 'Empty paragraph' => [
+ // 'html' => '',
+ // 'marker' => 'foo',
+ // 'expect' => '',
+ // ],
+ // 'Existing attributes' => [
+ // 'html' => 'Text
',
+ // 'marker' => 'foo',
+ // 'expect' => 'Text
',
+ // ],
+ // 'Multiple root elements' => [
+ // 'html' => "One
\nTwo
",
+ // 'marker' => 'foo',
+ // 'expect' => "One
\nTwo
",
+ // ],
+ // ];
+ // }
}
From 692abf53cd82cb53d48c29a6c6cff78ccf7afd65 Mon Sep 17 00:00:00 2001
From: Stuart McAlpine
Date: Mon, 10 Nov 2025 11:53:22 +0000
Subject: [PATCH 3/7] Remove require
---
src/Component/Post/BlockAttributes/index.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/Component/Post/BlockAttributes/index.js b/src/Component/Post/BlockAttributes/index.js
index 4d2f6b6b..7aa1d532 100644
--- a/src/Component/Post/BlockAttributes/index.js
+++ b/src/Component/Post/BlockAttributes/index.js
@@ -1,3 +1,2 @@
require( './addAttributes' );
require( './addControls' );
-require( './refreshAfterSave' );
From f92bcf46c03857c43634aa8523f14b251c4ebfc5 Mon Sep 17 00:00:00 2001
From: Stuart McAlpine
Date: Mon, 10 Nov 2025 14:06:31 +0000
Subject: [PATCH 4/7] Remove/disable all code for segment markers
---
.../Post/BlockAttributes/BlockAttributes.php | 260 +----------
.../Post/BlockAttributes/addControls.js | 19 -
src/Component/Post/PostContentUtils.php | 134 +-----
.../e2e/block-editor/block-inserter.cy.js | 161 -------
.../e2e/block-editor/segment-markers.cy.js | 438 ------------------
.../phpunit/Core/Player/Renderer/AmpTest.php | 47 +-
tests/phpunit/Core/PostContentUtilsTest.php | 91 ----
7 files changed, 10 insertions(+), 1140 deletions(-)
delete mode 100644 tests/cypress/e2e/block-editor/block-inserter.cy.js
delete mode 100644 tests/cypress/e2e/block-editor/segment-markers.cy.js
diff --git a/src/Component/Post/BlockAttributes/BlockAttributes.php b/src/Component/Post/BlockAttributes/BlockAttributes.php
index 02275668..f41276b3 100644
--- a/src/Component/Post/BlockAttributes/BlockAttributes.php
+++ b/src/Component/Post/BlockAttributes/BlockAttributes.php
@@ -13,15 +13,12 @@
namespace Beyondwords\Wordpress\Component\Post\BlockAttributes;
-use Beyondwords\Wordpress\Component\Post\PostContentUtils;
-use Beyondwords\Wordpress\Component\Settings\Fields\PlayerUI\PlayerUI;
-use Symfony\Component\Uid\Uuid;
-
/**
* BlockAttributes
*
* @since 3.7.0
- * @since 4.0.0 Renamed from BlockAudioAttribute to BlockAttributes to support multiple attributes
+ * @since 4.0.0 Renamed from BlockAudioAttribute to BlockAttributes to support multiple attributes.
+ * @since 6.0.0 Stop adding beyondwordsMarker attribute to blocks.
*/
class BlockAttributes
{
@@ -29,81 +26,12 @@ class BlockAttributes
* Init.
*
* @since 4.0.0
- * @since 6.0.0 Make static.
- * @since 6.0.1 Add REST API hooks for marker initialization.
+ * @since 6.0.0 Make static and remove renderBlock registration.
*/
public static function init()
{
add_filter('register_block_type_args', [self::class, 'registerAudioAttribute']);
add_filter('register_block_type_args', [self::class, 'registerMarkerAttribute']);
- // add_filter('render_block', [self::class, 'renderBlock'], 10, 2);
-
- // Register hooks for marker initialization
- // add_filter('wp_insert_post_data', [self::class, 'initializeBlockMarkersBeforeSave'], 10, 2);
- }
-
- /**
- * Initialize block markers before post data is saved to database.
- *
- * This function runs right before post data is inserted/updated in the database
- * and ensures all blocks have unique markers.
- *
- * @since 6.0.1
- *
- * @param array $data An array of slashed, sanitized, and processed post data.
- * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data.
- *
- * @return array Modified post data.
- */
- public static function initializeBlockMarkersBeforeSave($data, $postarr)
- {
- // Skip if no content
- if (empty($data['post_content'])) {
- return $data;
- }
-
- // Skip autosaves and revisions
- if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
- return $data;
- }
-
- if (wp_is_post_revision($postarr['ID'] ?? 0)) {
- return $data;
- }
-
- // Skip if post type doesn't support custom fields
- if (! empty($data['post_type']) && ! post_type_supports($data['post_type'], 'custom-fields')) {
- return $data;
- }
-
- // Parse blocks
- $blocks = parse_blocks($data['post_content']);
-
- if (empty($blocks)) {
- return $data;
- }
-
- // Track existing markers to detect duplicates
- $existingMarkers = [];
- $needsUpdate = false;
-
- // Process blocks recursively
- $updatedBlocks = self::processBlocksForMarkers($blocks, $existingMarkers, $needsUpdate);
-
- // Only update content if changes were made
- if ($needsUpdate) {
- // Serialize blocks back to content
- $data['post_content'] = serialize_blocks($updatedBlocks);
-
- // Debug: Log marker summary
- // error_log(sprintf(
- // 'BeyondWords: Generated markers for post %d, found %d existing markers',
- // $postarr['ID'] ?? 0,
- // count($existingMarkers)
- // ));
- }
-
- return $data;
}
/**
@@ -131,6 +59,8 @@ public static function registerAudioAttribute($args)
/**
* Register "Segment marker" attribute for Gutenberg blocks.
*
+ * @deprecated This attribute is no longer used as of 6.0.0, but kept for backward compatibility.
+ *
* @since 6.0.0 Make static.
*/
public static function registerMarkerAttribute($args)
@@ -149,184 +79,4 @@ public static function registerMarkerAttribute($args)
return $args;
}
-
- /**
- * Render block as HTML.
- *
- * Performs some checks and then attempts to add data-beyondwords-marker
- * attribute to the root element of Gutenberg block.
- *
- * @since 4.0.0
- * @since 4.2.2 Rename method to renderBlock.
- * @since 6.0.0 Make static and update for Magic Embed.
- *
- * @param string $blockContent The block content (HTML).
- * @param string $block The full block, including name and attributes.
- *
- * @return string Block Content (HTML).
- */
- // public static function renderBlock($blockContent, $block)
- // {
- // // Skip adding marker if player UI is disabled
- // if (get_option(PlayerUI::OPTION_NAME) === PlayerUI::DISABLED) {
- // return $blockContent;
- // }
-
- // $postId = get_the_ID();
-
- // if (! $postId) {
- // return $blockContent;
- // }
-
- // $marker = $block['attrs']['beyondwordsMarker'] ?? '';
-
- // return PostContentUtils::addMarkerAttribute(
- // $blockContent,
- // $marker
- // );
- // }
-
- /**
- * Recursively process blocks to initialize and deduplicate markers.
- *
- * @since 6.0.1
- *
- * @param array $blocks Array of block arrays.
- * @param array $existingMarkers Reference to array tracking existing markers.
- * @param bool $needsUpdate Reference to flag indicating if update is needed.
- *
- * @return array Updated blocks.
- */
- // private static function processBlocksForMarkers($blocks, &$existingMarkers, &$needsUpdate)
- // {
- // $updatedBlocks = [];
-
- // foreach ($blocks as $block) {
- // // Skip blocks that shouldn't have markers
- // if (! self::shouldHaveBeyondWordsMarker($block['blockName'])) {
- // $updatedBlocks[] = $block;
- // continue;
- // }
-
- // // Check if block has beyondwordsAudio attribute
- // $hasAudio = $block['attrs']['beyondwordsAudio'] ?? true;
-
- // // Only process blocks with audio enabled
- // if ($hasAudio) {
- // $currentMarker = $block['attrs']['beyondwordsMarker'] ?? '';
-
- // // Check if marker is missing or duplicate
- // if (empty($currentMarker) || in_array($currentMarker, $existingMarkers, true)) {
- // // Generate new unique marker
- // $newMarker = self::generateUniqueUuid($existingMarkers);
-
- // // Debug: Log marker generation
- // // error_log(sprintf(
- // // 'BeyondWords: Generating marker for block %s (had: %s, generated: %s)',
- // // $block['blockName'] ?? 'unknown',
- // // $currentMarker ?: 'none',
- // // $newMarker
- // // ));
-
- // $block['attrs']['beyondwordsMarker'] = $newMarker;
- // $existingMarkers[] = $newMarker;
- // $needsUpdate = true;
- // } else {
- // // Track existing marker
- // $existingMarkers[] = $currentMarker;
- // }
- // }
-
- // // Process inner blocks recursively
- // if (! empty($block['innerBlocks'])) {
- // $block['innerBlocks'] = self::processBlocksForMarkers(
- // $block['innerBlocks'],
- // $existingMarkers,
- // $needsUpdate
- // );
- // }
-
- // $updatedBlocks[] = $block;
- // }
-
- // return $updatedBlocks;
- // }
-
- /**
- * Check if a block should have BeyondWords marker.
- *
- * @since 6.0.1
- *
- * @param string $blockName Block name.
- *
- * @return bool Whether the block should have a marker.
- */
- private static function shouldHaveBeyondWordsMarker($blockName)
- {
- // Skip blocks without a name
- if (empty($blockName)) {
- return false;
- }
-
- // Skip internal/UI blocks
- if (strpos($blockName, '__') === 0) {
- return false;
- }
-
- // Skip reusable blocks and template parts (these are containers)
- if (
- strpos($blockName, 'core/block') === 0 ||
- strpos($blockName, 'core/template') === 0
- ) {
- return false;
- }
-
- // Skip editor UI blocks
- $excludedBlocks = [
- 'core/freeform', // Classic editor
- 'core/legacy-widget',
- 'core/widget-area',
- 'core/navigation',
- 'core/navigation-link',
- 'core/navigation-submenu',
- 'core/site-logo',
- 'core/site-title',
- 'core/site-tagline',
- ];
-
- if (in_array($blockName, $excludedBlocks, true)) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Generate a unique UUID v4 that doesn't exist in the given array.
- *
- * @since 6.0.1
- *
- * @param array $existingMarkers Array of existing markers to check against.
- *
- * @return string UUID v4.
- */
- private static function generateUniqueUuid($existingMarkers)
- {
- $maxAttempts = 100;
- $attempts = 0;
-
- do {
- $uuid = Uuid::v4()->toRfc4122();
- $attempts++;
-
- // Ensure uniqueness
- if (! in_array($uuid, $existingMarkers, true)) {
- return $uuid;
- }
- } while ($attempts < $maxAttempts);
-
- // Fallback: append timestamp if somehow we can't generate unique UUID
- // This should never happen with proper UUIDs but provides safety
- return Uuid::v4()->toRfc4122() . '-' . time();
- }
}
diff --git a/src/Component/Post/BlockAttributes/addControls.js b/src/Component/Post/BlockAttributes/addControls.js
index 15af8aa7..3906f397 100644
--- a/src/Component/Post/BlockAttributes/addControls.js
+++ b/src/Component/Post/BlockAttributes/addControls.js
@@ -80,7 +80,6 @@ const withBeyondwordsBlockControls = createHigherOrderComponent(
}
const { attributes, setAttributes } = props;
- // const { beyondwordsAudio, beyondwordsMarker } = attributes;
const { beyondwordsAudio } = attributes;
const icon = !! beyondwordsAudio
@@ -117,24 +116,6 @@ const withBeyondwordsBlockControls = createHigherOrderComponent(
__nextHasNoMarginBottom
/>
- { /* { !! beyondwordsAudio && (
-
-
-
- ) } */ }
diff --git a/src/Component/Post/PostContentUtils.php b/src/Component/Post/PostContentUtils.php
index cf1bc1c5..97433fcd 100755
--- a/src/Component/Post/PostContentUtils.php
+++ b/src/Component/Post/PostContentUtils.php
@@ -166,6 +166,7 @@ public static function getPostSummary(int|\WP_Post $post): string|null
*
* @since 3.8.0
* @since 4.0.0 Replace for loop with array_reduce
+ * @since 6.0.0 Remove beyondwordsMarker attribute from rendered blocks.
*
* @return string The post body without excluded blocks.
*/
@@ -181,12 +182,6 @@ public static function getContentWithoutExcludedBlocks(int|\WP_Post $post): stri
$blocks = PostContentUtils::getAudioEnabledBlocks($post);
foreach ($blocks as $block) {
- // $marker = $block['attrs']['beyondwordsMarker'] ?? '';
-
- // $output .= PostContentUtils::addMarkerAttribute(
- // render_block($block),
- // $marker
- // );
$output .= render_block($block);
}
@@ -400,131 +395,4 @@ public static function getAuthorName(int $postId): string
return get_the_author_meta('display_name', $authorId);
}
-
- /**
- * Add data-beyondwords-marker attribute to the root elements in a HTML
- * string (typically the rendered HTML of a single block).
- *
- * Checks to see whether we can use WP_HTML_Tag_Processor, or whether we
- * fall back to using DOMDocument to add the marker.
- *
- * @since 4.2.2
- *
- * @param string $html HTML.
- * @param string $marker Marker UUID.
- *
- * @return string HTML.
- */
- // public static function addMarkerAttribute(string $html, string $marker): string
- // {
- // if (! $marker) {
- // return $html;
- // }
-
- // // Prefer WP_HTML_Tag_Processor, introduced in WordPress 6.2
- // if (class_exists('WP_HTML_Tag_Processor')) {
- // return PostContentUtils::addMarkerAttributeWithHTMLTagProcessor($html, $marker);
- // } else {
- // return PostContentUtils::addMarkerAttributeWithDOMDocument($html, $marker);
- // }
- // }
-
- /**
- * Add data-beyondwords-marker attribute to the root elements in a HTML
- * string using WP_HTML_Tag_Processor.
- *
- * @since 4.0.0
- * @since 4.2.2 Moved from src/Component/Post/BlockAttributes/BlockAttributes.php
- * to src/Component/Post/PostContentUtils.php
- * @since 4.7.0 Prevent empty data-beyondwords-marker attributes.
- *
- * @param string $html HTML.
- * @param string $marker Marker UUID.
- *
- * @return string HTML.
- */
- // public static function addMarkerAttributeWithHTMLTagProcessor(string $html, string $marker): string
- // {
- // if (! $marker) {
- // return $html;
- // }
-
- // // https://github.com/WordPress/gutenberg/pull/42485
- // $tags = new \WP_HTML_Tag_Processor($html);
-
- // if ($tags->next_tag()) {
- // $tags->set_attribute('data-beyondwords-marker', $marker);
- // }
-
- // return strval($tags);
- // }
-
- /**
- * Add data-beyondwords-marker attribute to the root elements in a HTML
- * string using DOMDocument.
- *
- * This is a fallback, since WP_HTML_Tag_Processor was only shipped with
- * WordPress 6.2 on 19 April 2023.
- *
- * https://make.wordpress.org/core/2022/10/13/whats-new-in-gutenberg-14-3-12-october/
- *
- * Note: It is not ideal to do all the $bodyElement/$fullHtml processing
- * in this method, but without it DOMDocument does not work as expected if
- * there is more than 1 root element. The approach here has been taken from
- * some historic Gutenberg code before they implemented WP_HTML_Tag_Processor:
- *
- * https://github.com/WordPress/gutenberg/blob/6671cef1179412a2bbd4969cbbc82705c7f69bac/lib/block-supports/index.php
- *
- * @since 4.0.0
- * @since 4.2.2 Moved from src/Component/Post/BlockAttributes/BlockAttributes.php
- * to src/Component/Post/PostContentUtils.php
- * @since 4.7.0 Prevent empty data-beyondwords-marker attributes.
- *
- * @param string $html HTML.
- * @param string $marker Marker UUID.
- *
- * @return string HTML.
- */
- // public static function addMarkerAttributeWithDOMDocument(string $html, string $marker): string
- // {
- // if (! $marker) {
- // return $html;
- // }
-
- // $dom = new \DOMDocument('1.0', 'utf-8');
-
- // $wrappedHtml =
- // ''
- // . $html
- // . '';
-
- // $success = $dom->loadHTML($wrappedHtml, LIBXML_HTML_NODEFDTD | LIBXML_COMPACT);
-
- // if (! $success) {
- // return $html;
- // }
-
- // // Structure is like ``, so body is the `lastChild` of our document.
- // $bodyElement = $dom->documentElement->lastChild;
-
- // $xpath = new \DOMXPath($dom);
- // $blockRoot = $xpath->query('./*', $bodyElement)[0];
-
- // if (empty($blockRoot)) {
- // return $html;
- // }
-
- // $blockRoot->setAttribute('data-beyondwords-marker', $marker);
-
- // // Avoid using `$dom->saveHtml( $node )` because the node results may not produce consistent
- // // whitespace. Saving the root HTML `$dom->saveHtml()` prevents this behavior.
- // $fullHtml = $dom->saveHtml();
-
- // // Find the open/close tags. The open tag needs to be adjusted so we get inside the tag
- // // and not the tag itself.
- // $start = strpos($fullHtml, '', 0) + strlen('');
- // $end = strpos($fullHtml, '', $start);
-
- // return trim(substr($fullHtml, $start, $end - $start));
- // }
}
diff --git a/tests/cypress/e2e/block-editor/block-inserter.cy.js b/tests/cypress/e2e/block-editor/block-inserter.cy.js
deleted file mode 100644
index 2fdcac1f..00000000
--- a/tests/cypress/e2e/block-editor/block-inserter.cy.js
+++ /dev/null
@@ -1,161 +0,0 @@
-/* global cy, beforeEach, context, it */
-
-context( 'Block Editor: Block Inserter', () => {
- beforeEach( () => {
- cy.login();
- } );
-
- const postTypes = require( '../../../../tests/fixtures/post-types.json' );
-
- postTypes
- .filter( ( x ) => x.supported )
- .forEach( ( postType ) => {
- it( `block inserter button appears correctly for ${ postType.name }`, () => {
- cy.visitPostEditor( postType.slug );
-
- // Add a title to make the editor active
- cy.get( '.editor-post-title__input' ).type(
- 'Test block inserter'
- );
-
- // Click after the title to focus the editor body
- cy.get( '.editor-post-title__input' ).type( '{enter}' );
-
- // Wait for the editor to be ready
- cy.wait( 500 );
-
- // The block inserter button ([+]) should be visible
- // This is the button that appears when you're in an empty block
- cy.get( 'button[aria-label="Add block"]' ).should(
- 'be.visible'
- );
-
- // Click the inserter button to open the block picker
- cy.get( 'button[aria-label="Add block"]' ).first().click();
-
- // The block picker popover should appear
- cy.get( '.block-editor-inserter__menu' ).should(
- 'be.visible'
- );
-
- // Should show "Browse all" option
- cy.contains( 'Browse all' ).should( 'be.visible' );
-
- // Close the inserter
- cy.get( 'body' ).type( '{esc}' );
- } );
-
- it( `can insert multiple blocks sequentially for ${ postType.name }`, () => {
- cy.visitPostEditor( postType.slug );
-
- // Add a title
- cy.get( '.editor-post-title__input' ).type(
- 'Test multiple blocks'
- );
- cy.get( '.editor-post-title__input' ).type( '{enter}' );
-
- // Wait for the editor to be ready
- cy.wait( 500 );
-
- // Type some text in the first block
- cy.get( '.block-editor-block-list__layout' )
- .first()
- .type( 'First paragraph' );
-
- // Press enter to create a new block
- cy.get( '.block-editor-block-list__layout' )
- .first()
- .type( '{enter}' );
-
- // The inserter button should still appear for the new block
- cy.get( 'button[aria-label="Add block"]' ).should(
- 'be.visible'
- );
-
- // Type in the second block
- cy.get( '.block-editor-block-list__layout' )
- .first()
- .type( 'Second paragraph{enter}' );
-
- // Type in the third block
- cy.get( '.block-editor-block-list__layout' )
- .first()
- .type( 'Third paragraph' );
-
- // Verify we have 3 paragraph blocks
- cy.get( '.wp-block-paragraph' ).should( 'have.length', 3 );
-
- // Verify the content
- cy.contains( '.wp-block-paragraph', 'First paragraph' );
- cy.contains( '.wp-block-paragraph', 'Second paragraph' );
- cy.contains( '.wp-block-paragraph', 'Third paragraph' );
- } );
-
- it.skip( `duplicated blocks get unique markers for ${ postType.name }`, () => {
- cy.visitPostEditor( postType.slug );
-
- // Add a title
- cy.get( '.editor-post-title__input' ).type(
- 'Test duplicate markers'
- );
- cy.get( '.editor-post-title__input' ).type( '{enter}' );
-
- // Wait for the editor to be ready
- cy.wait( 500 );
-
- // Type some text in the first block
- cy.get( '.block-editor-block-list__layout' )
- .first()
- .type( 'Original paragraph' );
-
- // Wait for the block to be created
- cy.wait( 500 );
-
- // Select the block by clicking on it
- cy.contains( '.wp-block-paragraph', 'Original paragraph' ).click();
-
- // Open the block options menu (three dots)
- cy.get( '.block-editor-block-toolbar' )
- .find( 'button[aria-label="Options"]' )
- .click();
-
- // Click "Duplicate" in the dropdown menu
- cy.contains( 'button', 'Duplicate' ).click();
-
- // Wait for duplication to complete
- cy.wait( 500 );
-
- // Verify we have 2 paragraph blocks with the same content
- cy.get( '.wp-block-paragraph' ).should( 'have.length', 2 );
-
- // Get the beyondwordsMarker attributes for both blocks
- cy.window().then( ( win ) => {
- const blocks = win.wp.data
- .select( 'core/block-editor' )
- .getBlocks();
-
- // Find the two paragraph blocks
- const paragraphBlocks = blocks.filter(
- ( block ) => block.name === 'core/paragraph'
- );
-
- expect( paragraphBlocks ).to.have.length( 2 );
-
- // Extract markers
- const marker1 =
- paragraphBlocks[ 0 ].attributes.beyondwordsMarker;
- const marker2 =
- paragraphBlocks[ 1 ].attributes.beyondwordsMarker;
-
- // Both should have markers
- expect( marker1 ).to.be.a( 'string' );
- expect( marker2 ).to.be.a( 'string' );
- expect( marker1 ).to.have.length.greaterThan( 0 );
- expect( marker2 ).to.have.length.greaterThan( 0 );
-
- // Markers should be different (not duplicates)
- expect( marker1 ).to.not.equal( marker2 );
- } );
- } );
- } );
-} );
diff --git a/tests/cypress/e2e/block-editor/segment-markers.cy.js b/tests/cypress/e2e/block-editor/segment-markers.cy.js
deleted file mode 100644
index 8991ffb0..00000000
--- a/tests/cypress/e2e/block-editor/segment-markers.cy.js
+++ /dev/null
@@ -1,438 +0,0 @@
-/* global Cypress, cy, beforeEach, context, expect, it */
-
-context( 'Block Editor: Segment markers', () => {
- beforeEach( () => {
- cy.login();
- } );
-
- const markerRegex =
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
-
- const postTypes = require( '../../../../tests/fixtures/post-types.json' );
-
- const testCases = [
- {
- id: 1,
- // eslint-disable-next-line max-len
- text: 'Latin symbols: á, é, í, ó, ú, ü, ñ, ¡, !, ¿, ?, Ä, ä, Ö, ö, Ü, ü, ẞ, ß, Æ, æ, Ø, ø, Å, å',
- },
- { id: 2, text: 'Kanji: 任天堂' },
- { id: 3, text: 'Katana: イリノイ州シカゴにて' },
- {
- id: 4,
- // eslint-disable-next-line max-len
- text: 'Mathematical symbols: αβγδεζηθικλμνξοπρσςτυφχψω ΓΔΘΛΞΠΣΦΨΩ ∫∑∏−±∞≈∝=≡≠≤≥×·⋅÷∂′″∇‰°∴∅ ∈∉∩∪⊂⊃⊆⊇¬∧∨∃∀⇒⇔→↔↑↓ℵ',
- },
- ];
-
- // Test priority post types
- postTypes
- .filter( ( x ) => x.priority )
- .forEach( ( postType ) => {
- it.skip( `A ${ postType.name } without audio should not have segment markers`, () => {
- cy.createPost( {
- postType,
- title: `I can add a ${ postType.name } without segment markers`,
- } );
-
- // cy.closeWelcomeToBlockEditorTips()
- cy.openBeyondwordsEditorPanel();
-
- cy.uncheckGenerateAudio( postType );
-
- // Add paragraphs
- cy.addParagraphBlock( 'One.' );
- cy.addParagraphBlock( 'Two.' );
-
- cy.publishWithConfirmation();
-
- // "View post"
- cy.viewPostViaSnackbar();
-
- cy.getPlayerScriptTag().should( 'not.exist' );
- cy.hasNoBeyondwordsWindowObject();
-
- cy.contains( 'p', 'One.' ).should(
- 'not.have.attr',
- 'data-beyondwords-marker'
- );
- cy.contains( 'p', 'Two.' ).should(
- 'not.have.attr',
- 'data-beyondwords-marker'
- );
- } );
-
- it.skip( `can add a ${ postType.name } with segment markers`, () => {
- cy.createPost( {
- postType,
- title: `I can add a ${ postType.name } with segment markers`,
- } );
-
- // cy.closeWelcomeToBlockEditorTips()
- cy.openBeyondwordsEditorPanel();
-
- cy.checkGenerateAudio( postType );
-
- /**
- * Ensure the marker is persistent (it DOES NOT change while typing)
- */
- cy.get( '.wp-block-post-content p:last-of-type' ).click();
- // Type a letter
- cy.get( 'body' ).type( `O` );
- // Check the marker
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .then( ( originalMarker ) => {
- // Type another letter
- cy.get( 'body' ).type( `K` ).wait( 200 );
- // Get marker value again and check it hasn't changed
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .should( 'equal', originalMarker );
- cy.get( 'body' ).type( `{enter}` ).wait( 200 );
- } );
-
- /**
- * Various test cases check we handle UTF-8 correctly
- */
- testCases.forEach( ( testCase ) => {
- // Add paragraph
- cy.addParagraphBlock( testCase.text );
-
- // Grab assigned marker from UI input
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .should( 'match', markerRegex ) // Check regex
- .as( `marker${ testCase.id }` );
- } );
-
- cy.publishWithConfirmation();
-
- // "View post"
- cy.viewPostViaSnackbar();
-
- cy.getPlayerScriptTag().should( 'exist' );
- cy.hasPlayerInstances( 1 );
-
- testCases.forEach( ( testCase ) => {
- cy.get( `@marker${ testCase.id }` ).then( ( marker ) => {
- cy.contains( 'p', testCase.text )
- .invoke( 'attr', 'data-beyondwords-marker' )
- .should( 'not.be.empty' ); // @todo check marker
- } );
- } );
-
- cy.task( 'deactivatePlugin', 'speechkit' );
- cy.reload();
-
- // Check content on page again, after deactivating the plugin
- testCases.forEach( ( testCase ) => {
- cy.contains( 'p', testCase.text ) // Text should be an exact UTF-8 match
- .should( 'not.have.attr', 'data-beyondwords-marker' );
- } );
-
- cy.task( 'activatePlugin', 'speechkit' );
- } );
-
- it.skip( `assigns unique markers for duplicated blocks in a ${ postType.name }`, () => {
- cy.createPost( {
- postType,
- title: `I see unique markers for duplicated blocks in a ${ postType.name }`,
- } );
-
- // cy.closeWelcomeToBlockEditorTips()
- cy.openBeyondwordsEditorPanel();
-
- cy.checkGenerateAudio( postType );
-
- // Add paragraph
- cy.addParagraphBlock( 'Test.' );
-
- // Grab assigned marker from UI input
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .as( 'marker1' );
-
- // Add first paragraph
- cy.get( '.editor-post-title' ).click();
- cy.contains( 'p.wp-block-paragraph', 'Test.' ).click();
-
- // Duplicate paragraph
- cy.get( '.block-editor-block-settings-menu' ).click();
- cy.contains(
- '.components-menu-item__item',
- 'Duplicate'
- ).click();
-
- cy.get( 'p:contains(Test.)' ).should( 'have.length', 2 );
-
- cy.publishWithConfirmation();
-
- // "View post"
- cy.viewPostViaSnackbar();
-
- cy.getPlayerScriptTag().should( 'exist' );
- cy.hasPlayerInstances( 1 );
-
- cy.get( '.entry-content p:not(:empty)' )
- .should( 'have.length', 2 )
- .mapInvoke( 'getAttribute', 'data-beyondwords-marker' )
- .then( ( markers ) => {
- // Markers must be unique
- const unique = Cypress._.uniq( markers );
- expect(
- unique,
- 'all markers are unique'
- ).to.have.length( markers.length );
-
- // All markers must be UUIDs
- expect( markers[ 0 ] ).to.match( markerRegex );
- expect( markers[ 1 ] ).to.match( markerRegex );
- } );
- } );
-
- it.skip( 'assigns markers when blocks are added programatically', () => {
- cy.createPost( {
- title: `I see markers when blocks are added programatically`,
- } );
-
- // cy.closeWelcomeToBlockEditorTips()
- cy.openBeyondwordsEditorPanel();
-
- cy.checkGenerateAudio( postType );
-
- // Add paragraph
- cy.createBlockProgramatically( 'core/paragraph', {
- content: 'One.',
- } );
-
- // Add paragraph
- cy.createBlockProgramatically( 'core/paragraph', {
- content: 'Two.',
- } );
-
- cy.get( 'p:contains(One.)' ).should( 'have.length', 1 );
- cy.get( 'p:contains(Two.)' ).should( 'have.length', 1 );
-
- cy.publishWithConfirmation();
-
- // "View post"
- cy.viewPostViaSnackbar();
-
- cy.getPlayerScriptTag().should( 'exist' );
- cy.hasPlayerInstances( 1 );
-
- cy.get( '.entry-content p:not(:empty)' )
- .should( 'have.length', 2 )
- .mapInvoke( 'getAttribute', 'data-beyondwords-marker' )
- .then( ( markers ) => {
- // Markers must be unique
- const unique = Cypress._.uniq( markers );
- expect(
- unique,
- 'all markers are unique'
- ).to.have.length( markers.length );
-
- // All markers must be UUIDs
- expect( markers[ 0 ] ).to.match( markerRegex );
- expect( markers[ 1 ] ).to.match( markerRegex );
- } );
- } );
-
- // So far unable to write tests for pasted content, all attempts have failed :(
- it.skip( 'assigns markers when content is pasted', () => {
- cy.createPost( {
- title: `I see markers for pasted content`,
- } );
-
- // cy.closeWelcomeToBlockEditorTips()
- cy.openBeyondwordsEditorPanel();
-
- cy.checkGenerateAudio( postType );
-
- // Click "+ block" button
- cy.get(
- '.block-editor-default-block-appender__content'
- ).click();
-
- cy.get( '.wp-block.is-selected' ).paste( 'One.\n\nTwo.' );
-
- cy.get( 'p:contains(One.)' ).should( 'have.length', 1 );
- cy.get( 'p:contains(Two.)' ).should( 'have.length', 1 );
-
- cy.publishWithConfirmation();
-
- // "View post"
- cy.viewPostViaSnackbar();
-
- cy.getPlayerScriptTag().should( 'exist' );
- cy.hasPlayerInstances( 1 );
-
- cy.get( '.entry-content p:not(:empty)' )
- .should( 'have.length', 2 )
- .mapInvoke( 'getAttribute', 'data-beyondwords-marker' )
- .then( ( markers ) => {
- // Markers must be unique
- const unique = Cypress._.uniq( markers );
- expect(
- unique,
- 'all markers are unique'
- ).to.have.length( markers.length );
-
- // All markers must be UUIDs
- expect( markers[ 0 ] ).to.match( markerRegex );
- expect( markers[ 1 ] ).to.match( markerRegex );
- } );
- } );
- } );
-
- it.skip( `makes existing duplicate segment markers unique`, () => {
- cy.createPost( {
- title: `I see existing duplicate markers are replaced with unique markers`,
- } );
-
- // cy.closeWelcomeToBlockEditorTips()
- cy.openBeyondwordsEditorPanel();
-
- cy.getBlockEditorCheckbox( 'Generate audio' ).check();
-
- // Add paragraph
- cy.createBlockProgramatically( 'core/paragraph', {
- content: 'One.',
- attributes: {
- beyondwordsMarker: '[DUPLICATE MARKER]',
- },
- } );
-
- // Add paragraph
- cy.createBlockProgramatically( 'core/paragraph', {
- content: 'Two.',
- attributes: {
- beyondwordsMarker: '[DUPLICATE MARKER]',
- },
- } );
-
- // Add paragraph
- cy.createBlockProgramatically( 'core/paragraph', {
- content: 'Three.',
- attributes: {
- beyondwordsMarker: '[DUPLICATE MARKER]',
- },
- } );
-
- cy.publishWithConfirmation();
-
- // "View post"
- cy.viewPostViaSnackbar();
-
- cy.getPlayerScriptTag().should( 'exist' );
- cy.hasPlayerInstances( 1 );
-
- cy.get( '.entry-content p:not(:empty)' )
- .should( 'have.length', 3 )
- .mapInvoke( 'getAttribute', 'data-beyondwords-marker' )
- .then( ( markers ) => {
- // Markers must be unique
- const unique = Cypress._.uniq( markers );
- expect( unique, 'all markers are unique' ).to.have.length(
- markers.length
- );
-
- // All markers must be UUIDs
- expect( markers[ 0 ] ).to.match( markerRegex );
- expect( markers[ 1 ] ).to.match( markerRegex );
- expect( markers[ 2 ] ).to.match( markerRegex );
- } );
- } );
-
- it.skip( 'preserves markers when resaving a post', () => {
- cy.createPost( {
- title: 'Markers should not change on resave',
- } );
-
- // cy.closeWelcomeToBlockEditorTips()
- cy.openBeyondwordsEditorPanel();
-
- cy.getBlockEditorCheckbox( 'Generate audio' ).check();
-
- // Add two paragraphs
- cy.addParagraphBlock( 'One' );
- cy.addParagraphBlock( 'Two' );
-
- // Click on first paragraph and check placeholder is shown (no marker yet)
- cy.contains( 'p.wp-block-paragraph', 'One' ).click();
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .should( 'have.attr', 'placeholder', 'Generated on save' );
-
- // Publish the post
- cy.publishWithConfirmation();
-
- // Wait for the post to be published and markers to be refreshed
- cy.wait( 2000 );
-
- // Click on first paragraph and grab its marker
- cy.contains( 'p.wp-block-paragraph', 'One' ).click();
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .should( 'match', markerRegex )
- .as( 'markerOne1' );
-
- // Click on second paragraph and grab its marker
- cy.contains( 'p.wp-block-paragraph', 'Two' ).click();
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .should( 'match', markerRegex )
- .as( 'markerTwo1' );
-
- // Add a third paragraph
- cy.addParagraphBlock( 'Three' );
-
- // Update the post (resave)
- cy.get( '.editor-post-publish-button' ).click();
-
- // Wait for the post to be updated and markers to be refreshed
- cy.wait( 2000 );
-
- // Verify first paragraph still has the same marker
- cy.contains( 'p.wp-block-paragraph', 'One' ).click();
- cy.get( '@markerOne1' ).then( ( originalMarker ) => {
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .should( 'equal', originalMarker );
- } );
-
- // Verify second paragraph still has the same marker
- cy.contains( 'p.wp-block-paragraph', 'Two' ).click();
- cy.get( '@markerTwo1' ).then( ( originalMarker ) => {
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .should( 'equal', originalMarker );
- } );
-
- // Verify third paragraph has a new marker
- cy.contains( 'p.wp-block-paragraph', 'Three' ).click();
- cy.contains( 'label', 'Segment marker' )
- .siblings( 'input' )
- .first()
- .invoke( 'val' )
- .should( 'match', markerRegex );
- } );
-} );
diff --git a/tests/phpunit/Core/Player/Renderer/AmpTest.php b/tests/phpunit/Core/Player/Renderer/AmpTest.php
index 514a1183..475a0a9d 100644
--- a/tests/phpunit/Core/Player/Renderer/AmpTest.php
+++ b/tests/phpunit/Core/Player/Renderer/AmpTest.php
@@ -5,9 +5,11 @@
use \Symfony\Component\DomCrawler\Crawler;
/**
- * Class Amp
+ * Test the Amp player renderer.
*
- * Renders the AMP-compatible BeyondWords player.
+ * Note that we are are not testing Amp::check() here due to limitations
+ * with mocking the amp_is_request() function in the current test environment.
+ * The Amp::check() method is covered by integration tests when the AMP plugin is active.
*/
class AmpTest extends TestCase
{
@@ -29,47 +31,6 @@ public function tearDown(): void
parent::tearDown();
}
- /**
- * @test
- */
- public function check()
- {
- $this->markTestSkipped(
- 'This test requires mocking amp_is_request() in a separate process, ' .
- 'which conflicts with the current Xdebug configuration in the test environment. ' .
- 'The Amp::check() method is covered by integration tests when the AMP plugin is active.'
- );
-
- // Note: Original test code is preserved below but not executed:
- //
- // Load stub to define amp_is_request() function
- // require_once __DIR__ . '/../../../Stubs/amp_is_request_true.php';
- //
- // $this->assertTrue(\amp_is_request());
- //
- // // Test 1: Post without BeyondWords meta should return false
- // $post = self::factory()->post->create_and_get([
- // 'post_title' => 'Amp::check::1',
- // ]);
- //
- // $this->assertFalse(Amp::check($post));
- //
- // wp_delete_post($post->ID, true);
- //
- // // Test 2: Post with BeyondWords content should return true
- // $post = self::factory()->post->create_and_get([
- // 'post_title' => 'Amp::check::2',
- // 'meta_input' => [
- // 'beyondwords_project_id' => BEYONDWORDS_TESTS_PROJECT_ID,
- // 'beyondwords_podcast_id' => BEYONDWORDS_TESTS_CONTENT_ID,
- // ],
- // ]);
- //
- // $this->assertTrue(Amp::check($post));
- //
- // wp_delete_post($post->ID, true);
- }
-
/**
* @test
*/
diff --git a/tests/phpunit/Core/PostContentUtilsTest.php b/tests/phpunit/Core/PostContentUtilsTest.php
index ab0bfc20..9f0a4f6d 100644
--- a/tests/phpunit/Core/PostContentUtilsTest.php
+++ b/tests/phpunit/Core/PostContentUtilsTest.php
@@ -614,95 +614,4 @@ public function getAuthorName()
wp_delete_post($post->ID, true);
}
-
- /**
- * @test
- * @dataProvider addMarkerAttributeWithHTMLTagProcessorProvider
- */
- // public function addMarkerAttributeWithHTMLTagProcessor($html, $marker, $expect) {
- // $result = PostContentUtils::addMarkerAttributeWithHTMLTagProcessor($html, $marker);
-
- // $this->assertSame($expect, trim($result));
- // }
-
- // public function addMarkerAttributeWithHTMLTagProcessorProvider($args) {
- // return [
- // 'No HTML' => [
- // 'html' => '',
- // 'marker' => 'foo',
- // 'expect' => '',
- // ],
- // 'No marker' => [
- // 'html' => 'Text
',
- // 'marker' => '',
- // 'expect' => 'Text
',
- // ],
- // 'Paragraph' => [
- // 'html' => 'Text
',
- // 'marker' => 'foo',
- // 'expect' => 'Text
',
- // ],
- // 'Empty paragraph' => [
- // 'html' => '',
- // 'marker' => 'foo',
- // 'expect' => '',
- // ],
- // 'Existing attributes' => [
- // 'html' => 'Text
',
- // 'marker' => 'foo',
- // 'expect' => 'Text
',
- // ],
- // 'Multiple root elements' => [
- // 'html' => "One
\nTwo
",
- // 'marker' => 'foo',
- // 'expect' => "One
\nTwo
",
- // ],
- // ];
- // }
-
- /**
- * @test
- * @dataProvider addMarkerAttributeWithDOMDocumentProvider
- */
- // public function addMarkerAttributeWithDOMDocument($html, $marker, $expect)
- // {
- // $result = PostContentUtils::addMarkerAttributeWithDOMDocument($html, $marker);
-
- // $this->assertSame($expect, trim($result));
- // }
-
- // public function addMarkerAttributeWithDOMDocumentProvider($args) {
- // return [
- // 'No HTML' => [
- // 'html' => '',
- // 'marker' => 'foo',
- // 'expect' => '',
- // ],
- // 'No marker' => [
- // 'html' => 'Text
',
- // 'marker' => '',
- // 'expect' => 'Text
',
- // ],
- // 'Paragraph' => [
- // 'html' => 'Text
',
- // 'marker' => 'foo',
- // 'expect' => 'Text
',
- // ],
- // 'Empty paragraph' => [
- // 'html' => '',
- // 'marker' => 'foo',
- // 'expect' => '',
- // ],
- // 'Existing attributes' => [
- // 'html' => 'Text
',
- // 'marker' => 'foo',
- // 'expect' => 'Text
',
- // ],
- // 'Multiple root elements' => [
- // 'html' => "One
\nTwo
",
- // 'marker' => 'foo',
- // 'expect' => "One
\nTwo
",
- // ],
- // ];
- // }
}
From ae0691a532d5307c34d842e14f92e442c5e00cce Mon Sep 17 00:00:00 2001
From: Stuart McAlpine
Date: Mon, 10 Nov 2025 14:10:45 +0000
Subject: [PATCH 5/7] Remove symfony/uid
---
composer.json | 3 +-
composer.lock | 159 +-------------------------------------------------
2 files changed, 2 insertions(+), 160 deletions(-)
diff --git a/composer.json b/composer.json
index 2aa7d05e..3ad97919 100644
--- a/composer.json
+++ b/composer.json
@@ -7,8 +7,7 @@
"require": {
"php": ">=8.0",
"symfony/dom-crawler": "^5.4",
- "symfony/property-access": "^5.4",
- "symfony/uid": "^7.3"
+ "symfony/property-access": "^5.4"
},
"require-dev": {
"automattic/vipwpcs": "^3.0.1",
diff --git a/composer.lock b/composer.lock
index ca087322..414c570f 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": "cf0482212addeb396b0cced4fb5170fc",
+ "content-hash": "871f668f3d5222a0e58403030261d782",
"packages": [
{
"name": "symfony/deprecation-contracts",
@@ -567,89 +567,6 @@
],
"time": "2025-01-02T08:10:11+00:00"
},
- {
- "name": "symfony/polyfill-uuid",
- "version": "v1.33.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-uuid.git",
- "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
- "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2",
- "shasum": ""
- },
- "require": {
- "php": ">=7.2"
- },
- "provide": {
- "ext-uuid": "*"
- },
- "suggest": {
- "ext-uuid": "For best performance"
- },
- "type": "library",
- "extra": {
- "thanks": {
- "url": "https://github.com/symfony/polyfill",
- "name": "symfony/polyfill"
- }
- },
- "autoload": {
- "files": [
- "bootstrap.php"
- ],
- "psr-4": {
- "Symfony\\Polyfill\\Uuid\\": ""
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Grégoire Pineau",
- "email": "lyrixx@lyrixx.info"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill for uuid functions",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "polyfill",
- "portable",
- "uuid"
- ],
- "support": {
- "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://github.com/nicolas-grekas",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2024-09-09T11:45:10+00:00"
- },
{
"name": "symfony/property-access",
"version": "v5.4.45",
@@ -910,80 +827,6 @@
}
],
"time": "2025-09-11T14:36:48+00:00"
- },
- {
- "name": "symfony/uid",
- "version": "v7.3.1",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/uid.git",
- "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb",
- "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb",
- "shasum": ""
- },
- "require": {
- "php": ">=8.2",
- "symfony/polyfill-uuid": "^1.15"
- },
- "require-dev": {
- "symfony/console": "^6.4|^7.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Uid\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Grégoire Pineau",
- "email": "lyrixx@lyrixx.info"
- },
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Provides an object-oriented API to generate and represent UIDs",
- "homepage": "https://symfony.com",
- "keywords": [
- "UID",
- "ulid",
- "uuid"
- ],
- "support": {
- "source": "https://github.com/symfony/uid/tree/v7.3.1"
- },
- "funding": [
- {
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
- }
- ],
- "time": "2025-06-27T19:55:54+00:00"
}
],
"packages-dev": [
From 52bfb6ea5d07ac1461abe493fb7c6f77f22bf363 Mon Sep 17 00:00:00 2001
From: Stuart McAlpine
Date: Mon, 10 Nov 2025 14:21:39 +0000
Subject: [PATCH 6/7] Add shared isBeyondwordsSupportedBlock check
---
.../Post/BlockAttributes/addAttributes.js | 47 ++-----------------
.../Post/BlockAttributes/addControls.js | 47 ++-----------------
.../isBeyondwordsSupportedBlock.js | 45 ++++++++++++++++++
3 files changed, 51 insertions(+), 88 deletions(-)
create mode 100644 src/Component/Post/BlockAttributes/isBeyondwordsSupportedBlock.js
diff --git a/src/Component/Post/BlockAttributes/addAttributes.js b/src/Component/Post/BlockAttributes/addAttributes.js
index 88ecc9b8..fe43983b 100644
--- a/src/Component/Post/BlockAttributes/addAttributes.js
+++ b/src/Component/Post/BlockAttributes/addAttributes.js
@@ -4,50 +4,9 @@
import { addFilter } from '@wordpress/hooks';
/**
- * Check if a block should have BeyondWords attributes.
- * Only content blocks that can be read aloud should have these attributes.
- *
- * @param {string} name Block name.
- * @return {boolean} Whether the block should have BeyondWords attributes.
+ * Internal dependencies
*/
-function shouldHaveBeyondWordsAttributes( name ) {
- // Skip blocks without a name
- if ( ! name ) {
- return false;
- }
-
- // Skip internal/UI blocks
- if ( name.startsWith( '__' ) ) {
- return false;
- }
-
- // Skip reusable blocks and template parts (these are containers)
- if (
- name.startsWith( 'core/block' ) ||
- name.startsWith( 'core/template' )
- ) {
- return false;
- }
-
- // Skip editor UI blocks
- const excludedBlocks = [
- 'core/freeform', // Classic editor
- 'core/legacy-widget',
- 'core/widget-area',
- 'core/navigation',
- 'core/navigation-link',
- 'core/navigation-submenu',
- 'core/site-logo',
- 'core/site-title',
- 'core/site-tagline',
- ];
-
- if ( excludedBlocks.includes( name ) ) {
- return false;
- }
-
- return true;
-}
+import { isBeyondwordsSupportedBlock } from './isBeyondwordsSupportedBlock';
/**
* Register custom block attributes for BeyondWords.
@@ -62,7 +21,7 @@ function shouldHaveBeyondWordsAttributes( name ) {
*/
function addAttributes( settings, name ) {
// Only add attributes to content blocks
- if ( ! shouldHaveBeyondWordsAttributes( name ) ) {
+ if ( ! isBeyondwordsSupportedBlock( name ) ) {
return settings;
}
diff --git a/src/Component/Post/BlockAttributes/addControls.js b/src/Component/Post/BlockAttributes/addControls.js
index 3906f397..53172b3e 100644
--- a/src/Component/Post/BlockAttributes/addControls.js
+++ b/src/Component/Post/BlockAttributes/addControls.js
@@ -6,7 +6,6 @@ import { InspectorControls, BlockControls } from '@wordpress/block-editor';
import {
PanelBody,
PanelRow,
- TextControl,
ToggleControl,
ToolbarButton,
ToolbarGroup,
@@ -15,49 +14,9 @@ import { createHigherOrderComponent } from '@wordpress/compose';
import { addFilter } from '@wordpress/hooks';
/**
- * Check if a block should have BeyondWords controls.
- *
- * @param {string} name Block name.
- * @return {boolean} Whether the block should have controls.
+ * Internal dependencies
*/
-function shouldHaveBeyondWordsControls( name ) {
- // Skip blocks without a name
- if ( ! name ) {
- return false;
- }
-
- // Skip internal/UI blocks
- if ( name.startsWith( '__' ) ) {
- return false;
- }
-
- // Skip reusable blocks and template parts (these are containers)
- if (
- name.startsWith( 'core/block' ) ||
- name.startsWith( 'core/template' )
- ) {
- return false;
- }
-
- // Skip editor UI blocks
- const excludedBlocks = [
- 'core/freeform', // Classic editor
- 'core/legacy-widget',
- 'core/widget-area',
- 'core/navigation',
- 'core/navigation-link',
- 'core/navigation-submenu',
- 'core/site-logo',
- 'core/site-title',
- 'core/site-tagline',
- ];
-
- if ( excludedBlocks.includes( name ) ) {
- return false;
- }
-
- return true;
-}
+import { isBeyondwordsSupportedBlock } from './isBeyondwordsSupportedBlock';
/**
* Add BeyondWords controls to Gutenberg Blocks.
@@ -75,7 +34,7 @@ const withBeyondwordsBlockControls = createHigherOrderComponent(
// Skip blocks that shouldn't have controls
// Do this check BEFORE accessing attributes to avoid unnecessary processing
- if ( ! shouldHaveBeyondWordsControls( name ) ) {
+ if ( ! isBeyondwordsSupportedBlock( name ) ) {
return ;
}
diff --git a/src/Component/Post/BlockAttributes/isBeyondwordsSupportedBlock.js b/src/Component/Post/BlockAttributes/isBeyondwordsSupportedBlock.js
new file mode 100644
index 00000000..502fee8f
--- /dev/null
+++ b/src/Component/Post/BlockAttributes/isBeyondwordsSupportedBlock.js
@@ -0,0 +1,45 @@
+/**
+ * Check if a block is supported by BeyondWords.
+ * Only content blocks that can be read aloud should have BeyondWords attributes and controls.
+ *
+ * @param {string} name Block name.
+ * @return {boolean} Whether the block is supported by BeyondWords.
+ */
+export function isBeyondwordsSupportedBlock( name ) {
+ // Skip blocks without a name
+ if ( ! name ) {
+ return false;
+ }
+
+ // Skip internal/UI blocks
+ if ( name.startsWith( '__' ) ) {
+ return false;
+ }
+
+ // Skip reusable blocks and template parts (these are containers)
+ if (
+ name.startsWith( 'core/block' ) ||
+ name.startsWith( 'core/template' )
+ ) {
+ return false;
+ }
+
+ // Skip editor UI blocks
+ const excludedBlocks = [
+ 'core/freeform', // Classic editor
+ 'core/legacy-widget',
+ 'core/widget-area',
+ 'core/navigation',
+ 'core/navigation-link',
+ 'core/navigation-submenu',
+ 'core/site-logo',
+ 'core/site-title',
+ 'core/site-tagline',
+ ];
+
+ if ( excludedBlocks.includes( name ) ) {
+ return false;
+ }
+
+ return true;
+}
From 6f053751e7f18acaa72ec5d61b8a613929a3045c Mon Sep 17 00:00:00 2001
From: Stuart McAlpine
Date: Mon, 10 Nov 2025 14:27:51 +0000
Subject: [PATCH 7/7] Suppress warnings for legitimate uses of WordPress core
filters
---
src/Component/Post/PostContentUtils.php | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/Component/Post/PostContentUtils.php b/src/Component/Post/PostContentUtils.php
index 97433fcd..e92bea55 100755
--- a/src/Component/Post/PostContentUtils.php
+++ b/src/Component/Post/PostContentUtils.php
@@ -84,6 +84,7 @@ public static function getPostBody(int|\WP_Post $post): string|null
}
// Apply the_content filters to handle shortcodes etc
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying core WordPress filter
$content = apply_filters('the_content', $content);
// Trim to remove trailing newlines – common for WordPress content
@@ -146,6 +147,7 @@ public static function getPostSummary(int|\WP_Post $post): string|null
// Escape characters
$summary = htmlentities($post->post_excerpt, ENT_QUOTES | ENT_XHTML);
// Apply WordPress filters
+ // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying core WordPress filter
$summary = apply_filters('get_the_excerpt', $summary);
// Convert line breaks into paragraphs
$summary = trim(wpautop($summary));