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\n

Three

\n\n"; + $withoutBlocks = "

One

\n\n

\n\n

Three

\n\n"; - $withoutBlocksExpect = "

One

\n\n

\n\n

Three

"; + $withoutBlocksExpect = "

One

\n\n

\n\n

Three

"; 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
\n
Two
", - 'marker' => 'foo', - 'expect' => "
One
\n
Two
", - ], - ]; - } + // 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
\n
Two
", + // 'marker' => 'foo', + // 'expect' => "
One
\n
Two
", + // ], + // ]; + // } /** * @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
\n
Two
", - 'marker' => 'foo', - 'expect' => "
One
\n
Two
", - ], - ]; - } + // 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
\n
Two
", + // 'marker' => 'foo', + // 'expect' => "
One
\n
Two
", + // ], + // ]; + // } } 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
\n
Two
", - // 'marker' => 'foo', - // 'expect' => "
One
\n
Two
", - // ], - // ]; - // } - - /** - * @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
\n
Two
", - // 'marker' => 'foo', - // 'expect' => "
One
\n
Two
", - // ], - // ]; - // } } 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));