From 68905a88be4757d4e691495d9c2daaec36e1fdda Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 30 Jul 2024 16:27:46 +0400 Subject: [PATCH 1/9] Update REST API controllers for newer compats --- ...ss-gutenberg-rest-global-styles-revisions-controller-6-6.php | 2 +- .../class-gutenberg-rest-templates-controller-6-6.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php b/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php index f725366c33cfb..3e5d4cdd68454 100644 --- a/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php +++ b/lib/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php @@ -15,7 +15,7 @@ * * @see WP_REST_Controller */ -class Gutenberg_REST_Global_Styles_Revisions_Controller_6_6 extends Gutenberg_REST_Global_Styles_Revisions_Controller_6_5 { +class Gutenberg_REST_Global_Styles_Revisions_Controller_6_6 extends WP_REST_Global_Styles_Revisions_Controller { /** * Prepares the revision for the REST response. * diff --git a/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php b/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php index e670afacea5b3..034187ca9a70a 100644 --- a/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php +++ b/lib/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php @@ -12,7 +12,7 @@ * `edit_theme_options` capability. In order to allow other roles to also view the templates, * we need to override the permissions check for the REST API endpoints. */ -class Gutenberg_REST_Templates_Controller_6_6 extends Gutenberg_REST_Templates_Controller_6_4 { +class Gutenberg_REST_Templates_Controller_6_6 extends WP_REST_Templates_Controller { /** * Checks if a given request has access to read templates. From b369052af070353d750989178f2a69ff1a4f4dbc Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 30 Jul 2024 16:31:28 +0400 Subject: [PATCH 2/9] Plugin: Remove compat layers for WP 6.4 and 6.5 --- lib/compat/plugin/footnotes.php | 250 -- lib/compat/wordpress-6.4/block-hooks.php | 377 -- lib/compat/wordpress-6.4/blocks.php | 23 - ...tenberg-rest-block-patterns-controller.php | 46 - ...global-styles-revisions-controller-6-4.php | 164 - ...utenberg-rest-templates-controller-6-4.php | 75 - .../font-face/class-wp-font-face-resolver.php | 183 - .../fonts/font-face/class-wp-font-face.php | 435 -- lib/compat/wordpress-6.4/fonts/fonts.php | 78 - ...class-gutenberg-html-tag-processor-6-4.php | 2457 ------------ ...ass-wp-html-active-formatting-elements.php | 190 - .../html-api/class-wp-html-open-elements.php | 435 -- .../class-wp-html-processor-state.php | 146 - .../html-api/class-wp-html-processor.php | 1446 ------- .../html-api/class-wp-html-token.php | 109 - .../class-wp-html-unsupported-exception.php | 34 - lib/compat/wordpress-6.4/kses.php | 18 - lib/compat/wordpress-6.4/rest-api.php | 40 - lib/compat/wordpress-6.4/script-loader.php | 112 - lib/compat/wordpress-6.4/theme-previews.php | 27 - .../block-bindings/block-bindings.php | 89 - .../class-wp-block-bindings-registry.php | 288 -- .../class-wp-block-bindings-source.php | 99 - .../block-bindings/post-meta.php | 65 - lib/compat/wordpress-6.5/block-patterns.php | 79 - lib/compat/wordpress-6.5/blocks.php | 414 -- ...global-styles-revisions-controller-6-5.php | 111 - .../wordpress-6.5/class-wp-script-modules.php | 362 -- lib/compat/wordpress-6.5/compat.php | 92 - .../fonts/class-wp-font-collection.php | 298 -- .../fonts/class-wp-font-library.php | 145 - .../fonts/class-wp-font-utils.php | 260 -- ...ss-wp-rest-font-collections-controller.php | 326 -- .../class-wp-rest-font-faces-controller.php | 952 ----- ...class-wp-rest-font-families-controller.php | 567 --- lib/compat/wordpress-6.5/fonts/fonts.php | 485 --- ...ass-gutenberg-html-attribute-token-6-5.php | 116 - ...class-gutenberg-html-open-elements-6-5.php | 462 --- .../class-gutenberg-html-processor-6-5.php | 1929 --------- ...ass-gutenberg-html-processor-state-6-5.php | 143 - .../class-gutenberg-html-span-6-5.php | 56 - ...class-gutenberg-html-tag-processor-6-5.php | 3555 ----------------- ...ss-gutenberg-html-text-replacement-6-5.php | 64 - ...interactivity-api-directives-processor.php | 246 -- .../class-wp-interactivity-api.php | 992 ----- .../interactivity-api/interactivity-api.php | 205 - lib/compat/wordpress-6.5/kses.php | 18 - .../navigation-block-variations.php | 143 - lib/compat/wordpress-6.5/rest-api.php | 136 - lib/compat/wordpress-6.5/script-loader.php | 207 - lib/compat/wordpress-6.5/scripts-modules.php | 224 -- lib/load.php | 88 - 52 files changed, 19861 deletions(-) delete mode 100644 lib/compat/plugin/footnotes.php delete mode 100644 lib/compat/wordpress-6.4/block-hooks.php delete mode 100644 lib/compat/wordpress-6.4/blocks.php delete mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php delete mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php delete mode 100644 lib/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php delete mode 100644 lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php delete mode 100644 lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php delete mode 100644 lib/compat/wordpress-6.4/fonts/fonts.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-active-formatting-elements.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-open-elements.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-processor-state.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-processor.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-token.php delete mode 100644 lib/compat/wordpress-6.4/html-api/class-wp-html-unsupported-exception.php delete mode 100644 lib/compat/wordpress-6.4/kses.php delete mode 100644 lib/compat/wordpress-6.4/rest-api.php delete mode 100644 lib/compat/wordpress-6.4/script-loader.php delete mode 100644 lib/compat/wordpress-6.4/theme-previews.php delete mode 100644 lib/compat/wordpress-6.5/block-bindings/block-bindings.php delete mode 100644 lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php delete mode 100644 lib/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-source.php delete mode 100644 lib/compat/wordpress-6.5/block-bindings/post-meta.php delete mode 100644 lib/compat/wordpress-6.5/block-patterns.php delete mode 100644 lib/compat/wordpress-6.5/blocks.php delete mode 100644 lib/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php delete mode 100644 lib/compat/wordpress-6.5/class-wp-script-modules.php delete mode 100644 lib/compat/wordpress-6.5/compat.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-font-collection.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-font-library.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-font-utils.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php delete mode 100644 lib/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php delete mode 100644 lib/compat/wordpress-6.5/fonts/fonts.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-open-elements-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-state-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php delete mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php delete mode 100644 lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php delete mode 100644 lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php delete mode 100644 lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php delete mode 100644 lib/compat/wordpress-6.5/kses.php delete mode 100644 lib/compat/wordpress-6.5/navigation-block-variations.php delete mode 100644 lib/compat/wordpress-6.5/rest-api.php delete mode 100644 lib/compat/wordpress-6.5/script-loader.php delete mode 100644 lib/compat/wordpress-6.5/scripts-modules.php diff --git a/lib/compat/plugin/footnotes.php b/lib/compat/plugin/footnotes.php deleted file mode 100644 index a3d89aba0ae96..0000000000000 --- a/lib/compat/plugin/footnotes.php +++ /dev/null @@ -1,250 +0,0 @@ -post_parent; - - // Just making sure we're updating the right revision. - if ( $post->ID === $post_id ) { - $footnotes = get_post_meta( $post_id, 'footnotes', true ); - - if ( $footnotes ) { - // Can't use update_post_meta() because it doesn't allow revisions. - update_metadata( 'post', $wp_temporary_footnote_revision_id, 'footnotes', wp_slash( $footnotes ) ); - } - } - } - } - - if ( ! function_exists( 'wp_post_revision_meta_keys' ) ) { - add_action( 'rest_after_insert_post', 'wp_add_footnotes_revisions_to_post_meta' ); - add_action( 'rest_after_insert_page', 'wp_add_footnotes_revisions_to_post_meta' ); - } - } - - if ( ! function_exists( 'wp_restore_footnotes_from_revision' ) ) { - - /** - * Restores the footnotes meta value from the revision. - * - * @since 6.3.0 - * @since 6.4.0 Core added post meta revisions, so this is no longer needed. - * - * @param int $post_id The post ID. - * @param int $revision_id The revision ID. - */ - function wp_restore_footnotes_from_revision( $post_id, $revision_id ) { - $footnotes = get_post_meta( $revision_id, 'footnotes', true ); - - if ( $footnotes ) { - update_post_meta( $post_id, 'footnotes', wp_slash( $footnotes ) ); - } else { - delete_post_meta( $post_id, 'footnotes' ); - } - } - if ( ! function_exists( 'wp_post_revision_meta_keys' ) ) { - add_action( 'wp_restore_post_revision', 'wp_restore_footnotes_from_revision', 10, 2 ); - } - } - - if ( ! function_exists( '_wp_rest_api_autosave_meta' ) ) { - - /** - * The REST API autosave endpoint doesn't save meta, so we can use the - * `wp_creating_autosave` when it updates an exiting autosave, and - * `_wp_put_post_revision` when it creates a new autosave. - * - * @since 6.3.0 - * @since 6.4.0 Core added post meta revisions, so this is no longer needed. - * - * @param int|array $autosave The autosave ID or array. - */ - function _wp_rest_api_autosave_meta( $autosave ) { - // Ensure it's a REST API request. - if ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) { - return; - } - - $body = rest_get_server()->get_raw_data(); - $body = json_decode( $body, true ); - - if ( ! isset( $body['meta']['footnotes'] ) ) { - return; - } - - // `wp_creating_autosave` passes the array, - // `_wp_put_post_revision` passes the ID. - $id = is_int( $autosave ) ? $autosave : $autosave['ID']; - - if ( ! $id ) { - return; - } - - // Can't use update_post_meta() because it doesn't allow revisions. - update_metadata( 'post', $id, 'footnotes', wp_slash( $body['meta']['footnotes'] ) ); - } - - if ( ! function_exists( 'wp_post_revision_meta_keys' ) ) { - // See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L391C1-L391C1. - add_action( 'wp_creating_autosave', '_wp_rest_api_autosave_meta' ); - // See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L398. - // Then https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/revision.php#L367. - add_action( '_wp_put_post_revision', '_wp_rest_api_autosave_meta' ); - } - } - - if ( ! function_exists( '_wp_rest_api_force_autosave_difference' ) ) { - - /** - * This is a workaround for the autosave endpoint returning early if the - * revision field are equal. The problem is that "footnotes" is not real - * revision post field, so there's nothing to compare against. - * - * This trick sets the "footnotes" field (value doesn't matter), which will - * cause the autosave endpoint to always update the latest revision. That should - * be fine, it should be ok to update the revision even if nothing changed. Of - * course, this is temporary fix. - * - * @since 6.3.0 - * @since 6.4.0 Core added post meta revisions, so this is no longer needed. - * - * @param WP_Post $prepared_post The prepared post object. - * @param WP_REST_Request $request The request object. - * - * See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L365-L384. - * See https://github.com/WordPress/wordpress-develop/blob/2103cb9966e57d452c94218bbc3171579b536a40/src/wp-includes/rest-api/endpoints/class-wp-rest-autosaves-controller.php#L219. - */ - function _wp_rest_api_force_autosave_difference( $prepared_post, $request ) { - // We only want to be altering POST requests. - if ( $request->get_method() !== 'POST' ) { - return $prepared_post; - } - - // Only alter requests for the '/autosaves' route. - if ( substr( $request->get_route(), -strlen( '/autosaves' ) ) !== '/autosaves' ) { - return $prepared_post; - } - - $prepared_post->footnotes = '[]'; - return $prepared_post; - } - if ( ! function_exists( 'wp_post_revision_meta_keys' ) ) { - add_filter( 'rest_pre_insert_post', '_wp_rest_api_force_autosave_difference', 10, 2 ); - } - } -} diff --git a/lib/compat/wordpress-6.4/block-hooks.php b/lib/compat/wordpress-6.4/block-hooks.php deleted file mode 100644 index f77582caf1345..0000000000000 --- a/lib/compat/wordpress-6.4/block-hooks.php +++ /dev/null @@ -1,377 +0,0 @@ - 'before', - 'after' => 'after', - 'firstChild' => 'first_child', - 'lastChild' => 'last_child', - ); - - $inserted_block_name = $metadata['name']; - foreach ( $block_hooks as $anchor_block_name => $position ) { - // Avoid infinite recursion (hooking to itself). - if ( $inserted_block_name === $anchor_block_name ) { - _doing_it_wrong( - __METHOD__, - __( 'Cannot hook block to itself.', 'gutenberg' ), - '6.4.0' - ); - continue; - } - - if ( ! isset( $property_mappings[ $position ] ) ) { - continue; - } - - $mapped_position = $property_mappings[ $position ]; - - gutenberg_add_hooked_block( $inserted_block_name, $mapped_position, $anchor_block_name ); - - $settings['block_hooks'][ $anchor_block_name ] = $mapped_position; - } - - // Copied from `get_block_editor_server_block_settings()`. - $fields_to_pick = array( - 'api_version' => 'apiVersion', - 'title' => 'title', - 'description' => 'description', - 'icon' => 'icon', - 'attributes' => 'attributes', - 'provides_context' => 'providesContext', - 'uses_context' => 'usesContext', - 'selectors' => 'selectors', - 'supports' => 'supports', - 'category' => 'category', - 'styles' => 'styles', - 'textdomain' => 'textdomain', - 'parent' => 'parent', - 'ancestor' => 'ancestor', - 'keywords' => 'keywords', - 'example' => 'example', - 'variations' => 'variations', - 'allowed_blocks' => 'allowedBlocks', - ); - // Add `block_hooks` to the list of fields to pick. - $fields_to_pick['block_hooks'] = 'blockHooks'; - - $exposed_settings = array_intersect_key( $settings, $fields_to_pick ); - - // TODO: Make work for blocks registered via direct call to gutenberg_add_hooked_block(). - wp_add_inline_script( - 'wp-blocks', - 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( array( $inserted_block_name => $exposed_settings ) ) . ');' - ); - - return $settings; -} - -/** - * Register a hooked block for automatic insertion into a given block hook. - * - * A block hook is specified by a block type and a relative position. The hooked block - * will be automatically inserted in the given position next to the "anchor" block - * whenever the latter is encountered. This applies both to the frontend and to the markup - * returned by the templates and patterns REST API endpoints. - * - * This is currently done by filtering parsed blocks as obtained from a block template, - * template part, or pattern, and injecting the hooked block where applicable. - * - * @todo In the long run, we'd likely want some sort of registry for hooked blocks. - * - * @param string $hooked_block The name of the block to insert. - * @param string $position The desired position of the hooked block, relative to its anchor block. - * Can be 'before', 'after', 'first_child', or 'last_child'. - * @param string $anchor_block The name of the block to insert the hooked block next to. - * @return void - */ -function gutenberg_add_hooked_block( $hooked_block, $position, $anchor_block ) { - $hooked_block_array = array( - 'blockName' => $hooked_block, - 'attrs' => array(), - 'innerHTML' => '', - 'innerContent' => array(), - 'innerBlocks' => array(), - ); - - $inserter = gutenberg_insert_hooked_block( $hooked_block_array, $position, $anchor_block ); - add_filter( 'gutenberg_serialize_block', $inserter, 10, 1 ); - - /* - * The block-types REST API controller uses objects of the `WP_Block_Type` class, which are - * in turn created upon block type registration. However, that class does not contain - * a `block_hooks` property (and is not easily extensible), so we have to use a different - * mechanism to communicate to the controller which hooked blocks have been registered for - * automatic insertion. We're doing so here (i.e. upon block registration), by adding a filter to - * the controller's response. - */ - $controller_extender = gutenberg_add_block_hooks_field_to_block_type_controller( $hooked_block, $position, $anchor_block ); - add_filter( 'rest_prepare_block_type', $controller_extender, 10, 2 ); -} - -/** - * Return a function that auto-inserts a block next to a given "anchor" block. - * - * This is a helper function used in the implementation of block hooks. - * It is not meant for public use. - * - * The auto-inserted block can be inserted before or after the anchor block, - * or as the first or last child of the anchor block. - * - * Note that the returned function mutates the automatically inserted block's - * designated parent block by inserting into the parent's `innerBlocks` array, - * and by updating the parent's `innerContent` array accordingly. - * - * @param array $inserted_block The block to insert. - * @param string $relative_position The position relative to the given block. - * Can be 'before', 'after', 'first_child', or 'last_child'. - * @param string $anchor_block_type The automatically inserted block will be inserted next to instances of this block type. - * @return callable A function that accepts a block's content and returns the content with the inserted block. - */ -function gutenberg_insert_hooked_block( $inserted_block, $relative_position, $anchor_block_type ) { - return function ( $block ) use ( $inserted_block, $relative_position, $anchor_block_type ) { - if ( $anchor_block_type === $block['blockName'] ) { - if ( 'first_child' === $relative_position ) { - array_unshift( $block['innerBlocks'], $inserted_block ); - // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) - // when rendering blocks, we also need to prepend a value (`null`, to mark a block - // location) to that array after HTML content for the inner blocks wrapper. - $chunk_index = 0; - for ( $index = $chunk_index; $index < count( $block['innerContent'] ); $index++ ) { - if ( is_null( $block['innerContent'][ $index ] ) ) { - $chunk_index = $index; - break; - } - } - array_splice( $block['innerContent'], $chunk_index, 0, array( null ) ); - } elseif ( 'last_child' === $relative_position ) { - array_push( $block['innerBlocks'], $inserted_block ); - // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) - // when rendering blocks, we also need to correctly append a value (`null`, to mark a block - // location) to that array before the remaining HTML content for the inner blocks wrapper. - $chunk_index = count( $block['innerContent'] ); - for ( $index = count( $block['innerContent'] ); $index > 0; $index-- ) { - if ( is_null( $block['innerContent'][ $index - 1 ] ) ) { - $chunk_index = $index; - break; - } - } - array_splice( $block['innerContent'], $chunk_index, 0, array( null ) ); - } - return $block; - } - - $anchor_block_index = array_search( $anchor_block_type, array_column( $block['innerBlocks'], 'blockName' ), true ); - if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) { - if ( 'after' === $relative_position ) { - ++$anchor_block_index; - } - array_splice( $block['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) ); - - // Find matching `innerContent` chunk index. - $chunk_index = 0; - while ( $anchor_block_index > 0 ) { - if ( ! is_string( $block['innerContent'][ $chunk_index ] ) ) { - --$anchor_block_index; - } - ++$chunk_index; - } - // Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`) - // when rendering blocks, we also need to insert a value (`null`, to mark a block - // location) into that array. - array_splice( $block['innerContent'], $chunk_index, 0, array( null ) ); - } - return $block; - }; -} - -/** - * Add block hooks information to a block type's controller. - * - * @param array $inserted_block_type The type of block to insert. - * @param string $position The position relative to the anchor block. - * Can be 'before', 'after', 'first_child', or 'last_child'. - * @param string $anchor_block_type The hooked block will be inserted next to instances of this block type. - * @return callable A filter for the `rest_prepare_block_type` hook that adds a `block_hooks` field to the network response. - */ -function gutenberg_add_block_hooks_field_to_block_type_controller( $inserted_block_type, $position, $anchor_block_type ) { - return function ( $response, $block_type ) use ( $inserted_block_type, $position, $anchor_block_type ) { - if ( $block_type->name !== $inserted_block_type ) { - return $response; - } - - $data = $response->get_data(); - if ( ! isset( $data['block_hooks'] ) ) { - $data['block_hooks'] = array(); - } - $data['block_hooks'][ $anchor_block_type ] = $position; - $response->set_data( $data ); - return $response; - }; -} - -/** - * Parse and reserialize block templates to allow running filters. - * - * By parsing a block template's content and then reserializing it - * via `gutenberg_serialize_blocks()`, we are able to run filters - * on the parsed blocks. This allows us to modify (parsed) blocks during - * depth-first traversal already provided by the serialization process, - * rather than having to do so in a separate pass. - * - * @param WP_Block_Template[] $query_result Array of found block templates. - * @return WP_Block_Template[] Updated array of found block templates. - */ -function gutenberg_parse_and_serialize_block_templates( $query_result ) { - foreach ( $query_result as $block_template ) { - if ( empty( $block_template->content ) || 'custom' === $block_template->source ) { - continue; - } - $blocks = parse_blocks( $block_template->content ); - $block_template->content = gutenberg_serialize_blocks( $blocks ); - } - - return $query_result; -} - -/** - * Filters the block template object after it has been (potentially) fetched from the theme file. - * - * By parsing a block template's content and then reserializing it - * via `gutenberg_serialize_blocks()`, we are able to run filters - * on the parsed blocks. This allows us to modify (parsed) blocks during - * depth-first traversal already provided by the serialization process, - * rather than having to do so in a separate pass. - * - * @param WP_Block_Template|null $block_template The found block template, or null if there is none. - */ -function gutenberg_parse_and_serialize_blocks( $block_template ) { - if ( empty( $block_template->content ) ) { - return $block_template; - } - - $blocks = parse_blocks( $block_template->content ); - $block_template->content = gutenberg_serialize_blocks( $blocks ); - - return $block_template; -} - -/** - * Register the `block_hooks` field for the block-types REST API controller. - * - * @return void - */ -function gutenberg_register_block_hooks_rest_field() { - register_rest_field( - 'block-type', - 'block_hooks', - array( - 'schema' => array( - 'description' => __( 'This block is automatically inserted near any occurrence of the block types used as keys of this map, into a relative position given by the corresponding value.', 'gutenberg' ), - 'type' => 'object', - 'patternProperties' => array( - '^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array( - 'type' => 'string', - 'enum' => array( 'before', 'after', 'first_child', 'last_child' ), - ), - ), - ), - ) - ); -} - -// Install the polyfill for Block Hooks only if it isn't already handled in WordPress core. -if ( ! function_exists( 'traverse_and_serialize_blocks' ) ) { - add_filter( 'block_type_metadata_settings', 'gutenberg_add_hooked_blocks', 10, 2 ); - add_filter( 'get_block_templates', 'gutenberg_parse_and_serialize_block_templates', 10, 1 ); - add_filter( 'get_block_file_template', 'gutenberg_parse_and_serialize_blocks', 10, 1 ); - add_action( 'rest_api_init', 'gutenberg_register_block_hooks_rest_field' ); -} - -// Helper functions. -// ----------------- -// The sole purpose of the following two functions (`gutenberg_serialize_block` -// and `gutenberg_serialize_blocks`), which are otherwise copies of their unprefixed -// counterparts (`serialize_block` and `serialize_blocks`) is to apply a filter -// (also called `gutenberg_serialize_block`) as an entry point for modifications -// to the parsed blocks. - -/** - * Filterable version of `serialize_block()`. - * - * This function is identical to `serialize_block()`, except that it applies - * the `gutenberg_serialize_block` filter to each block before it is serialized. - * - * @param array $block The block to be serialized. - * @return string The serialized block. - * - * @see serialize_block() - */ -function gutenberg_serialize_block( $block ) { - $block_content = ''; - - /** - * Filters a parsed block before it is serialized. - * - * @param array $block The block to be serialized. - */ - $block = apply_filters( 'gutenberg_serialize_block', $block ); - - $index = 0; - foreach ( $block['innerContent'] as $chunk ) { - if ( is_string( $chunk ) ) { - $block_content .= $chunk; - } else { // Compare to WP_Block::render(). - $inner_block = $block['innerBlocks'][ $index++ ]; - $block_content .= gutenberg_serialize_block( $inner_block ); - } - } - - if ( ! is_array( $block['attrs'] ) ) { - $block['attrs'] = array(); - } - - return get_comment_delimited_block_content( - $block['blockName'], - $block['attrs'], - $block_content - ); -} - -/** - * Filterable version of `serialize_blocks()`. - * - * This function is identical to `serialize_blocks()`, except that it applies - * the `gutenberg_serialize_block` filter to each block before it is serialized. - * - * @param array $blocks The blocks to be serialized. - * @return string[] The serialized blocks. - * - * @see serialize_blocks() - */ -function gutenberg_serialize_blocks( $blocks ) { - return implode( '', array_map( 'gutenberg_serialize_block', $blocks ) ); -} diff --git a/lib/compat/wordpress-6.4/blocks.php b/lib/compat/wordpress-6.4/blocks.php deleted file mode 100644 index 74fa9253e45d5..0000000000000 --- a/lib/compat/wordpress-6.4/blocks.php +++ /dev/null @@ -1,23 +0,0 @@ -get_data(); - - if ( empty( $data['content'] ) ) { - return $response; - } - - $blocks = parse_blocks( $data['content'] ); - $data['content'] = gutenberg_serialize_blocks( $blocks ); // Serialize or render? - - return rest_ensure_response( $data ); - } -} diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php deleted file mode 100644 index 4c7df97c33e57..0000000000000 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php +++ /dev/null @@ -1,164 +0,0 @@ -get_parent( $request['parent'] ); - $global_styles_config = $this->get_decoded_global_styles_json( $post->post_content ); - - if ( is_wp_error( $global_styles_config ) ) { - return $global_styles_config; - } - - $fields = $this->get_fields_for_response( $request ); - $data = array(); - - if ( ! empty( $global_styles_config['styles'] ) || ! empty( $global_styles_config['settings'] ) ) { - $global_styles_config = ( new WP_Theme_JSON_Gutenberg( $global_styles_config, 'custom' ) )->get_raw_data(); - if ( rest_is_field_included( 'settings', $fields ) ) { - $data['settings'] = ! empty( $global_styles_config['settings'] ) ? $global_styles_config['settings'] : new stdClass(); - } - if ( rest_is_field_included( 'styles', $fields ) ) { - $data['styles'] = ! empty( $global_styles_config['styles'] ) ? $global_styles_config['styles'] : new stdClass(); - } - } - - if ( rest_is_field_included( 'author', $fields ) ) { - $data['author'] = (int) $post->post_author; - } - - if ( rest_is_field_included( 'date', $fields ) ) { - $data['date'] = $this->prepare_date_response( $post->post_date_gmt, $post->post_date ); - } - - if ( rest_is_field_included( 'date_gmt', $fields ) ) { - $data['date_gmt'] = $this->prepare_date_response( $post->post_date_gmt ); - } - - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = (int) $post->ID; - } - - if ( rest_is_field_included( 'modified', $fields ) ) { - $data['modified'] = $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ); - } - - if ( rest_is_field_included( 'modified_gmt', $fields ) ) { - $data['modified_gmt'] = $this->prepare_date_response( $post->post_modified_gmt ); - } - - if ( rest_is_field_included( 'parent', $fields ) ) { - $data['parent'] = (int) $parent->ID; - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - return rest_ensure_response( $data ); - } - - /** - * Retrieves the revision's schema, conforming to JSON Schema. - * - * @since 6.3.0 - * - * @return array Item schema data. - */ - public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); - } - - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => "{$this->parent_post_type}-revision", - 'type' => 'object', - // Base properties for every Revision. - 'properties' => array( - - /* - * Adds settings and styles from the WP_REST_Revisions_Controller item fields. - * Leaves out GUID as global styles shouldn't be accessible via URL. - */ - 'author' => array( - 'description' => __( 'The ID for the author of the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'date' => array( - 'description' => __( "The date the revision was published, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'date_gmt' => array( - 'description' => __( 'The date the revision was published, as GMT.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'id' => array( - 'description' => __( 'Unique identifier for the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'modified' => array( - 'description' => __( "The date the revision was last modified, in the site's timezone.", 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'modified_gmt' => array( - 'description' => __( 'The date the revision was last modified, as GMT.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'date-time', - 'context' => array( 'view', 'edit' ), - ), - 'parent' => array( - 'description' => __( 'The ID for the parent of the revision.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'view', 'edit', 'embed' ), - ), - - // Adds settings and styles from the WP_REST_Global_Styles_Controller parent schema. - 'styles' => array( - 'description' => __( 'Global styles.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - 'settings' => array( - 'description' => __( 'Global settings.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } -} diff --git a/lib/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php b/lib/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php deleted file mode 100644 index ec969519f9ac4..0000000000000 --- a/lib/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php +++ /dev/null @@ -1,75 +0,0 @@ -get_fields_for_response( $request ); - - $response = parent::prepare_item_for_response( $item, $request ); - - if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { - $links = $this->prepare_revision_links( $template ); - $response->add_links( $links ); - if ( ! empty( $links['self']['href'] ) ) { - $actions = $this->get_available_actions(); - $self = $links['self']['href']; - foreach ( $actions as $rel ) { - $response->add_link( $rel, $self ); - } - } - } - - return $response; - } - - /** - * Adds revisions to links. - * - * @param WP_Block_Template $template Template instance. - * @return array Links for the given post. - */ - protected function prepare_revision_links( $template ) { - $links = array(); - - if ( post_type_supports( $this->post_type, 'revisions' ) && (int) $template->wp_id ) { - $revisions = wp_get_latest_revision_id_and_total_count( (int) $template->wp_id ); - $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; - $revisions_base = sprintf( '/%s/%s/%s/revisions', $this->namespace, $this->rest_base, $template->id ); - - $links['version-history'] = array( - 'href' => rest_url( $revisions_base ), - 'count' => $revisions_count, - ); - - if ( $revisions_count > 0 ) { - $links['predecessor-version'] = array( - 'href' => rest_url( $revisions_base . '/' . $revisions['latest_id'] ), - 'id' => $revisions['latest_id'], - ); - } - } - - return $links; - } -} diff --git a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php b/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php deleted file mode 100644 index 556663b881366..0000000000000 --- a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php +++ /dev/null @@ -1,183 +0,0 @@ - $src_url ) { - // Skip if the src doesn't start with the placeholder, as there's nothing to replace. - if ( ! str_starts_with( $src_url, $placeholder ) ) { - continue; - } - - $src_file = str_replace( $placeholder, '', $src_url ); - $src[ $src_key ] = get_theme_file_uri( $src_file ); - } - - return $src; - } - - /** - * Converts all first dimension keys into kebab-case. - * - * @since 6.4.0 - * - * @param array $data The array to process. - * @return array Data with first dimension keys converted into kebab-case. - */ - private static function to_kebab_case( array $data ) { - foreach ( $data as $key => $value ) { - $kebab_case = _wp_to_kebab_case( $key ); - $data[ $kebab_case ] = $value; - if ( $kebab_case !== $key ) { - unset( $data[ $key ] ); - } - } - - return $data; - } - } -} diff --git a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php b/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php deleted file mode 100644 index 6bea6eb86cc71..0000000000000 --- a/lib/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php +++ /dev/null @@ -1,435 +0,0 @@ - '', - 'font-style' => 'normal', - 'font-weight' => '400', - 'font-display' => 'fallback', - ); - - /** - * Valid font-face property names. - * - * @since 6.4.0 - * - * @var string[] - */ - private $valid_font_face_properties = array( - 'ascent-override', - 'descent-override', - 'font-display', - 'font-family', - 'font-stretch', - 'font-style', - 'font-weight', - 'font-variant', - 'font-feature-settings', - 'font-variation-settings', - 'line-gap-override', - 'size-adjust', - 'src', - 'unicode-range', - ); - - /** - * Valid font-display values. - * - * @since 6.4.0 - * - * @var string[] - */ - private $valid_font_display = array( 'auto', 'block', 'fallback', 'swap', 'optional' ); - - /** - * Array of font-face style tag's attribute(s) - * where the key is the attribute name and the - * value is its value. - * - * @since 6.4.0 - * - * @var string[] - */ - private $style_tag_attrs = array(); - - /** - * Creates and initializes an instance of WP_Font_Face. - * - * @since 6.4.0 - */ - public function __construct() { - if ( - function_exists( 'is_admin' ) && ! is_admin() - && - function_exists( 'current_theme_supports' ) && ! current_theme_supports( 'html5', 'style' ) - ) { - $this->style_tag_attrs = array( 'type' => 'text/css' ); - } - } - - /** - * Generates and prints the `@font-face` styles for the given fonts. - * - * @since 6.4.0 - * - * @param array[][] $fonts Optional. The font-families and their font variations. - * See {@see wp_print_font_faces()} for the supported fields. - * Default empty array. - */ - public function generate_and_print( array $fonts ) { - $fonts = $this->validate_fonts( $fonts ); - - // Bail out if there are no fonts are given to process. - if ( empty( $fonts ) ) { - return; - } - - $css = $this->get_css( $fonts ); - - /* - * The font-face CSS is contained within and open a ` inside an HTML comment. - * - STYLE content is raw text. - * - TITLE content is plain text but character references are decoded. - * - TEXTAREA content is plain text but character references are decoded. - * - XMP (deprecated) content is raw text. - * - * ### Modifying HTML attributes for a found tag - * - * Once you've found the start of an opening tag you can modify - * any number of the attributes on that tag. You can set a new - * value for an attribute, remove the entire attribute, or do - * nothing and move on to the next opening tag. - * - * Example: - * - * if ( $tags->next_tag( array( 'class_name' => 'wp-group-block' ) ) ) { - * $tags->set_attribute( 'title', 'This groups the contained content.' ); - * $tags->remove_attribute( 'data-test-id' ); - * } - * - * If `set_attribute()` is called for an existing attribute it will - * overwrite the existing value. Similarly, calling `remove_attribute()` - * for a non-existing attribute has no effect on the document. Both - * of these methods are safe to call without knowing if a given attribute - * exists beforehand. - * - * ### Modifying CSS classes for a found tag - * - * The tag processor treats the `class` attribute as a special case. - * Because it's a common operation to add or remove CSS classes, this - * interface adds helper methods to make that easier. - * - * As with attribute values, adding or removing CSS classes is a safe - * operation that doesn't require checking if the attribute or class - * exists before making changes. If removing the only class then the - * entire `class` attribute will be removed. - * - * Example: - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `Yippee!` - * // to `Yippee!` - * $tags->add_class( 'is-active' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * // from `` - * // to ` - * $tags->remove_class( 'rugby' ); - * - * When class changes are enqueued but a direct change to `class` is made via - * `set_attribute` then the changes to `set_attribute` (or `remove_attribute`) - * will take precedence over those made through `add_class` and `remove_class`. - * - * ### Bookmarks - * - * While scanning through the input HTMl document it's possible to set - * a named bookmark when a particular tag is found. Later on, after - * continuing to scan other tags, it's possible to `seek` to one of - * the set bookmarks and then proceed again from that point forward. - * - * Because bookmarks create processing overhead one should avoid - * creating too many of them. As a rule, create only bookmarks - * of known string literal names; avoid creating "mark_{$index}" - * and so on. It's fine from a performance standpoint to create a - * bookmark and update it frequently, such as within a loop. - * - * $total_todos = 0; - * while ( $p->next_tag( array( 'tag_name' => 'UL', 'class_name' => 'todo' ) ) ) { - * $p->set_bookmark( 'list-start' ); - * while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - * if ( 'UL' === $p->get_tag() && $p->is_tag_closer() ) { - * $p->set_bookmark( 'list-end' ); - * $p->seek( 'list-start' ); - * $p->set_attribute( 'data-contained-todos', (string) $total_todos ); - * $total_todos = 0; - * $p->seek( 'list-end' ); - * break; - * } - * - * if ( 'LI' === $p->get_tag() && ! $p->is_tag_closer() ) { - * $total_todos++; - * } - * } - * } - * - * ## Tokens and finer-grained processing. - * - * It's possible to scan through every lexical token in the - * HTML document using the `next_token()` function. This - * alternative form takes no argument and provides no built-in - * query syntax. - * - * Example: - * - * $title = '(untitled)'; - * $text = ''; - * while ( $processor->next_token() ) { - * switch ( $processor->get_token_name() ) { - * case '#text': - * $text .= $processor->get_modifiable_text(); - * break; - * - * case 'BR': - * $text .= "\n"; - * break; - * - * case 'TITLE': - * $title = $processor->get_modifiable_text(); - * break; - * } - * } - * return trim( "# {$title}\n\n{$text}" ); - * - * ### Tokens and _modifiable text_. - * - * #### Special "atomic" HTML elements. - * - * Not all HTML elements are able to contain other elements inside of them. - * For instance, the contents inside a TITLE element are plaintext (except - * that character references like & will be decoded). This means that - * if the string `` appears inside a TITLE element, then it's not an - * image tag, but rather it's text describing an image tag. Likewise, the - * contents of a SCRIPT or STYLE element are handled entirely separately in - * a browser than the contents of other elements because they represent a - * different language than HTML. - * - * For these elements the Tag Processor treats the entire sequence as one, - * from the opening tag, including its contents, through its closing tag. - * This means that the it's not possible to match the closing tag for a - * SCRIPT element unless it's unexpected; the Tag Processor already matched - * it when it found the opening tag. - * - * The inner contents of these elements are that element's _modifiable text_. - * - * The special elements are: - * - `SCRIPT` whose contents are treated as raw plaintext but supports a legacy - * style of including Javascript inside of HTML comments to avoid accidentally - * closing the SCRIPT from inside a Javascript string. E.g. `console.log( '' )`. - * - `TITLE` and `TEXTAREA` whose contents are treated as plaintext and then any - * character references are decoded. E.g. `1 < 2 < 3` becomes `1 < 2 < 3`. - * - `IFRAME`, `NOSCRIPT`, `NOEMBED`, `NOFRAME`, `STYLE` whose contents are treated as - * raw plaintext and left as-is. E.g. `1 < 2 < 3` remains `1 < 2 < 3`. - * - * #### Other tokens with modifiable text. - * - * There are also non-elements which are void/self-closing in nature and contain - * modifiable text that is part of that individual syntax token itself. - * - * - `#text` nodes, whose entire token _is_ the modifiable text. - * - HTML comments and tokens that become comments due to some syntax error. The - * text for these tokens is the portion of the comment inside of the syntax. - * E.g. for `` the text is `" comment "` (note the spaces are included). - * - `CDATA` sections, whose text is the content inside of the section itself. E.g. for - * `` the text is `"some content"` (with restrictions [1]). - * - "Funky comments," which are a special case of invalid closing tags whose name is - * invalid. The text for these nodes is the text that a browser would transform into - * an HTML comment when parsing. E.g. for `` the text is `%post_author`. - * - `DOCTYPE` declarations like `` which have no closing tag. - * - XML Processing instruction nodes like `` (with restrictions [2]). - * - The empty end tag `` which is ignored in the browser and DOM. - * - * [1]: There are no CDATA sections in HTML. When encountering `` becomes a bogus HTML comment, meaning there can be no CDATA - * section in an HTML document containing `>`. The Tag Processor will first find - * all valid and bogus HTML comments, and then if the comment _would_ have been a - * CDATA section _were they to exist_, it will indicate this as the type of comment. - * - * [2]: XML allows a broader range of characters in a processing instruction's target name - * and disallows "xml" as a name, since it's special. The Tag Processor only recognizes - * target names with an ASCII-representable subset of characters. It also exhibits the - * same constraint as with CDATA sections, in that `>` cannot exist within the token - * since Processing Instructions do no exist within HTML and their syntax transforms - * into a bogus comment in the DOM. - * - * ## Design and limitations - * - * The Tag Processor is designed to linearly scan HTML documents and tokenize - * HTML tags and their attributes. It's designed to do this as efficiently as - * possible without compromising parsing integrity. Therefore it will be - * slower than some methods of modifying HTML, such as those incorporating - * over-simplified PCRE patterns, but will not introduce the defects and - * failures that those methods bring in, which lead to broken page renders - * and often to security vulnerabilities. On the other hand, it will be faster - * than full-blown HTML parsers such as DOMDocument and use considerably - * less memory. It requires a negligible memory overhead, enough to consider - * it a zero-overhead system. - * - * The performance characteristics are maintained by avoiding tree construction - * and semantic cleanups which are specified in HTML5. Because of this, for - * example, it's not possible for the Tag Processor to associate any given - * opening tag with its corresponding closing tag, or to return the inner markup - * inside an element. Systems may be built on top of the Tag Processor to do - * this, but the Tag Processor is and should be constrained so it can remain an - * efficient, low-level, and reliable HTML scanner. - * - * The Tag Processor's design incorporates a "garbage-in-garbage-out" philosophy. - * HTML5 specifies that certain invalid content be transformed into different forms - * for display, such as removing null bytes from an input document and replacing - * invalid characters with the Unicode replacement character `U+FFFD` (visually "�"). - * Where errors or transformations exist within the HTML5 specification, the Tag Processor - * leaves those invalid inputs untouched, passing them through to the final browser - * to handle. While this implies that certain operations will be non-spec-compliant, - * such as reading the value of an attribute with invalid content, it also preserves a - * simplicity and efficiency for handling those error cases. - * - * Most operations within the Tag Processor are designed to minimize the difference - * between an input and output document for any given change. For example, the - * `add_class` and `remove_class` methods preserve whitespace and the class ordering - * within the `class` attribute; and when encountering tags with duplicated attributes, - * the Tag Processor will leave those invalid duplicate attributes where they are but - * update the proper attribute which the browser will read for parsing its value. An - * exception to this rule is that all attribute updates store their values as - * double-quoted strings, meaning that attributes on input with single-quoted or - * unquoted values will appear in the output with double-quotes. - * - * ### Scripting Flag - * - * The Tag Processor parses HTML with the "scripting flag" disabled. This means - * that it doesn't run any scripts while parsing the page. In a browser with - * JavaScript enabled, for example, the script can change the parse of the - * document as it loads. On the server, however, evaluating JavaScript is not - * only impractical, but also unwanted. - * - * Practically this means that the Tag Processor will descend into NOSCRIPT - * elements and process its child tags. Were the scripting flag enabled, such - * as in a typical browser, the contents of NOSCRIPT are skipped entirely. - * - * This allows the HTML API to process the content that will be presented in - * a browser when scripting is disabled, but it offers a different view of a - * page than most browser sessions will experience. E.g. the tags inside the - * NOSCRIPT disappear. - * - * ### Text Encoding - * - * The Tag Processor assumes that the input HTML document is encoded with a - * text encoding compatible with 7-bit ASCII's '<', '>', '&', ';', '/', '=', - * "'", '"', 'a' - 'z', 'A' - 'Z', and the whitespace characters ' ', tab, - * carriage-return, newline, and form-feed. - * - * In practice, this includes almost every single-byte encoding as well as - * UTF-8. Notably, however, it does not include UTF-16. If providing input - * that's incompatible, then convert the encoding beforehand. - * - * @since 6.2.0 - * @since 6.2.1 Fix: Support for various invalid comments; attribute updates are case-insensitive. - * @since 6.3.2 Fix: Skip HTML-like content inside rawtext elements such as STYLE. - * @since 6.5.0 Pauses processor when input ends in an incomplete syntax token. - * Introduces "special" elements which act like void elements, e.g. TITLE, STYLE. - * Allows scanning through all tokens and processing modifiable text, where applicable. - */ -class Gutenberg_HTML_Tag_Processor_6_5 { - /** - * The maximum number of bookmarks allowed to exist at - * any given time. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::set_bookmark() - */ - const MAX_BOOKMARKS = 10; - - /** - * Maximum number of times seek() can be called. - * Prevents accidental infinite loops. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::seek() - */ - const MAX_SEEK_OPS = 1000; - - /** - * The HTML document to parse. - * - * @since 6.2.0 - * @var string - */ - protected $html; - - /** - * The last query passed to next_tag(). - * - * @since 6.2.0 - * @var array|null - */ - private $last_query; - - /** - * The tag name this processor currently scans for. - * - * @since 6.2.0 - * @var string|null - */ - private $sought_tag_name; - - /** - * The CSS class name this processor currently scans for. - * - * @since 6.2.0 - * @var string|null - */ - private $sought_class_name; - - /** - * The match offset this processor currently scans for. - * - * @since 6.2.0 - * @var int|null - */ - private $sought_match_offset; - - /** - * Whether to visit tag closers, e.g. , when walking an input document. - * - * @since 6.2.0 - * @var bool - */ - private $stop_on_tag_closers; - - /** - * Specifies mode of operation of the parser at any given time. - * - * | State | Meaning | - * | ----------------|----------------------------------------------------------------------| - * | *Ready* | The parser is ready to run. | - * | *Complete* | There is nothing left to parse. | - * | *Incomplete* | The HTML ended in the middle of a token; nothing more can be parsed. | - * | *Matched tag* | Found an HTML tag; it's possible to modify its attributes. | - * | *Text node* | Found a #text node; this is plaintext and modifiable. | - * | *CDATA node* | Found a CDATA section; this is modifiable. | - * | *Comment* | Found a comment or bogus comment; this is modifiable. | - * | *Presumptuous* | Found an empty tag closer: ``. | - * | *Funky comment* | Found a tag closer with an invalid tag name; this is modifiable. | - * - * @since 6.5.0 - * - * @see WP_HTML_Tag_Processor::STATE_READY - * @see WP_HTML_Tag_Processor::STATE_COMPLETE - * @see WP_HTML_Tag_Processor::STATE_INCOMPLETE_INPUT - * @see WP_HTML_Tag_Processor::STATE_MATCHED_TAG - * @see WP_HTML_Tag_Processor::STATE_TEXT_NODE - * @see WP_HTML_Tag_Processor::STATE_CDATA_NODE - * @see WP_HTML_Tag_Processor::STATE_COMMENT - * @see WP_HTML_Tag_Processor::STATE_DOCTYPE - * @see WP_HTML_Tag_Processor::STATE_PRESUMPTUOUS_TAG - * @see WP_HTML_Tag_Processor::STATE_FUNKY_COMMENT - * - * @var string - */ - protected $parser_state = self::STATE_READY; - - /** - * What kind of syntax token became an HTML comment. - * - * Since there are many ways in which HTML syntax can create an HTML comment, - * this indicates which of those caused it. This allows the Tag Processor to - * represent more from the original input document than would appear in the DOM. - * - * @since 6.5.0 - * - * @var string|null - */ - protected $comment_type = null; - - /** - * How many bytes from the original HTML document have been read and parsed. - * - * This value points to the latest byte offset in the input document which - * has been already parsed. It is the internal cursor for the Tag Processor - * and updates while scanning through the HTML tokens. - * - * @since 6.2.0 - * @var int - */ - private $bytes_already_parsed = 0; - - /** - * Byte offset in input document where current token starts. - * - * Example: - * - *
... - * 01234 - * - token starts at 0 - * - * @since 6.5.0 - * - * @var int|null - */ - private $token_starts_at; - - /** - * Byte length of current token. - * - * Example: - * - *
... - * 012345678901234 - * - token length is 14 - 0 = 14 - * - * a is a token. - * 0123456789 123456789 123456789 - * - token length is 17 - 2 = 15 - * - * @since 6.5.0 - * - * @var int|null - */ - private $token_length; - - /** - * Byte offset in input document where current tag name starts. - * - * Example: - * - *
... - * 01234 - * - tag name starts at 1 - * - * @since 6.2.0 - * - * @var int|null - */ - private $tag_name_starts_at; - - /** - * Byte length of current tag name. - * - * Example: - * - *
... - * 01234 - * --- tag name length is 3 - * - * @since 6.2.0 - * - * @var int|null - */ - private $tag_name_length; - - /** - * Byte offset into input document where current modifiable text starts. - * - * @since 6.5.0 - * - * @var int - */ - private $text_starts_at; - - /** - * Byte length of modifiable text. - * - * @since 6.5.0 - * - * @var string - */ - private $text_length; - - /** - * Whether the current tag is an opening tag, e.g.
, or a closing tag, e.g.
. - * - * @var bool - */ - private $is_closing_tag; - - /** - * Lazily-built index of attributes found within an HTML tag, keyed by the attribute name. - * - * Example: - * - * // Supposing the parser is working through this content - * // and stops after recognizing the `id` attribute. - * //
- * // ^ parsing will continue from this point. - * $this->attributes = array( - * 'id' => new WP_HTML_Attribute_Token( 'id', 9, 6, 5, 11, false ) - * ); - * - * // When picking up parsing again, or when asking to find the - * // `class` attribute we will continue and add to this array. - * $this->attributes = array( - * 'id' => new WP_HTML_Attribute_Token( 'id', 9, 6, 5, 11, false ), - * 'class' => new WP_HTML_Attribute_Token( 'class', 23, 7, 17, 13, false ) - * ); - * - * // Note that only the `class` attribute value is stored in the index. - * // That's because it is the only value used by this class at the moment. - * - * @since 6.2.0 - * @var WP_HTML_Attribute_Token[] - */ - private $attributes = array(); - - /** - * Tracks spans of duplicate attributes on a given tag, used for removing - * all copies of an attribute when calling `remove_attribute()`. - * - * @since 6.3.2 - * - * @var (WP_HTML_Span[])[]|null - */ - private $duplicate_attributes = null; - - /** - * Which class names to add or remove from a tag. - * - * These are tracked separately from attribute updates because they are - * semantically distinct, whereas this interface exists for the common - * case of adding and removing class names while other attributes are - * generally modified as with DOM `setAttribute` calls. - * - * When modifying an HTML document these will eventually be collapsed - * into a single `set_attribute( 'class', $changes )` call. - * - * Example: - * - * // Add the `wp-block-group` class, remove the `wp-group` class. - * $classname_updates = array( - * // Indexed by a comparable class name. - * 'wp-block-group' => WP_HTML_Tag_Processor::ADD_CLASS, - * 'wp-group' => WP_HTML_Tag_Processor::REMOVE_CLASS - * ); - * - * @since 6.2.0 - * @var bool[] - */ - private $classname_updates = array(); - - /** - * Tracks a semantic location in the original HTML which - * shifts with updates as they are applied to the document. - * - * @since 6.2.0 - * @var WP_HTML_Span[] - */ - protected $bookmarks = array(); - - const ADD_CLASS = true; - const REMOVE_CLASS = false; - const SKIP_CLASS = null; - - /** - * Lexical replacements to apply to input HTML document. - * - * "Lexical" in this class refers to the part of this class which - * operates on pure text _as text_ and not as HTML. There's a line - * between the public interface, with HTML-semantic methods like - * `set_attribute` and `add_class`, and an internal state that tracks - * text offsets in the input document. - * - * When higher-level HTML methods are called, those have to transform their - * operations (such as setting an attribute's value) into text diffing - * operations (such as replacing the sub-string from indices A to B with - * some given new string). These text-diffing operations are the lexical - * updates. - * - * As new higher-level methods are added they need to collapse their - * operations into these lower-level lexical updates since that's the - * Tag Processor's internal language of change. Any code which creates - * these lexical updates must ensure that they do not cross HTML syntax - * boundaries, however, so these should never be exposed outside of this - * class or any classes which intentionally expand its functionality. - * - * These are enqueued while editing the document instead of being immediately - * applied to avoid processing overhead, string allocations, and string - * copies when applying many updates to a single document. - * - * Example: - * - * // Replace an attribute stored with a new value, indices - * // sourced from the lazily-parsed HTML recognizer. - * $start = $attributes['src']->start; - * $length = $attributes['src']->length; - * $modifications[] = new WP_HTML_Text_Replacement( $start, $length, $new_value ); - * - * // Correspondingly, something like this will appear in this array. - * $lexical_updates = array( - * WP_HTML_Text_Replacement( 14, 28, 'https://my-site.my-domain/wp-content/uploads/2014/08/kittens.jpg' ) - * ); - * - * @since 6.2.0 - * @var WP_HTML_Text_Replacement[] - */ - protected $lexical_updates = array(); - - /** - * Tracks and limits `seek()` calls to prevent accidental infinite loops. - * - * @since 6.2.0 - * @var int - * - * @see WP_HTML_Tag_Processor::seek() - */ - protected $seek_count = 0; - - /** - * Constructor. - * - * @since 6.2.0 - * - * @param string $html HTML to process. - */ - public function __construct( $html ) { - $this->html = $html; - } - - /** - * Finds the next tag matching the $query. - * - * @since 6.2.0 - * @since 6.5.0 No longer processes incomplete tokens at end of document; pauses the processor at start of token. - * - * @param array|string|null $query { - * Optional. Which tag name to find, having which class, etc. Default is to find any tag. - * - * @type string|null $tag_name Which tag to find, or `null` for "any tag." - * @type int|null $match_offset Find the Nth tag matching all search criteria. - * 1 for "first" tag, 3 for "third," etc. - * Defaults to first tag. - * @type string|null $class_name Tag must contain this whole class name to match. - * @type string|null $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. - * } - * @return bool Whether a tag was matched. - */ - public function next_tag( $query = null ) { - $this->parse_query( $query ); - $already_found = 0; - - do { - if ( false === $this->next_token() ) { - return false; - } - - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - continue; - } - - if ( $this->matches() ) { - ++$already_found; - } - } while ( $already_found < $this->sought_match_offset ); - - return true; - } - - /** - * Finds the next token in the HTML document. - * - * An HTML document can be viewed as a stream of tokens, - * where tokens are things like HTML tags, HTML comments, - * text nodes, etc. This method finds the next token in - * the HTML document and returns whether it found one. - * - * If it starts parsing a token and reaches the end of the - * document then it will seek to the start of the last - * token and pause, returning `false` to indicate that it - * failed to find a complete token. - * - * Possible token types, based on the HTML specification: - * - * - an HTML tag, whether opening, closing, or void. - * - a text node - the plaintext inside tags. - * - an HTML comment. - * - a DOCTYPE declaration. - * - a processing instruction, e.g. ``. - * - * The Tag Processor currently only supports the tag token. - * - * @since 6.5.0 - * - * @return bool Whether a token was parsed. - */ - public function next_token() { - return $this->base_class_next_token(); - } - - /** - * Internal method which finds the next token in the HTML document. - * - * This method is a protected internal function which implements the logic for - * finding the next token in a document. It exists so that the parser can update - * its state without affecting the location of the cursor in the document and - * without triggering subclass methods for things like `next_token()`, e.g. when - * applying patches before searching for the next token. - * - * @since 6.5.0 - * - * @access private - * - * @return bool Whether a token was parsed. - */ - private function base_class_next_token() { - $was_at = $this->bytes_already_parsed; - $this->after_tag(); - - // Don't proceed if there's nothing more to scan. - if ( - self::STATE_COMPLETE === $this->parser_state || - self::STATE_INCOMPLETE_INPUT === $this->parser_state - ) { - return false; - } - - /* - * The next step in the parsing loop determines the parsing state; - * clear it so that state doesn't linger from the previous step. - */ - $this->parser_state = self::STATE_READY; - - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_COMPLETE; - return false; - } - - // Find the next tag if it exists. - if ( false === $this->parse_next_tag() ) { - if ( self::STATE_INCOMPLETE_INPUT === $this->parser_state ) { - $this->bytes_already_parsed = $was_at; - } - - return false; - } - - /* - * For legacy reasons the rest of this function handles tags and their - * attributes. If the processor has reached the end of the document - * or if it matched any other token then it should return here to avoid - * attempting to process tag-specific syntax. - */ - if ( - self::STATE_INCOMPLETE_INPUT !== $this->parser_state && - self::STATE_COMPLETE !== $this->parser_state && - self::STATE_MATCHED_TAG !== $this->parser_state - ) { - return true; - } - - // Parse all of its attributes. - while ( $this->parse_next_attribute() ) { - continue; - } - - // Ensure that the tag closes before the end of the document. - if ( - self::STATE_INCOMPLETE_INPUT === $this->parser_state || - $this->bytes_already_parsed >= strlen( $this->html ) - ) { - // Does this appropriately clear state (parsed attributes)? - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - $this->bytes_already_parsed = $was_at; - - return false; - } - - $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); - if ( false === $tag_ends_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - $this->bytes_already_parsed = $was_at; - - return false; - } - $this->parser_state = self::STATE_MATCHED_TAG; - $this->token_length = $tag_ends_at - $this->token_starts_at; - $this->bytes_already_parsed = $tag_ends_at + 1; - - /* - * For non-DATA sections which might contain text that looks like HTML tags but - * isn't, scan with the appropriate alternative mode. Looking at the first letter - * of the tag name as a pre-check avoids a string allocation when it's not needed. - */ - $t = $this->html[ $this->tag_name_starts_at ]; - if ( - $this->is_closing_tag || - ! ( - 'i' === $t || 'I' === $t || - 'n' === $t || 'N' === $t || - 's' === $t || 'S' === $t || - 't' === $t || 'T' === $t || - 'x' === $t || 'X' === $t - ) - ) { - return true; - } - - $tag_name = $this->get_tag(); - - /* - * Preserve the opening tag pointers, as these will be overwritten - * when finding the closing tag. They will be reset after finding - * the closing to tag to point to the opening of the special atomic - * tag sequence. - */ - $tag_name_starts_at = $this->tag_name_starts_at; - $tag_name_length = $this->tag_name_length; - $tag_ends_at = $this->token_starts_at + $this->token_length; - $attributes = $this->attributes; - $duplicate_attributes = $this->duplicate_attributes; - - // Find the closing tag if necessary. - $found_closer = false; - switch ( $tag_name ) { - case 'SCRIPT': - $found_closer = $this->skip_script_data(); - break; - - case 'TEXTAREA': - case 'TITLE': - $found_closer = $this->skip_rcdata( $tag_name ); - break; - - /* - * In the browser this list would include the NOSCRIPT element, - * but the Tag Processor is an environment with the scripting - * flag disabled, meaning that it needs to descend into the - * NOSCRIPT element to be able to properly process what will be - * sent to a browser. - * - * Note that this rule makes HTML5 syntax incompatible with XML, - * because the parsing of this token depends on client application. - * The NOSCRIPT element cannot be represented in the XHTML syntax. - */ - case 'IFRAME': - case 'NOEMBED': - case 'NOFRAMES': - case 'STYLE': - case 'XMP': - $found_closer = $this->skip_rawtext( $tag_name ); - break; - - // No other tags should be treated in their entirety here. - default: - return true; - } - - if ( ! $found_closer ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - $this->bytes_already_parsed = $was_at; - return false; - } - - /* - * The values here look like they reference the opening tag but they reference - * the closing tag instead. This is why the opening tag values were stored - * above in a variable. It reads confusingly here, but that's because the - * functions that skip the contents have moved all the internal cursors past - * the inner content of the tag. - */ - $this->token_starts_at = $was_at; - $this->token_length = $this->bytes_already_parsed - $this->token_starts_at; - $this->text_starts_at = $tag_ends_at + 1; - $this->text_length = $this->tag_name_starts_at - $this->text_starts_at; - $this->tag_name_starts_at = $tag_name_starts_at; - $this->tag_name_length = $tag_name_length; - $this->attributes = $attributes; - $this->duplicate_attributes = $duplicate_attributes; - - return true; - } - - /** - * Whether the processor paused because the input HTML document ended - * in the middle of a syntax element, such as in the middle of a tag. - * - * Example: - * - * $processor = new WP_HTML_Tag_Processor( '" ); - * $p->next_tag(); - * foreach ( $p->class_list() as $class_name ) { - * echo "{$class_name} "; - * } - * // Outputs: "free lang-en " - * - * @since 6.4.0 - */ - public function class_list() { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return; - } - - /** @var string $class contains the string value of the class attribute, with character references decoded. */ - $class = $this->get_attribute( 'class' ); - - if ( ! is_string( $class ) ) { - return; - } - - $seen = array(); - - $at = 0; - while ( $at < strlen( $class ) ) { - // Skip past any initial boundary characters. - $at += strspn( $class, " \t\f\r\n", $at ); - if ( $at >= strlen( $class ) ) { - return; - } - - // Find the byte length until the next boundary. - $length = strcspn( $class, " \t\f\r\n", $at ); - if ( 0 === $length ) { - return; - } - - /* - * CSS class names are case-insensitive in the ASCII range. - * - * @see https://www.w3.org/TR/CSS2/syndata.html#x1 - */ - $name = strtolower( substr( $class, $at, $length ) ); - $at += $length; - - /* - * It's expected that the number of class names for a given tag is relatively small. - * Given this, it is probably faster overall to scan an array for a value rather - * than to use the class name as a key and check if it's a key of $seen. - */ - if ( in_array( $name, $seen, true ) ) { - continue; - } - - $seen[] = $name; - yield $name; - } - } - - - /** - * Returns if a matched tag contains the given ASCII case-insensitive class name. - * - * @since 6.4.0 - * - * @param string $wanted_class Look for this CSS class name, ASCII case-insensitive. - * @return bool|null Whether the matched tag contains the given class name, or null if not matched. - */ - public function has_class( $wanted_class ) { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return null; - } - - $wanted_class = strtolower( $wanted_class ); - - foreach ( $this->class_list() as $class_name ) { - if ( $class_name === $wanted_class ) { - return true; - } - } - - return false; - } - - - /** - * Sets a bookmark in the HTML document. - * - * Bookmarks represent specific places or tokens in the HTML - * document, such as a tag opener or closer. When applying - * edits to a document, such as setting an attribute, the - * text offsets of that token may shift; the bookmark is - * kept updated with those shifts and remains stable unless - * the entire span of text in which the token sits is removed. - * - * Release bookmarks when they are no longer needed. - * - * Example: - * - *

Surprising fact you may not know!

- * ^ ^ - * \-|-- this `H2` opener bookmark tracks the token - * - *

Surprising fact you may no… - * ^ ^ - * \-|-- it shifts with edits - * - * Bookmarks provide the ability to seek to a previously-scanned - * place in the HTML document. This avoids the need to re-scan - * the entire document. - * - * Example: - * - *
  • One
  • Two
  • Three
- * ^^^^ - * want to note this last item - * - * $p = new WP_HTML_Tag_Processor( $html ); - * $in_list = false; - * while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) { - * if ( 'UL' === $p->get_tag() ) { - * if ( $p->is_tag_closer() ) { - * $in_list = false; - * $p->set_bookmark( 'resume' ); - * if ( $p->seek( 'last-li' ) ) { - * $p->add_class( 'last-li' ); - * } - * $p->seek( 'resume' ); - * $p->release_bookmark( 'last-li' ); - * $p->release_bookmark( 'resume' ); - * } else { - * $in_list = true; - * } - * } - * - * if ( 'LI' === $p->get_tag() ) { - * $p->set_bookmark( 'last-li' ); - * } - * } - * - * Bookmarks intentionally hide the internal string offsets - * to which they refer. They are maintained internally as - * updates are applied to the HTML document and therefore - * retain their "position" - the location to which they - * originally pointed. The inability to use bookmarks with - * functions like `substr` is therefore intentional to guard - * against accidentally breaking the HTML. - * - * Because bookmarks allocate memory and require processing - * for every applied update, they are limited and require - * a name. They should not be created with programmatically-made - * names, such as "li_{$index}" with some loop. As a general - * rule they should only be created with string-literal names - * like "start-of-section" or "last-paragraph". - * - * Bookmarks are a powerful tool to enable complicated behavior. - * Consider double-checking that you need this tool if you are - * reaching for it, as inappropriate use could lead to broken - * HTML structure or unwanted processing overhead. - * - * @since 6.2.0 - * - * @param string $name Identifies this particular bookmark. - * @return bool Whether the bookmark was successfully created. - */ - public function set_bookmark( $name ) { - // It only makes sense to set a bookmark if the parser has paused on a concrete token. - if ( - self::STATE_COMPLETE === $this->parser_state || - self::STATE_INCOMPLETE_INPUT === $this->parser_state - ) { - return false; - } - - if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= static::MAX_BOOKMARKS ) { - _doing_it_wrong( - __METHOD__, - __( 'Too many bookmarks: cannot create any more.' ), - '6.2.0' - ); - return false; - } - - $this->bookmarks[ $name ] = new Gutenberg_HTML_Span_6_5( $this->token_starts_at, $this->token_length ); - - return true; - } - - - /** - * Removes a bookmark that is no longer needed. - * - * Releasing a bookmark frees up the small - * performance overhead it requires. - * - * @param string $name Name of the bookmark to remove. - * @return bool Whether the bookmark already existed before removal. - */ - public function release_bookmark( $name ) { - if ( ! array_key_exists( $name, $this->bookmarks ) ) { - return false; - } - - unset( $this->bookmarks[ $name ] ); - - return true; - } - - /** - * Skips contents of generic rawtext elements. - * - * @since 6.3.2 - * - * @see https://html.spec.whatwg.org/#generic-raw-text-element-parsing-algorithm - * - * @param string $tag_name The uppercase tag name which will close the RAWTEXT region. - * @return bool Whether an end to the RAWTEXT region was found before the end of the document. - */ - private function skip_rawtext( $tag_name ) { - /* - * These two functions distinguish themselves on whether character references are - * decoded, and since functionality to read the inner markup isn't supported, it's - * not necessary to implement these two functions separately. - */ - return $this->skip_rcdata( $tag_name ); - } - - /** - * Skips contents of RCDATA elements, namely title and textarea tags. - * - * @since 6.2.0 - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state - * - * @param string $tag_name The uppercase tag name which will close the RCDATA region. - * @return bool Whether an end to the RCDATA region was found before the end of the document. - */ - private function skip_rcdata( $tag_name ) { - $html = $this->html; - $doc_length = strlen( $html ); - $tag_length = strlen( $tag_name ); - - $at = $this->bytes_already_parsed; - - while ( false !== $at && $at < $doc_length ) { - $at = strpos( $this->html, 'tag_name_starts_at = $at; - - // Fail if there is no possible tag closer. - if ( false === $at || ( $at + $tag_length ) >= $doc_length ) { - return false; - } - - $at += 2; - - /* - * Find a case-insensitive match to the tag name. - * - * Because tag names are limited to US-ASCII there is no - * need to perform any kind of Unicode normalization when - * comparing; any character which could be impacted by such - * normalization could not be part of a tag name. - */ - for ( $i = 0; $i < $tag_length; $i++ ) { - $tag_char = $tag_name[ $i ]; - $html_char = $html[ $at + $i ]; - - if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { - $at += $i; - continue 2; - } - } - - $at += $tag_length; - $this->bytes_already_parsed = $at; - - if ( $at >= strlen( $html ) ) { - return false; - } - - /* - * Ensure that the tag name terminates to avoid matching on - * substrings of a longer tag name. For example, the sequence - * "' !== $c ) { - continue; - } - - while ( $this->parse_next_attribute() ) { - continue; - } - - $at = $this->bytes_already_parsed; - if ( $at >= strlen( $this->html ) ) { - return false; - } - - if ( '>' === $html[ $at ] ) { - $this->bytes_already_parsed = $at + 1; - return true; - } - - if ( $at + 1 >= strlen( $this->html ) ) { - return false; - } - - if ( '/' === $html[ $at ] && '>' === $html[ $at + 1 ] ) { - $this->bytes_already_parsed = $at + 2; - return true; - } - } - - return false; - } - - /** - * Skips contents of script tags. - * - * @since 6.2.0 - * - * @return bool Whether the script tag was closed before the end of the document. - */ - private function skip_script_data() { - $state = 'unescaped'; - $html = $this->html; - $doc_length = strlen( $html ); - $at = $this->bytes_already_parsed; - - while ( false !== $at && $at < $doc_length ) { - $at += strcspn( $html, '-<', $at ); - - /* - * For all script states a "-->" transitions - * back into the normal unescaped script mode, - * even if that's the current state. - */ - if ( - $at + 2 < $doc_length && - '-' === $html[ $at ] && - '-' === $html[ $at + 1 ] && - '>' === $html[ $at + 2 ] - ) { - $at += 3; - $state = 'unescaped'; - continue; - } - - // Everything of interest past here starts with "<". - if ( $at + 1 >= $doc_length || '<' !== $html[ $at++ ] ) { - continue; - } - - /* - * Unlike with "-->", the "`. Unlike other comment - * and bogus comment syntax, these leave no clear insertion point for text and - * they need to be modified specially in order to contain text. E.g. to store - * `?` as the modifiable text, the `` needs to become ``, which - * involves inserting an additional `-` into the token after the modifiable text. - */ - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT; - $this->token_length = $closer_at + $span_of_dashes + 1 - $this->token_starts_at; - - // Only provide modifiable text if the token is long enough to contain it. - if ( $span_of_dashes >= 2 ) { - $this->comment_type = self::COMMENT_AS_HTML_COMMENT; - $this->text_starts_at = $this->token_starts_at + 4; - $this->text_length = $span_of_dashes - 2; - } - - $this->bytes_already_parsed = $closer_at + $span_of_dashes + 1; - return true; - } - - /* - * Comments may be closed by either a --> or an invalid --!>. - * The first occurrence closes the comment. - * - * See https://html.spec.whatwg.org/#parse-error-incorrectly-closed-comment - */ - --$closer_at; // Pre-increment inside condition below reduces risk of accidental infinite looping. - while ( ++$closer_at < $doc_length ) { - $closer_at = strpos( $html, '--', $closer_at ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - if ( $closer_at + 2 < $doc_length && '>' === $html[ $closer_at + 2 ] ) { - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_HTML_COMMENT; - $this->token_length = $closer_at + 3 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 4; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 3; - return true; - } - - if ( - $closer_at + 3 < $doc_length && - '!' === $html[ $closer_at + 2 ] && - '>' === $html[ $closer_at + 3 ] - ) { - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_HTML_COMMENT; - $this->token_length = $closer_at + 4 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 4; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 4; - return true; - } - } - } - - /* - * ` - * These are ASCII-case-insensitive. - * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state - */ - if ( - $doc_length > $at + 8 && - ( 'D' === $html[ $at + 2 ] || 'd' === $html[ $at + 2 ] ) && - ( 'O' === $html[ $at + 3 ] || 'o' === $html[ $at + 3 ] ) && - ( 'C' === $html[ $at + 4 ] || 'c' === $html[ $at + 4 ] ) && - ( 'T' === $html[ $at + 5 ] || 't' === $html[ $at + 5 ] ) && - ( 'Y' === $html[ $at + 6 ] || 'y' === $html[ $at + 6 ] ) && - ( 'P' === $html[ $at + 7 ] || 'p' === $html[ $at + 7 ] ) && - ( 'E' === $html[ $at + 8 ] || 'e' === $html[ $at + 8 ] ) - ) { - $closer_at = strpos( $html, '>', $at + 9 ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->parser_state = self::STATE_DOCTYPE; - $this->token_length = $closer_at + 1 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 9; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 1; - return true; - } - - /* - * Anything else here is an incorrectly-opened comment and transitions - * to the bogus comment state - skip to the nearest >. If no closer is - * found then the HTML was truncated inside the markup declaration. - */ - $closer_at = strpos( $html, '>', $at + 1 ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_INVALID_HTML; - $this->token_length = $closer_at + 1 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 2; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 1; - - /* - * Identify nodes that would be CDATA if HTML had CDATA sections. - * - * This section must occur after identifying the bogus comment end - * because in an HTML parser it will span to the nearest `>`, even - * if there's no `]]>` as would be required in an XML document. It - * is therefore not possible to parse a CDATA section containing - * a `>` in the HTML syntax. - * - * Inside foreign elements there is a discrepancy between browsers - * and the specification on this. - * - * @todo Track whether the Tag Processor is inside a foreign element - * and require the proper closing `]]>` in those cases. - */ - if ( - $this->token_length >= 10 && - '[' === $html[ $this->token_starts_at + 2 ] && - 'C' === $html[ $this->token_starts_at + 3 ] && - 'D' === $html[ $this->token_starts_at + 4 ] && - 'A' === $html[ $this->token_starts_at + 5 ] && - 'T' === $html[ $this->token_starts_at + 6 ] && - 'A' === $html[ $this->token_starts_at + 7 ] && - '[' === $html[ $this->token_starts_at + 8 ] && - ']' === $html[ $closer_at - 1 ] && - ']' === $html[ $closer_at - 2 ] - ) { - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_CDATA_LOOKALIKE; - $this->text_starts_at += 7; - $this->text_length -= 9; - } - - return true; - } - - /* - * is a missing end tag name, which is ignored. - * - * This was also known as the "presumptuous empty tag" - * in early discussions as it was proposed to close - * the nearest previous opening tag. - * - * See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name - */ - if ( '>' === $html[ $at + 1 ] ) { - $this->parser_state = self::STATE_PRESUMPTUOUS_TAG; - $this->token_length = $at + 2 - $this->token_starts_at; - $this->bytes_already_parsed = $at + 2; - return true; - } - - /* - * ` - * See https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state - */ - if ( '?' === $html[ $at + 1 ] ) { - $closer_at = strpos( $html, '>', $at + 2 ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->parser_state = self::STATE_COMMENT; - $this->comment_type = self::COMMENT_AS_INVALID_HTML; - $this->token_length = $closer_at + 1 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 2; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 1; - - /* - * Identify a Processing Instruction node were HTML to have them. - * - * This section must occur after identifying the bogus comment end - * because in an HTML parser it will span to the nearest `>`, even - * if there's no `?>` as would be required in an XML document. It - * is therefore not possible to parse a Processing Instruction node - * containing a `>` in the HTML syntax. - * - * XML allows for more target names, but this code only identifies - * those with ASCII-representable target names. This means that it - * may identify some Processing Instruction nodes as bogus comments, - * but it will not misinterpret the HTML structure. By limiting the - * identification to these target names the Tag Processor can avoid - * the need to start parsing UTF-8 sequences. - * - * > NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | - * [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | - * [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | - * [#x10000-#xEFFFF] - * > NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] - * - * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#NT-PITarget - */ - if ( $this->token_length >= 5 && '?' === $html[ $closer_at - 1 ] ) { - $comment_text = substr( $html, $this->token_starts_at + 2, $this->token_length - 4 ); - $pi_target_length = strspn( $comment_text, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:_' ); - - if ( 0 < $pi_target_length ) { - $pi_target_length += strspn( $comment_text, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:_-.', $pi_target_length ); - - $this->comment_type = self::COMMENT_AS_PI_NODE_LOOKALIKE; - $this->tag_name_starts_at = $this->token_starts_at + 2; - $this->tag_name_length = $pi_target_length; - $this->text_starts_at += $pi_target_length; - $this->text_length -= $pi_target_length + 1; - } - } - - return true; - } - - /* - * If a non-alpha starts the tag name in a tag closer it's a comment. - * Find the first `>`, which closes the comment. - * - * This parser classifies these particular comments as special "funky comments" - * which are made available for further processing. - * - * See https://html.spec.whatwg.org/#parse-error-invalid-first-character-of-tag-name - */ - if ( $this->is_closing_tag ) { - // No chance of finding a closer. - if ( $at + 3 > $doc_length ) { - return false; - } - - $closer_at = strpos( $html, '>', $at + 3 ); - if ( false === $closer_at ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->parser_state = self::STATE_FUNKY_COMMENT; - $this->token_length = $closer_at + 1 - $this->token_starts_at; - $this->text_starts_at = $this->token_starts_at + 2; - $this->text_length = $closer_at - $this->text_starts_at; - $this->bytes_already_parsed = $closer_at + 1; - return true; - } - - ++$at; - } - - return false; - } - - /** - * Parses the next attribute. - * - * @since 6.2.0 - * - * @return bool Whether an attribute was found before the end of the document. - */ - private function parse_next_attribute() { - // Skip whitespace and slashes. - $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed ); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - /* - * Treat the equal sign as a part of the attribute - * name if it is the first encountered byte. - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state - */ - $name_length = '=' === $this->html[ $this->bytes_already_parsed ] - ? 1 + strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed + 1 ) - : strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed ); - - // No attribute, just tag closer. - if ( 0 === $name_length || $this->bytes_already_parsed + $name_length >= strlen( $this->html ) ) { - return false; - } - - $attribute_start = $this->bytes_already_parsed; - $attribute_name = substr( $this->html, $attribute_start, $name_length ); - $this->bytes_already_parsed += $name_length; - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $this->skip_whitespace(); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - $has_value = '=' === $this->html[ $this->bytes_already_parsed ]; - if ( $has_value ) { - ++$this->bytes_already_parsed; - $this->skip_whitespace(); - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - switch ( $this->html[ $this->bytes_already_parsed ] ) { - case "'": - case '"': - $quote = $this->html[ $this->bytes_already_parsed ]; - $value_start = $this->bytes_already_parsed + 1; - $value_length = strcspn( $this->html, $quote, $value_start ); - $attribute_end = $value_start + $value_length + 1; - $this->bytes_already_parsed = $attribute_end; - break; - - default: - $value_start = $this->bytes_already_parsed; - $value_length = strcspn( $this->html, "> \t\f\r\n", $value_start ); - $attribute_end = $value_start + $value_length; - $this->bytes_already_parsed = $attribute_end; - } - } else { - $value_start = $this->bytes_already_parsed; - $value_length = 0; - $attribute_end = $attribute_start + $name_length; - } - - if ( $attribute_end >= strlen( $this->html ) ) { - $this->parser_state = self::STATE_INCOMPLETE_INPUT; - - return false; - } - - if ( $this->is_closing_tag ) { - return true; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $comparable_name = strtolower( $attribute_name ); - - // If an attribute is listed many times, only use the first declaration and ignore the rest. - if ( ! array_key_exists( $comparable_name, $this->attributes ) ) { - $this->attributes[ $comparable_name ] = new Gutenberg_HTML_Attribute_Token_6_5( - $attribute_name, - $value_start, - $value_length, - $attribute_start, - $attribute_end - $attribute_start, - ! $has_value - ); - - return true; - } - - /* - * Track the duplicate attributes so if we remove it, all disappear together. - * - * While `$this->duplicated_attributes` could always be stored as an `array()`, - * which would simplify the logic here, storing a `null` and only allocating - * an array when encountering duplicates avoids needless allocations in the - * normative case of parsing tags with no duplicate attributes. - */ - $duplicate_span = new Gutenberg_HTML_Span_6_5( $attribute_start, $attribute_end - $attribute_start ); - if ( null === $this->duplicate_attributes ) { - $this->duplicate_attributes = array( $comparable_name => array( $duplicate_span ) ); - } elseif ( ! array_key_exists( $comparable_name, $this->duplicate_attributes ) ) { - $this->duplicate_attributes[ $comparable_name ] = array( $duplicate_span ); - } else { - $this->duplicate_attributes[ $comparable_name ][] = $duplicate_span; - } - - return true; - } - - /** - * Move the internal cursor past any immediate successive whitespace. - * - * @since 6.2.0 - */ - private function skip_whitespace() { - $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n", $this->bytes_already_parsed ); - } - - /** - * Applies attribute updates and cleans up once a tag is fully parsed. - * - * @since 6.2.0 - */ - private function after_tag() { - /* - * There could be lexical updates enqueued for an attribute that - * also exists on the next tag. In order to avoid conflating the - * attributes across the two tags, lexical updates with names - * need to be flushed to raw lexical updates. - */ - $this->class_name_updates_to_attributes_updates(); - - /* - * Purge updates if there are too many. The actual count isn't - * scientific, but a few values from 100 to a few thousand were - * tests to find a practially-useful limit. - * - * If the update queue grows too big, then the Tag Processor - * will spend more time iterating through them and lose the - * efficiency gains of deferring applying them. - */ - if ( 1000 < count( $this->lexical_updates ) ) { - $this->get_updated_html(); - } - - foreach ( $this->lexical_updates as $name => $update ) { - /* - * Any updates appearing after the cursor should be applied - * before proceeding, otherwise they may be overlooked. - */ - if ( $update->start >= $this->bytes_already_parsed ) { - $this->get_updated_html(); - break; - } - - if ( is_int( $name ) ) { - continue; - } - - $this->lexical_updates[] = $update; - unset( $this->lexical_updates[ $name ] ); - } - - $this->token_starts_at = null; - $this->token_length = null; - $this->tag_name_starts_at = null; - $this->tag_name_length = null; - $this->text_starts_at = 0; - $this->text_length = 0; - $this->is_closing_tag = null; - $this->attributes = array(); - $this->comment_type = null; - $this->duplicate_attributes = null; - } - - /** - * Converts class name updates into tag attributes updates - * (they are accumulated in different data formats for performance). - * - * @since 6.2.0 - * - * @see WP_HTML_Tag_Processor::$lexical_updates - * @see WP_HTML_Tag_Processor::$classname_updates - */ - private function class_name_updates_to_attributes_updates() { - if ( count( $this->classname_updates ) === 0 ) { - return; - } - - $existing_class = $this->get_enqueued_attribute_value( 'class' ); - if ( null === $existing_class || true === $existing_class ) { - $existing_class = ''; - } - - if ( false === $existing_class && isset( $this->attributes['class'] ) ) { - $existing_class = substr( - $this->html, - $this->attributes['class']->value_starts_at, - $this->attributes['class']->value_length - ); - } - - if ( false === $existing_class ) { - $existing_class = ''; - } - - /** - * Updated "class" attribute value. - * - * This is incrementally built while scanning through the existing class - * attribute, skipping removed classes on the way, and then appending - * added classes at the end. Only when finished processing will the - * value contain the final new value. - - * @var string $class - */ - $class = ''; - - /** - * Tracks the cursor position in the existing - * class attribute value while parsing. - * - * @var int $at - */ - $at = 0; - - /** - * Indicates if there's any need to modify the existing class attribute. - * - * If a call to `add_class()` and `remove_class()` wouldn't impact - * the `class` attribute value then there's no need to rebuild it. - * For example, when adding a class that's already present or - * removing one that isn't. - * - * This flag enables a performance optimization when none of the enqueued - * class updates would impact the `class` attribute; namely, that the - * processor can continue without modifying the input document, as if - * none of the `add_class()` or `remove_class()` calls had been made. - * - * This flag is set upon the first change that requires a string update. - * - * @var bool $modified - */ - $modified = false; - - // Remove unwanted classes by only copying the new ones. - $existing_class_length = strlen( $existing_class ); - while ( $at < $existing_class_length ) { - // Skip to the first non-whitespace character. - $ws_at = $at; - $ws_length = strspn( $existing_class, " \t\f\r\n", $ws_at ); - $at += $ws_length; - - // Capture the class name – it's everything until the next whitespace. - $name_length = strcspn( $existing_class, " \t\f\r\n", $at ); - if ( 0 === $name_length ) { - // If no more class names are found then that's the end. - break; - } - - $name = substr( $existing_class, $at, $name_length ); - $at += $name_length; - - // If this class is marked for removal, start processing the next one. - $remove_class = ( - isset( $this->classname_updates[ $name ] ) && - self::REMOVE_CLASS === $this->classname_updates[ $name ] - ); - - // If a class has already been seen then skip it; it should not be added twice. - if ( ! $remove_class ) { - $this->classname_updates[ $name ] = self::SKIP_CLASS; - } - - if ( $remove_class ) { - $modified = true; - continue; - } - - /* - * Otherwise, append it to the new "class" attribute value. - * - * There are options for handling whitespace between tags. - * Preserving the existing whitespace produces fewer changes - * to the HTML content and should clarify the before/after - * content when debugging the modified output. - * - * This approach contrasts normalizing the inter-class - * whitespace to a single space, which might appear cleaner - * in the output HTML but produce a noisier change. - */ - $class .= substr( $existing_class, $ws_at, $ws_length ); - $class .= $name; - } - - // Add new classes by appending those which haven't already been seen. - foreach ( $this->classname_updates as $name => $operation ) { - if ( self::ADD_CLASS === $operation ) { - $modified = true; - - $class .= strlen( $class ) > 0 ? ' ' : ''; - $class .= $name; - } - } - - $this->classname_updates = array(); - if ( ! $modified ) { - return; - } - - if ( strlen( $class ) > 0 ) { - $this->set_attribute( 'class', $class ); - } else { - $this->remove_attribute( 'class' ); - } - } - - /** - * Applies attribute updates to HTML document. - * - * @since 6.2.0 - * @since 6.2.1 Accumulates shift for internal cursor and passed pointer. - * @since 6.3.0 Invalidate any bookmarks whose targets are overwritten. - * - * @param int $shift_this_point Accumulate and return shift for this position. - * @return int How many bytes the given pointer moved in response to the updates. - */ - private function apply_attributes_updates( $shift_this_point = 0 ) { - if ( ! count( $this->lexical_updates ) ) { - return 0; - } - - $accumulated_shift_for_given_point = 0; - - /* - * Attribute updates can be enqueued in any order but updates - * to the document must occur in lexical order; that is, each - * replacement must be made before all others which follow it - * at later string indices in the input document. - * - * Sorting avoid making out-of-order replacements which - * can lead to mangled output, partially-duplicated - * attributes, and overwritten attributes. - */ - usort( $this->lexical_updates, array( self::class, 'sort_start_ascending' ) ); - - $bytes_already_copied = 0; - $output_buffer = ''; - foreach ( $this->lexical_updates as $diff ) { - $shift = strlen( $diff->text ) - $diff->length; - - // Adjust the cursor position by however much an update affects it. - if ( $diff->start < $this->bytes_already_parsed ) { - $this->bytes_already_parsed += $shift; - } - - // Accumulate shift of the given pointer within this function call. - if ( $diff->start <= $shift_this_point ) { - $accumulated_shift_for_given_point += $shift; - } - - $output_buffer .= substr( $this->html, $bytes_already_copied, $diff->start - $bytes_already_copied ); - $output_buffer .= $diff->text; - $bytes_already_copied = $diff->start + $diff->length; - } - - $this->html = $output_buffer . substr( $this->html, $bytes_already_copied ); - - /* - * Adjust bookmark locations to account for how the text - * replacements adjust offsets in the input document. - */ - foreach ( $this->bookmarks as $bookmark_name => $bookmark ) { - $bookmark_end = $bookmark->start + $bookmark->length; - - /* - * Each lexical update which appears before the bookmark's endpoints - * might shift the offsets for those endpoints. Loop through each change - * and accumulate the total shift for each bookmark, then apply that - * shift after tallying the full delta. - */ - $head_delta = 0; - $tail_delta = 0; - - foreach ( $this->lexical_updates as $diff ) { - $diff_end = $diff->start + $diff->length; - - if ( $bookmark->start < $diff->start && $bookmark_end < $diff->start ) { - break; - } - - if ( $bookmark->start >= $diff->start && $bookmark_end < $diff_end ) { - $this->release_bookmark( $bookmark_name ); - continue 2; - } - - $delta = strlen( $diff->text ) - $diff->length; - - if ( $bookmark->start >= $diff->start ) { - $head_delta += $delta; - } - - if ( $bookmark_end >= $diff_end ) { - $tail_delta += $delta; - } - } - - $bookmark->start += $head_delta; - $bookmark->length += $tail_delta - $head_delta; - } - - $this->lexical_updates = array(); - - return $accumulated_shift_for_given_point; - } - - /** - * Checks whether a bookmark with the given name exists. - * - * @since 6.3.0 - * - * @param string $bookmark_name Name to identify a bookmark that potentially exists. - * @return bool Whether that bookmark exists. - */ - public function has_bookmark( $bookmark_name ) { - return array_key_exists( $bookmark_name, $this->bookmarks ); - } - - /** - * Move the internal cursor in the Tag Processor to a given bookmark's location. - * - * In order to prevent accidental infinite loops, there's a - * maximum limit on the number of times seek() can be called. - * - * @since 6.2.0 - * - * @param string $bookmark_name Jump to the place in the document identified by this bookmark name. - * @return bool Whether the internal cursor was successfully moved to the bookmark's location. - */ - public function seek( $bookmark_name ) { - if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) { - _doing_it_wrong( - __METHOD__, - __( 'Unknown bookmark name.' ), - '6.2.0' - ); - return false; - } - - if ( ++$this->seek_count > static::MAX_SEEK_OPS ) { - _doing_it_wrong( - __METHOD__, - __( 'Too many calls to seek() - this can lead to performance issues.' ), - '6.2.0' - ); - return false; - } - - // Flush out any pending updates to the document. - $this->get_updated_html(); - - // Point this tag processor before the sought tag opener and consume it. - $this->bytes_already_parsed = $this->bookmarks[ $bookmark_name ]->start; - $this->parser_state = self::STATE_READY; - return $this->next_token(); - } - - /** - * Compare two WP_HTML_Text_Replacement objects. - * - * @since 6.2.0 - * - * @param WP_HTML_Text_Replacement $a First attribute update. - * @param WP_HTML_Text_Replacement $b Second attribute update. - * @return int Comparison value for string order. - */ - private static function sort_start_ascending( $a, $b ) { - $by_start = $a->start - $b->start; - if ( 0 !== $by_start ) { - return $by_start; - } - - $by_text = isset( $a->text, $b->text ) ? strcmp( $a->text, $b->text ) : 0; - if ( 0 !== $by_text ) { - return $by_text; - } - - /* - * This code should be unreachable, because it implies the two replacements - * start at the same location and contain the same text. - */ - return $a->length - $b->length; - } - - /** - * Return the enqueued value for a given attribute, if one exists. - * - * Enqueued updates can take different data types: - * - If an update is enqueued and is boolean, the return will be `true` - * - If an update is otherwise enqueued, the return will be the string value of that update. - * - If an attribute is enqueued to be removed, the return will be `null` to indicate that. - * - If no updates are enqueued, the return will be `false` to differentiate from "removed." - * - * @since 6.2.0 - * - * @param string $comparable_name The attribute name in its comparable form. - * @return string|boolean|null Value of enqueued update if present, otherwise false. - */ - private function get_enqueued_attribute_value( $comparable_name ) { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return false; - } - - if ( ! isset( $this->lexical_updates[ $comparable_name ] ) ) { - return false; - } - - $enqueued_text = $this->lexical_updates[ $comparable_name ]->text; - - // Removed attributes erase the entire span. - if ( '' === $enqueued_text ) { - return null; - } - - /* - * Boolean attribute updates are just the attribute name without a corresponding value. - * - * This value might differ from the given comparable name in that there could be leading - * or trailing whitespace, and that the casing follows the name given in `set_attribute`. - * - * Example: - * - * $p->set_attribute( 'data-TEST-id', 'update' ); - * 'update' === $p->get_enqueued_attribute_value( 'data-test-id' ); - * - * Detect this difference based on the absence of the `=`, which _must_ exist in any - * attribute containing a value, e.g. ``. - * ¹ ² - * 1. Attribute with a string value. - * 2. Boolean attribute whose value is `true`. - */ - $equals_at = strpos( $enqueued_text, '=' ); - if ( false === $equals_at ) { - return true; - } - - /* - * Finally, a normal update's value will appear after the `=` and - * be double-quoted, as performed incidentally by `set_attribute`. - * - * e.g. `type="text"` - * ¹² ³ - * 1. Equals is here. - * 2. Double-quoting starts one after the equals sign. - * 3. Double-quoting ends at the last character in the update. - */ - $enqueued_value = substr( $enqueued_text, $equals_at + 2, -1 ); - return html_entity_decode( $enqueued_value ); - } - - /** - * Returns the value of a requested attribute from a matched tag opener if that attribute exists. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag( array( 'class_name' => 'test' ) ) === true; - * $p->get_attribute( 'data-test-id' ) === '14'; - * $p->get_attribute( 'enabled' ) === true; - * $p->get_attribute( 'aria-label' ) === null; - * - * $p->next_tag() === false; - * $p->get_attribute( 'class' ) === null; - * - * @since 6.2.0 - * - * @param string $name Name of attribute whose value is requested. - * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`. - */ - public function get_attribute( $name ) { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return null; - } - - $comparable = strtolower( $name ); - - /* - * For every attribute other than `class` it's possible to perform a quick check if - * there's an enqueued lexical update whose value takes priority over what's found in - * the input document. - * - * The `class` attribute is special though because of the exposed helpers `add_class` - * and `remove_class`. These form a builder for the `class` attribute, so an additional - * check for enqueued class changes is required in addition to the check for any enqueued - * attribute values. If any exist, those enqueued class changes must first be flushed out - * into an attribute value update. - */ - if ( 'class' === $name ) { - $this->class_name_updates_to_attributes_updates(); - } - - // Return any enqueued attribute value updates if they exist. - $enqueued_value = $this->get_enqueued_attribute_value( $comparable ); - if ( false !== $enqueued_value ) { - return $enqueued_value; - } - - if ( ! isset( $this->attributes[ $comparable ] ) ) { - return null; - } - - $attribute = $this->attributes[ $comparable ]; - - /* - * This flag distinguishes an attribute with no value - * from an attribute with an empty string value. For - * unquoted attributes this could look very similar. - * It refers to whether an `=` follows the name. - * - * e.g.
- * ¹ ² - * 1. Attribute `boolean-attribute` is `true`. - * 2. Attribute `empty-attribute` is `""`. - */ - if ( true === $attribute->is_true ) { - return true; - } - - $raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length ); - - return html_entity_decode( $raw_value ); - } - - /** - * Gets lowercase names of all attributes matching a given prefix in the current tag. - * - * Note that matching is case-insensitive. This is in accordance with the spec: - * - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag( array( 'class_name' => 'test' ) ) === true; - * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); - * - * $p->next_tag() === false; - * $p->get_attribute_names_with_prefix( 'data-' ) === null; - * - * @since 6.2.0 - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - * - * @param string $prefix Prefix of requested attribute names. - * @return array|null List of attribute names, or `null` when no tag opener is matched. - */ - public function get_attribute_names_with_prefix( $prefix ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return null; - } - - $comparable = strtolower( $prefix ); - - $matches = array(); - foreach ( array_keys( $this->attributes ) as $attr_name ) { - if ( str_starts_with( $attr_name, $comparable ) ) { - $matches[] = $attr_name; - } - } - return $matches; - } - - /** - * Returns the uppercase name of the matched tag. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
Test
' ); - * $p->next_tag() === true; - * $p->get_tag() === 'DIV'; - * - * $p->next_tag() === false; - * $p->get_tag() === null; - * - * @since 6.2.0 - * - * @return string|null Name of currently matched tag in input HTML, or `null` if none found. - */ - public function get_tag() { - if ( null === $this->tag_name_starts_at ) { - return null; - } - - $tag_name = substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length ); - - if ( self::STATE_MATCHED_TAG === $this->parser_state ) { - return strtoupper( $tag_name ); - } - - if ( - self::STATE_COMMENT === $this->parser_state && - self::COMMENT_AS_PI_NODE_LOOKALIKE === $this->get_comment_type() - ) { - return $tag_name; - } - - return null; - } - - /** - * Indicates if the currently matched tag contains the self-closing flag. - * - * No HTML elements ought to have the self-closing flag and for those, the self-closing - * flag will be ignored. For void elements this is benign because they "self close" - * automatically. For non-void HTML elements though problems will appear if someone - * intends to use a self-closing element in place of that element with an empty body. - * For HTML foreign elements and custom elements the self-closing flag determines if - * they self-close or not. - * - * This function does not determine if a tag is self-closing, - * but only if the self-closing flag is present in the syntax. - * - * @since 6.3.0 - * - * @return bool Whether the currently matched tag contains the self-closing flag. - */ - public function has_self_closing_flag() { - if ( self::STATE_MATCHED_TAG !== $this->parser_state ) { - return false; - } - - /* - * The self-closing flag is the solidus at the _end_ of the tag, not the beginning. - * - * Example: - * - *
- * ^ this appears one character before the end of the closing ">". - */ - return '/' === $this->html[ $this->token_starts_at + $this->token_length - 1 ]; - } - - /** - * Indicates if the current tag token is a tag closer. - * - * Example: - * - * $p = new WP_HTML_Tag_Processor( '
' ); - * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); - * $p->is_tag_closer() === false; - * - * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); - * $p->is_tag_closer() === true; - * - * @since 6.2.0 - * - * @return bool Whether the current tag is a tag closer. - */ - public function is_tag_closer() { - return ( - self::STATE_MATCHED_TAG === $this->parser_state && - $this->is_closing_tag - ); - } - - /** - * Indicates the kind of matched token, if any. - * - * This differs from `get_token_name()` in that it always - * returns a static string indicating the type, whereas - * `get_token_name()` may return values derived from the - * token itself, such as a tag name or processing - * instruction tag. - * - * Possible values: - * - `#tag` when matched on a tag. - * - `#text` when matched on a text node. - * - `#cdata-section` when matched on a CDATA node. - * - `#comment` when matched on a comment. - * - `#doctype` when matched on a DOCTYPE declaration. - * - `#presumptuous-tag` when matched on an empty tag closer. - * - `#funky-comment` when matched on a funky comment. - * - * @since 6.5.0 - * - * @return string|null What kind of token is matched, or null. - */ - public function get_token_type() { - switch ( $this->parser_state ) { - case self::STATE_MATCHED_TAG: - return '#tag'; - - case self::STATE_DOCTYPE: - return '#doctype'; - - default: - return $this->get_token_name(); - } - } - - /** - * Returns the node name represented by the token. - * - * This matches the DOM API value `nodeName`. Some values - * are static, such as `#text` for a text node, while others - * are dynamically generated from the token itself. - * - * Dynamic names: - * - Uppercase tag name for tag matches. - * - `html` for DOCTYPE declarations. - * - * Note that if the Tag Processor is not matched on a token - * then this function will return `null`, either because it - * hasn't yet found a token or because it reached the end - * of the document without matching a token. - * - * @since 6.5.0 - * - * @return string|null Name of the matched token. - */ - public function get_token_name() { - switch ( $this->parser_state ) { - case self::STATE_MATCHED_TAG: - return $this->get_tag(); - - case self::STATE_TEXT_NODE: - return '#text'; - - case self::STATE_CDATA_NODE: - return '#cdata-section'; - - case self::STATE_COMMENT: - return '#comment'; - - case self::STATE_DOCTYPE: - return 'html'; - - case self::STATE_PRESUMPTUOUS_TAG: - return '#presumptuous-tag'; - - case self::STATE_FUNKY_COMMENT: - return '#funky-comment'; - } - } - - /** - * Indicates what kind of comment produced the comment node. - * - * Because there are different kinds of HTML syntax which produce - * comments, the Tag Processor tracks and exposes this as a type - * for the comment. Nominally only regular HTML comments exist as - * they are commonly known, but a number of unrelated syntax errors - * also produce comments. - * - * @see self::COMMENT_AS_ABRUPTLY_CLOSED_COMMENT - * @see self::COMMENT_AS_CDATA_LOOKALIKE - * @see self::COMMENT_AS_INVALID_HTML - * @see self::COMMENT_AS_HTML_COMMENT - * @see self::COMMENT_AS_PI_NODE_LOOKALIKE - * - * @since 6.5.0 - * - * @return string|null - */ - public function get_comment_type() { - if ( self::STATE_COMMENT !== $this->parser_state ) { - return null; - } - - return $this->comment_type; - } - - /** - * Returns the modifiable text for a matched token, or an empty string. - * - * Modifiable text is text content that may be read and changed without - * changing the HTML structure of the document around it. This includes - * the contents of `#text` nodes in the HTML as well as the inner - * contents of HTML comments, Processing Instructions, and others, even - * though these nodes aren't part of a parsed DOM tree. They also contain - * the contents of SCRIPT and STYLE tags, of TEXTAREA tags, and of any - * other section in an HTML document which cannot contain HTML markup (DATA). - * - * If a token has no modifiable text then an empty string is returned to - * avoid needless crashing or type errors. An empty string does not mean - * that a token has modifiable text, and a token with modifiable text may - * have an empty string (e.g. a comment with no contents). - * - * @since 6.5.0 - * - * @return string - */ - public function get_modifiable_text() { - if ( null === $this->text_starts_at ) { - return ''; - } - - $text = substr( $this->html, $this->text_starts_at, $this->text_length ); - - // Comment data is not decoded. - if ( - self::STATE_CDATA_NODE === $this->parser_state || - self::STATE_COMMENT === $this->parser_state || - self::STATE_DOCTYPE === $this->parser_state || - self::STATE_FUNKY_COMMENT === $this->parser_state - ) { - return $text; - } - - $tag_name = $this->get_tag(); - if ( - // Script data is not decoded. - 'SCRIPT' === $tag_name || - - // RAWTEXT data is not decoded. - 'IFRAME' === $tag_name || - 'NOEMBED' === $tag_name || - 'NOFRAMES' === $tag_name || - 'STYLE' === $tag_name || - 'XMP' === $tag_name - ) { - return $text; - } - - $decoded = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE ); - - /* - * TEXTAREA skips a leading newline, but this newline may appear not only as the - * literal character `\n`, but also as a character reference, such as in the - * following markup: ``. - * - * For these cases it's important to first decode the text content before checking - * for a leading newline and removing it. - */ - if ( - self::STATE_MATCHED_TAG === $this->parser_state && - 'TEXTAREA' === $tag_name && - strlen( $decoded ) > 0 && - "\n" === $decoded[0] - ) { - return substr( $decoded, 1 ); - } - - return $decoded; - } - - /** - * Updates or creates a new attribute on the currently matched tag with the passed value. - * - * For boolean attributes special handling is provided: - * - When `true` is passed as the value, then only the attribute name is added to the tag. - * - When `false` is passed, the attribute gets removed if it existed before. - * - * For string attributes, the value is escaped using the `esc_attr` function. - * - * @since 6.2.0 - * @since 6.2.1 Fix: Only create a single update for multiple calls with case-variant attribute names. - * - * @param string $name The attribute name to target. - * @param string|bool $value The new attribute value. - * @return bool Whether an attribute value was set. - */ - public function set_attribute( $name, $value ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return false; - } - - /* - * WordPress rejects more characters than are strictly forbidden - * in HTML5. This is to prevent additional security risks deeper - * in the WordPress and plugin stack. Specifically the - * less-than (<) greater-than (>) and ampersand (&) aren't allowed. - * - * The use of a PCRE match enables looking for specific Unicode - * code points without writing a UTF-8 decoder. Whereas scanning - * for one-byte characters is trivial (with `strcspn`), scanning - * for the longer byte sequences would be more complicated. Given - * that this shouldn't be in the hot path for execution, it's a - * reasonable compromise in efficiency without introducing a - * noticeable impact on the overall system. - * - * @see https://html.spec.whatwg.org/#attributes-2 - * - * @todo As the only regex pattern maybe we should take it out? - * Are Unicode patterns available broadly in Core? - */ - if ( preg_match( - '~[' . - // Syntax-like characters. - '"\'>& The values "true" and "false" are not allowed on boolean attributes. - * > To represent a false value, the attribute has to be omitted altogether. - * - HTML5 spec, https://html.spec.whatwg.org/#boolean-attributes - */ - if ( false === $value ) { - return $this->remove_attribute( $name ); - } - - if ( true === $value ) { - $updated_attribute = $name; - } else { - $comparable_name = strtolower( $name ); - - /* - * Escape URL attributes. - * - * @see https://html.spec.whatwg.org/#attributes-3 - */ - $escaped_new_value = in_array( $comparable_name, wp_kses_uri_attributes() ) ? esc_url( $value ) : esc_attr( $value ); - $updated_attribute = "{$name}=\"{$escaped_new_value}\""; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $comparable_name = strtolower( $name ); - - if ( isset( $this->attributes[ $comparable_name ] ) ) { - /* - * Update an existing attribute. - * - * Example – set attribute id to "new" in
: - * - *
- * ^-------------^ - * start end - * replacement: `id="new"` - * - * Result:
- */ - $existing_attribute = $this->attributes[ $comparable_name ]; - $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( - $existing_attribute->start, - $existing_attribute->length, - $updated_attribute - ); - } else { - /* - * Create a new attribute at the tag's name end. - * - * Example – add attribute id="new" to
: - * - *
- * ^ - * start and end - * replacement: ` id="new"` - * - * Result:
- */ - $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( - $this->tag_name_starts_at + $this->tag_name_length, - 0, - ' ' . $updated_attribute - ); - } - - /* - * Any calls to update the `class` attribute directly should wipe out any - * enqueued class changes from `add_class` and `remove_class`. - */ - if ( 'class' === $comparable_name && ! empty( $this->classname_updates ) ) { - $this->classname_updates = array(); - } - - return true; - } - - /** - * Remove an attribute from the currently-matched tag. - * - * @since 6.2.0 - * - * @param string $name The attribute name to remove. - * @return bool Whether an attribute was removed. - */ - public function remove_attribute( $name ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return false; - } - - /* - * > There must never be two or more attributes on - * > the same start tag whose names are an ASCII - * > case-insensitive match for each other. - * - HTML 5 spec - * - * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive - */ - $name = strtolower( $name ); - - /* - * Any calls to update the `class` attribute directly should wipe out any - * enqueued class changes from `add_class` and `remove_class`. - */ - if ( 'class' === $name && count( $this->classname_updates ) !== 0 ) { - $this->classname_updates = array(); - } - - /* - * If updating an attribute that didn't exist in the input - * document, then remove the enqueued update and move on. - * - * For example, this might occur when calling `remove_attribute()` - * after calling `set_attribute()` for the same attribute - * and when that attribute wasn't originally present. - */ - if ( ! isset( $this->attributes[ $name ] ) ) { - if ( isset( $this->lexical_updates[ $name ] ) ) { - unset( $this->lexical_updates[ $name ] ); - } - return false; - } - - /* - * Removes an existing tag attribute. - * - * Example – remove the attribute id from
: - *
- * ^-------------^ - * start end - * replacement: `` - * - * Result:
- */ - $this->lexical_updates[ $name ] = new Gutenberg_HTML_Text_Replacement_6_5( - $this->attributes[ $name ]->start, - $this->attributes[ $name ]->length, - '' - ); - - // Removes any duplicated attributes if they were also present. - if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) { - foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) { - $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( - $attribute_token->start, - $attribute_token->length, - '' - ); - } - } - - return true; - } - - /** - * Adds a new class name to the currently matched tag. - * - * @since 6.2.0 - * - * @param string $class_name The class name to add. - * @return bool Whether the class was set to be added. - */ - public function add_class( $class_name ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return false; - } - - $this->classname_updates[ $class_name ] = self::ADD_CLASS; - - return true; - } - - /** - * Removes a class name from the currently matched tag. - * - * @since 6.2.0 - * - * @param string $class_name The class name to remove. - * @return bool Whether the class was set to be removed. - */ - public function remove_class( $class_name ) { - if ( - self::STATE_MATCHED_TAG !== $this->parser_state || - $this->is_closing_tag - ) { - return false; - } - - if ( null !== $this->tag_name_starts_at ) { - $this->classname_updates[ $class_name ] = self::REMOVE_CLASS; - } - - return true; - } - - /** - * Returns the string representation of the HTML Tag Processor. - * - * @since 6.2.0 - * - * @see WP_HTML_Tag_Processor::get_updated_html() - * - * @return string The processed HTML. - */ - public function __toString() { - return $this->get_updated_html(); - } - - /** - * Returns the string representation of the HTML Tag Processor. - * - * @since 6.2.0 - * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates. - * @since 6.4.0 No longer calls subclass method `next_tag()` after updating HTML. - * - * @return string The processed HTML. - */ - public function get_updated_html() { - $requires_no_updating = 0 === count( $this->classname_updates ) && 0 === count( $this->lexical_updates ); - - /* - * When there is nothing more to update and nothing has already been - * updated, return the original document and avoid a string copy. - */ - if ( $requires_no_updating ) { - return $this->html; - } - - /* - * Keep track of the position right before the current tag. This will - * be necessary for reparsing the current tag after updating the HTML. - */ - $before_current_tag = $this->token_starts_at; - - /* - * 1. Apply the enqueued edits and update all the pointers to reflect those changes. - */ - $this->class_name_updates_to_attributes_updates(); - $before_current_tag += $this->apply_attributes_updates( $before_current_tag ); - - /* - * 2. Rewind to before the current tag and reparse to get updated attributes. - * - * At this point the internal cursor points to the end of the tag name. - * Rewind before the tag name starts so that it's as if the cursor didn't - * move; a call to `next_tag()` will reparse the recently-updated attributes - * and additional calls to modify the attributes will apply at this same - * location, but in order to avoid issues with subclasses that might add - * behaviors to `next_tag()`, the internal methods should be called here - * instead. - * - * It's important to note that in this specific place there will be no change - * because the processor was already at a tag when this was called and it's - * rewinding only to the beginning of this very tag before reprocessing it - * and its attributes. - * - *

Previous HTMLMore HTML

- * ↑ │ back up by the length of the tag name plus the opening < - * └←─┘ back up by strlen("em") + 1 ==> 3 - */ - $this->bytes_already_parsed = $before_current_tag; - $this->base_class_next_token(); - - return $this->html; - } - - /** - * Parses tag query input into internal search criteria. - * - * @since 6.2.0 - * - * @param array|string|null $query { - * Optional. Which tag name to find, having which class, etc. Default is to find any tag. - * - * @type string|null $tag_name Which tag to find, or `null` for "any tag." - * @type int|null $match_offset Find the Nth tag matching all search criteria. - * 1 for "first" tag, 3 for "third," etc. - * Defaults to first tag. - * @type string|null $class_name Tag must contain this class name to match. - * @type string $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. - * } - */ - private function parse_query( $query ) { - if ( null !== $query && $query === $this->last_query ) { - return; - } - - $this->last_query = $query; - $this->sought_tag_name = null; - $this->sought_class_name = null; - $this->sought_match_offset = 1; - $this->stop_on_tag_closers = false; - - // A single string value means "find the tag of this name". - if ( is_string( $query ) ) { - $this->sought_tag_name = $query; - return; - } - - // An empty query parameter applies no restrictions on the search. - if ( null === $query ) { - return; - } - - // If not using the string interface, an associative array is required. - if ( ! is_array( $query ) ) { - _doing_it_wrong( - __METHOD__, - __( 'The query argument must be an array or a tag name.' ), - '6.2.0' - ); - return; - } - - if ( isset( $query['tag_name'] ) && is_string( $query['tag_name'] ) ) { - $this->sought_tag_name = $query['tag_name']; - } - - if ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) ) { - $this->sought_class_name = $query['class_name']; - } - - if ( isset( $query['match_offset'] ) && is_int( $query['match_offset'] ) && 0 < $query['match_offset'] ) { - $this->sought_match_offset = $query['match_offset']; - } - - if ( isset( $query['tag_closers'] ) ) { - $this->stop_on_tag_closers = 'visit' === $query['tag_closers']; - } - } - - - /** - * Checks whether a given tag and its attributes match the search criteria. - * - * @since 6.2.0 - * - * @return bool Whether the given tag and its attribute match the search criteria. - */ - private function matches() { - if ( $this->is_closing_tag && ! $this->stop_on_tag_closers ) { - return false; - } - - // Does the tag name match the requested tag name in a case-insensitive manner? - if ( null !== $this->sought_tag_name ) { - /* - * String (byte) length lookup is fast. If they aren't the - * same length then they can't be the same string values. - */ - if ( strlen( $this->sought_tag_name ) !== $this->tag_name_length ) { - return false; - } - - /* - * Check each character to determine if they are the same. - * Defer calls to `strtoupper()` to avoid them when possible. - * Calling `strcasecmp()` here tested slowed than comparing each - * character, so unless benchmarks show otherwise, it should - * not be used. - * - * It's expected that most of the time that this runs, a - * lower-case tag name will be supplied and the input will - * contain lower-case tag names, thus normally bypassing - * the case comparison code. - */ - for ( $i = 0; $i < $this->tag_name_length; $i++ ) { - $html_char = $this->html[ $this->tag_name_starts_at + $i ]; - $tag_char = $this->sought_tag_name[ $i ]; - - if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { - return false; - } - } - } - - if ( null !== $this->sought_class_name && ! $this->has_class( $this->sought_class_name ) ) { - return false; - } - - return true; - } - - /** - * Parser Ready State. - * - * Indicates that the parser is ready to run and waiting for a state transition. - * It may not have started yet, or it may have just finished parsing a token and - * is ready to find the next one. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_READY = 'STATE_READY'; - - /** - * Parser Complete State. - * - * Indicates that the parser has reached the end of the document and there is - * nothing left to scan. It finished parsing the last token completely. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_COMPLETE = 'STATE_COMPLETE'; - - /** - * Parser Incomplete Input State. - * - * Indicates that the parser has reached the end of the document before finishing - * a token. It started parsing a token but there is a possibility that the input - * HTML document was truncated in the middle of a token. - * - * The parser is reset at the start of the incomplete token and has paused. There - * is nothing more than can be scanned unless provided a more complete document. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_INCOMPLETE_INPUT = 'STATE_INCOMPLETE_INPUT'; - - /** - * Parser Matched Tag State. - * - * Indicates that the parser has found an HTML tag and it's possible to get - * the tag name and read or modify its attributes (if it's not a closing tag). - * - * @since 6.5.0 - * - * @access private - */ - const STATE_MATCHED_TAG = 'STATE_MATCHED_TAG'; - - /** - * Parser Text Node State. - * - * Indicates that the parser has found a text node and it's possible - * to read and modify that text. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_TEXT_NODE = 'STATE_TEXT_NODE'; - - /** - * Parser CDATA Node State. - * - * Indicates that the parser has found a CDATA node and it's possible - * to read and modify its modifiable text. Note that in HTML there are - * no CDATA nodes outside of foreign content (SVG and MathML). Outside - * of foreign content, they are treated as HTML comments. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_CDATA_NODE = 'STATE_CDATA_NODE'; - - /** - * Indicates that the parser has found an HTML comment and it's - * possible to read and modify its modifiable text. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_COMMENT = 'STATE_COMMENT'; - - /** - * Indicates that the parser has found a DOCTYPE node and it's - * possible to read and modify its modifiable text. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_DOCTYPE = 'STATE_DOCTYPE'; - - /** - * Indicates that the parser has found an empty tag closer ``. - * - * Note that in HTML there are no empty tag closers, and they - * are ignored. Nonetheless, the Tag Processor still - * recognizes them as they appear in the HTML stream. - * - * These were historically discussed as a "presumptuous tag - * closer," which would close the nearest open tag, but were - * dismissed in favor of explicitly-closing tags. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_PRESUMPTUOUS_TAG = 'STATE_PRESUMPTUOUS_TAG'; - - /** - * Indicates that the parser has found a "funky comment" - * and it's possible to read and modify its modifiable text. - * - * Example: - * - * - * - * - * - * Funky comments are tag closers with invalid tag names. Note - * that in HTML these are turn into bogus comments. Nonetheless, - * the Tag Processor recognizes them in a stream of HTML and - * exposes them for inspection and modification. - * - * @since 6.5.0 - * - * @access private - */ - const STATE_FUNKY_COMMENT = 'STATE_WP_FUNKY'; - - /** - * Indicates that a comment was created when encountering abruptly-closed HTML comment. - * - * Example: - * - * - * - * - * @since 6.5.0 - */ - const COMMENT_AS_ABRUPTLY_CLOSED_COMMENT = 'COMMENT_AS_ABRUPTLY_CLOSED_COMMENT'; - - /** - * Indicates that a comment would be parsed as a CDATA node, - * were HTML to allow CDATA nodes outside of foreign content. - * - * Example: - * - * - * - * This is an HTML comment, but it looks like a CDATA node. - * - * @since 6.5.0 - */ - const COMMENT_AS_CDATA_LOOKALIKE = 'COMMENT_AS_CDATA_LOOKALIKE'; - - /** - * Indicates that a comment was created when encountering - * normative HTML comment syntax. - * - * Example: - * - * - * - * @since 6.5.0 - */ - const COMMENT_AS_HTML_COMMENT = 'COMMENT_AS_HTML_COMMENT'; - - /** - * Indicates that a comment would be parsed as a Processing - * Instruction node, were they to exist within HTML. - * - * Example: - * - * - * - * This is an HTML comment, but it looks like a CDATA node. - * - * @since 6.5.0 - */ - const COMMENT_AS_PI_NODE_LOOKALIKE = 'COMMENT_AS_PI_NODE_LOOKALIKE'; - - /** - * Indicates that a comment was created when encountering invalid - * HTML input, a so-called "bogus comment." - * - * Example: - * - * - * - * - * @since 6.5.0 - */ - const COMMENT_AS_INVALID_HTML = 'COMMENT_AS_INVALID_HTML'; -} diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php deleted file mode 100644 index 6409255833c81..0000000000000 --- a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php +++ /dev/null @@ -1,64 +0,0 @@ -start = $start; - $this->length = $length; - $this->text = $text; - } -} diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php deleted file mode 100644 index ba18307002d15..0000000000000 --- a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php +++ /dev/null @@ -1,246 +0,0 @@ -get_tag() ) { - return null; - } - - $positions = $this->get_after_opener_tag_and_before_closer_tag_positions(); - if ( ! $positions ) { - return null; - } - list( $after_opener_tag, $before_closer_tag ) = $positions; - - return substr( $this->html, $after_opener_tag, $before_closer_tag - $after_opener_tag ); - } - - /** - * Sets the content between two balanced tags. - * - * @since 6.5.0 - * - * @access private - * - * @param string $new_content The string to replace the content between the matching tags. - * @return bool Whether the content was successfully replaced. - */ - public function set_content_between_balanced_tags( string $new_content ): bool { - $positions = $this->get_after_opener_tag_and_before_closer_tag_positions( true ); - if ( ! $positions ) { - return false; - } - list( $after_opener_tag, $before_closer_tag ) = $positions; - - $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( - $after_opener_tag, - $before_closer_tag - $after_opener_tag, - esc_html( $new_content ) - ); - - return true; - } - - /** - * Appends content after the closing tag of a template tag. - * - * It positions the cursor in the closer tag of the balanced template tag, - * if it exists. - * - * @access private - * - * @param string $new_content The string to append after the closing template tag. - * @return bool Whether the content was successfully appended. - */ - public function append_content_after_template_tag_closer( string $new_content ): bool { - if ( empty( $new_content ) || 'TEMPLATE' !== $this->get_tag() || ! $this->is_tag_closer() ) { - return false; - } - - // Flushes any changes. - $this->get_updated_html(); - - $bookmark = 'append_content_after_template_tag_closer'; - $this->set_bookmark( $bookmark ); - $after_closing_tag = $this->bookmarks[ $bookmark ]->start + $this->bookmarks[ $bookmark ]->length + 1; - $this->release_bookmark( $bookmark ); - - // Appends the new content. - $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $after_closing_tag, 0, $new_content ); - - return true; - } - - /** - * Gets the positions right after the opener tag and right before the closer - * tag in a balanced tag. - * - * By default, it positions the cursor in the closer tag of the balanced tag. - * If $rewind is true, it seeks back to the opener tag. - * - * @since 6.5.0 - * - * @access private - * - * @param bool $rewind Optional. Whether to seek back to the opener tag after finding the positions. Defaults to false. - * @return array|null Start and end byte position, or null when no balanced tag bookmarks. - */ - private function get_after_opener_tag_and_before_closer_tag_positions( bool $rewind = false ) { - // Flushes any changes. - $this->get_updated_html(); - - $bookmarks = $this->get_balanced_tag_bookmarks(); - if ( ! $bookmarks ) { - return null; - } - list( $opener_tag, $closer_tag ) = $bookmarks; - - $after_opener_tag = $this->bookmarks[ $opener_tag ]->start + $this->bookmarks[ $opener_tag ]->length + 1; - $before_closer_tag = $this->bookmarks[ $closer_tag ]->start; - - if ( $rewind ) { - $this->seek( $opener_tag ); - } - - $this->release_bookmark( $opener_tag ); - $this->release_bookmark( $closer_tag ); - - return array( $after_opener_tag, $before_closer_tag ); - } - - /** - * Returns a pair of bookmarks for the current opener tag and the matching - * closer tag. - * - * It positions the cursor in the closer tag of the balanced tag, if it - * exists. - * - * @since 6.5.0 - * - * @return array|null A pair of bookmarks, or null if there's no matching closing tag. - */ - private function get_balanced_tag_bookmarks() { - static $i = 0; - $opener_tag = 'opener_tag_of_balanced_tag_' . ++$i; - - $this->set_bookmark( $opener_tag ); - if ( ! $this->next_balanced_tag_closer_tag() ) { - $this->release_bookmark( $opener_tag ); - return null; - } - - $closer_tag = 'closer_tag_of_balanced_tag_' . ++$i; - $this->set_bookmark( $closer_tag ); - - return array( $opener_tag, $closer_tag ); - } - - /** - * Finds the matching closing tag for an opening tag. - * - * When called while the processor is on an open tag, it traverses the HTML - * until it finds the matching closer tag, respecting any in-between content, - * including nested tags of the same name. Returns false when called on a - * closer tag, a tag that doesn't have a closer tag (void), a tag that - * doesn't visit the closer tag, or if no matching closing tag was found. - * - * @since 6.5.0 - * - * @access private - * - * @return bool Whether a matching closing tag was found. - */ - public function next_balanced_tag_closer_tag(): bool { - $depth = 0; - $tag_name = $this->get_tag(); - - if ( ! $this->has_and_visits_its_closer_tag() ) { - return false; - } - - while ( $this->next_tag( - array( - 'tag_name' => $tag_name, - 'tag_closers' => 'visit', - ) - ) ) { - if ( ! $this->is_tag_closer() ) { - ++$depth; - continue; - } - - if ( 0 === $depth ) { - return true; - } - - --$depth; - } - - return false; - } - - /** - * Checks whether the current tag has and will visit its matching closer tag. - * - * @since 6.5.0 - * - * @access private - * - * @return bool Whether the current tag has a closer tag. - */ - public function has_and_visits_its_closer_tag(): bool { - $tag_name = $this->get_tag(); - - return null !== $tag_name && ( - ! Gutenberg_HTML_Processor_6_5::is_void( $tag_name ) && - ! in_array( $tag_name, self::TAGS_THAT_DONT_VISIT_CLOSER_TAG, true ) - ); - } - } -} diff --git a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php deleted file mode 100644 index d0661d7585726..0000000000000 --- a/lib/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php +++ /dev/null @@ -1,992 +0,0 @@ - 'data_wp_interactive_processor', - 'data-wp-router-region' => 'data_wp_router_region_processor', - 'data-wp-context' => 'data_wp_context_processor', - 'data-wp-bind' => 'data_wp_bind_processor', - 'data-wp-class' => 'data_wp_class_processor', - 'data-wp-style' => 'data_wp_style_processor', - 'data-wp-text' => 'data_wp_text_processor', - /* - * `data-wp-each` needs to be processed in the last place because it moves - * the cursor to the end of the processed items to prevent them to be - * processed twice. - */ - 'data-wp-each' => 'data_wp_each_processor', - ); - - /** - * Holds the initial state of the different Interactivity API stores. - * - * This state is used during the server directive processing. Then, it is - * serialized and sent to the client as part of the interactivity data to be - * recovered during the hydration of the client interactivity stores. - * - * @since 6.5.0 - * @var array - */ - private $state_data = array(); - - /** - * Holds the configuration required by the different Interactivity API stores. - * - * This configuration is serialized and sent to the client as part of the - * interactivity data and can be accessed by the client interactivity stores. - * - * @since 6.5.0 - * @var array - */ - private $config_data = array(); - - /** - * Flag that indicates whether the `data-wp-router-region` directive has - * been found in the HTML and processed. - * - * The value is saved in a private property of the WP_Interactivity_API - * instance instead of using a static variable inside the processor - * function, which would hold the same value for all instances - * independently of whether they have processed any - * `data-wp-router-region` directive or not. - * - * @since 6.5.0 - * @var bool - */ - private $has_processed_router_region = false; - - /** - * Gets and/or sets the initial state of an Interactivity API store for a - * given namespace. - * - * If state for that store namespace already exists, it merges the new - * provided state with the existing one. - * - * @since 6.5.0 - * - * @param string $store_namespace The unique store namespace identifier. - * @param array $state Optional. The array that will be merged with the existing state for the specified - * store namespace. - * @return array The current state for the specified store namespace. This will be the updated state if a $state - * argument was provided. - */ - public function state( string $store_namespace, array $state = array() ): array { - if ( ! isset( $this->state_data[ $store_namespace ] ) ) { - $this->state_data[ $store_namespace ] = array(); - } - if ( is_array( $state ) ) { - $this->state_data[ $store_namespace ] = array_replace_recursive( - $this->state_data[ $store_namespace ], - $state - ); - } - return $this->state_data[ $store_namespace ]; - } - - /** - * Gets and/or sets the configuration of the Interactivity API for a given - * store namespace. - * - * If configuration for that store namespace exists, it merges the new - * provided configuration with the existing one. - * - * @since 6.5.0 - * - * @param string $store_namespace The unique store namespace identifier. - * @param array $config Optional. The array that will be merged with the existing configuration for the - * specified store namespace. - * @return array The configuration for the specified store namespace. This will be the updated configuration if a - * $config argument was provided. - */ - public function config( string $store_namespace, array $config = array() ): array { - if ( ! isset( $this->config_data[ $store_namespace ] ) ) { - $this->config_data[ $store_namespace ] = array(); - } - if ( is_array( $config ) ) { - $this->config_data[ $store_namespace ] = array_replace_recursive( - $this->config_data[ $store_namespace ], - $config - ); - } - return $this->config_data[ $store_namespace ]; - } - - /** - * Prints the serialized client-side interactivity data. - * - * Encodes the config and initial state into JSON and prints them inside a - * script tag of type "application/json". Once in the browser, the state will - * be parsed and used to hydrate the client-side interactivity stores and the - * configuration will be available using a `getConfig` utility. - * - * @since 6.5.0 - */ - public function print_client_interactivity_data() { - if ( empty( $this->state_data ) && empty( $this->config_data ) ) { - return; - } - - $interactivity_data = array(); - - $config = array(); - foreach ( $this->config_data as $key => $value ) { - if ( ! empty( $value ) ) { - $config[ $key ] = $value; - } - } - if ( ! empty( $config ) ) { - $interactivity_data['config'] = $config; - } - - $state = array(); - foreach ( $this->state_data as $key => $value ) { - if ( ! empty( $value ) ) { - $state[ $key ] = $value; - } - } - if ( ! empty( $state ) ) { - $interactivity_data['state'] = $state; - } - - if ( ! empty( $interactivity_data ) ) { - wp_print_inline_script_tag( - wp_json_encode( - $interactivity_data, - JSON_HEX_TAG | JSON_HEX_AMP - ), - array( - 'type' => 'application/json', - 'id' => 'wp-interactivity-data', - ) - ); - } - } - - /** - * Registers the `@wordpress/interactivity` script modules. - * - * @since 6.5.0 - */ - public function register_script_modules() { - $suffix = wp_scripts_get_suffix(); - - wp_register_script_module( - '@wordpress/interactivity', - includes_url( "js/dist/interactivity$suffix.js" ) - ); - - wp_register_script_module( - '@wordpress/interactivity-router', - includes_url( "js/dist/interactivity-router$suffix.js" ), - array( '@wordpress/interactivity' ) - ); - } - - /** - * Adds the necessary hooks for the Interactivity API. - * - * @since 6.5.0 - */ - public function add_hooks() { - add_action( 'wp_enqueue_scripts', array( $this, 'register_script_modules' ) ); - add_action( 'wp_footer', array( $this, 'print_client_interactivity_data' ) ); - } - - /** - * Processes the interactivity directives contained within the HTML content - * and updates the markup accordingly. - * - * @since 6.5.0 - * - * @param string $html The HTML content to process. - * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags. - */ - public function process_directives( string $html ): string { - if ( ! str_contains( $html, 'data-wp-' ) ) { - return $html; - } - $context_stack = array(); - $namespace_stack = array(); - $result = $this->process_directives_args( $html, $context_stack, $namespace_stack ); - return null === $result ? $html : $result; - } - - /** - * Processes the interactivity directives contained within the HTML content - * and updates the markup accordingly. - * - * It needs the context and namespace stacks to be passed by reference, and - * it returns null if the HTML contains unbalanced tags. - * - * @since 6.5.0 - * - * @param string $html The HTML content to process. - * @param array $context_stack The reference to the array used to keep track of contexts during processing. - * @param array $namespace_stack The reference to the array used to manage namespaces during processing. - * @return string|null The processed HTML content. It returns null when the HTML contains unbalanced tags. - */ - private function process_directives_args( string $html, array &$context_stack, array &$namespace_stack ) { - $p = new WP_Interactivity_API_Directives_Processor( $html ); - $tag_stack = array(); - $unbalanced = false; - - $directive_processor_prefixes = array_keys( self::$directive_processors ); - $directive_processor_prefixes_reversed = array_reverse( $directive_processor_prefixes ); - - while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { - $tag_name = $p->get_tag(); - - if ( 'SVG' === $tag_name || 'MATH' === $tag_name ) { - $unbalanced = true; - break; - } - - if ( $p->is_tag_closer() ) { - list( $opening_tag_name, $directives_prefixes ) = end( $tag_stack ); - - if ( 0 === count( $tag_stack ) || $opening_tag_name !== $tag_name ) { - - /* - * If the tag stack is empty or the matching opening tag is not the - * same than the closing tag, it means the HTML is unbalanced and it - * stops processing it. - */ - $unbalanced = true; - break; - } else { - // Remove the last tag from the stack. - array_pop( $tag_stack ); - } - } else { - if ( 0 !== count( $p->get_attribute_names_with_prefix( 'data-wp-each-child' ) ) ) { - /* - * If the tag has a `data-wp-each-child` directive, jump to its closer - * tag because those tags have already been processed. - */ - $p->next_balanced_tag_closer_tag(); - continue; - } else { - $directives_prefixes = array(); - - // Checks if there is a server directive processor registered for each directive. - foreach ( $p->get_attribute_names_with_prefix( 'data-wp-' ) as $attribute_name ) { - list( $directive_prefix ) = $this->extract_prefix_and_suffix( $attribute_name ); - if ( array_key_exists( $directive_prefix, self::$directive_processors ) ) { - $directives_prefixes[] = $directive_prefix; - } - } - - /* - * If this tag will visit its closer tag, it adds it to the tag stack - * so it can process its closing tag and check for unbalanced tags. - */ - if ( $p->has_and_visits_its_closer_tag() ) { - $tag_stack[] = array( $tag_name, $directives_prefixes ); - } - } - } - /* - * If the matching opener tag didn't have any directives, it can skip the - * processing. - */ - if ( 0 === count( $directives_prefixes ) ) { - continue; - } - - // Directive processing might be different depending on if it is entering the tag or exiting it. - $modes = array( - 'enter' => ! $p->is_tag_closer(), - 'exit' => $p->is_tag_closer() || ! $p->has_and_visits_its_closer_tag(), - ); - foreach ( $modes as $mode => $should_run ) { - if ( ! $should_run ) { - continue; - } - - /* - * Sorts the attributes by the order of the `directives_processor` array - * and checks what directives are present in this element. - */ - $existing_directives_prefixes = array_intersect( - 'enter' === $mode ? $directive_processor_prefixes : $directive_processor_prefixes_reversed, - $directives_prefixes - ); - foreach ( $existing_directives_prefixes as $directive_prefix ) { - $func = is_array( self::$directive_processors[ $directive_prefix ] ) - ? self::$directive_processors[ $directive_prefix ] - : array( $this, self::$directive_processors[ $directive_prefix ] ); - - call_user_func_array( - $func, - array( $p, $mode, &$context_stack, &$namespace_stack, &$tag_stack ) - ); - } - } - } - - /* - * It returns null if the HTML is unbalanced because unbalanced HTML is - * not safe to process. In that case, the Interactivity API runtime will - * update the HTML on the client side during the hydration. - */ - return $unbalanced || 0 < count( $tag_stack ) ? null : $p->get_updated_html(); - } - - /** - * Evaluates the reference path passed to a directive based on the current - * store namespace, state and context. - * - * @since 6.5.0 - * - * @param string|true $directive_value The directive attribute value string or `true` when it's a boolean attribute. - * @param string $default_namespace The default namespace to use if none is explicitly defined in the directive - * value. - * @param array|false $context The current context for evaluating the directive or false if there is no - * context. - * @return mixed|null The result of the evaluation. Null if the reference path doesn't exist. - */ - private function evaluate( $directive_value, string $default_namespace, $context = false ) { - list( $ns, $path ) = $this->extract_directive_value( $directive_value, $default_namespace ); - if ( empty( $path ) ) { - return null; - } - - $store = array( - 'state' => $this->state_data[ $ns ] ?? array(), - 'context' => $context[ $ns ] ?? array(), - ); - - // Checks if the reference path is preceded by a negation operator (!). - $should_negate_value = '!' === $path[0]; - $path = $should_negate_value ? substr( $path, 1 ) : $path; - - // Extracts the value from the store using the reference path. - $path_segments = explode( '.', $path ); - $current = $store; - foreach ( $path_segments as $path_segment ) { - if ( isset( $current[ $path_segment ] ) ) { - $current = $current[ $path_segment ]; - } else { - return null; - } - } - - // Returns the opposite if it contains a negation operator (!). - return $should_negate_value ? ! $current : $current; - } - - /** - * Extracts the directive attribute name to separate and return the directive - * prefix and an optional suffix. - * - * The suffix is the string after the first double hyphen and the prefix is - * everything that comes before the suffix. - * - * Example: - * - * extract_prefix_and_suffix( 'data-wp-interactive' ) => array( 'data-wp-interactive', null ) - * extract_prefix_and_suffix( 'data-wp-bind--src' ) => array( 'data-wp-bind', 'src' ) - * extract_prefix_and_suffix( 'data-wp-foo--and--bar' ) => array( 'data-wp-foo', 'and--bar' ) - * - * @since 6.5.0 - * - * @param string $directive_name The directive attribute name. - * @return array An array containing the directive prefix and optional suffix. - */ - private function extract_prefix_and_suffix( string $directive_name ): array { - return explode( '--', $directive_name, 2 ); - } - - /** - * Parses and extracts the namespace and reference path from the given - * directive attribute value. - * - * If the value doesn't contain an explicit namespace, it returns the - * default one. If the value contains a JSON object instead of a reference - * path, the function tries to parse it and return the resulting array. If - * the value contains strings that represent booleans ("true" and "false"), - * numbers ("1" and "1.2") or "null", the function also transform them to - * regular booleans, numbers and `null`. - * - * Example: - * - * extract_directive_value( 'actions.foo', 'myPlugin' ) => array( 'myPlugin', 'actions.foo' ) - * extract_directive_value( 'otherPlugin::actions.foo', 'myPlugin' ) => array( 'otherPlugin', 'actions.foo' ) - * extract_directive_value( '{ "isOpen": false }', 'myPlugin' ) => array( 'myPlugin', array( 'isOpen' => false ) ) - * extract_directive_value( 'otherPlugin::{ "isOpen": false }', 'myPlugin' ) => array( 'otherPlugin', array( 'isOpen' => false ) ) - * - * @since 6.5.0 - * - * @param string|true $directive_value The directive attribute value. It can be `true` when it's a boolean - * attribute. - * @param string|null $default_namespace Optional. The default namespace if none is explicitly defined. - * @return array An array containing the namespace in the first item and the JSON, the reference path, or null on the - * second item. - */ - private function extract_directive_value( $directive_value, $default_namespace = null ): array { - if ( empty( $directive_value ) || is_bool( $directive_value ) ) { - return array( $default_namespace, null ); - } - - // Replaces the value and namespace if there is a namespace in the value. - if ( 1 === preg_match( '/^([\w\-_\/]+)::./', $directive_value ) ) { - list($default_namespace, $directive_value) = explode( '::', $directive_value, 2 ); - } - - /* - * Tries to decode the value as a JSON object. If it fails and the value - * isn't `null`, it returns the value as it is. Otherwise, it returns the - * decoded JSON or null for the string `null`. - */ - $decoded_json = json_decode( $directive_value, true ); - if ( null !== $decoded_json || 'null' === $directive_value ) { - $directive_value = $decoded_json; - } - - return array( $default_namespace, $directive_value ); - } - - /** - * Transforms a kebab-case string to camelCase. - * - * @param string $str The kebab-case string to transform to camelCase. - * @return string The transformed camelCase string. - */ - private function kebab_to_camel_case( string $str ): string { - return lcfirst( - preg_replace_callback( - '/(-)([a-z])/', - function ( $matches ) { - return strtoupper( $matches[2] ); - }, - strtolower( rtrim( $str, '-' ) ) - ) - ); - } - - /** - * Processes the `data-wp-interactive` directive. - * - * It adds the default store namespace defined in the directive value to the - * stack so that it's available for the nested interactivity elements. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_interactive_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - // When exiting tags, it removes the last namespace from the stack. - if ( 'exit' === $mode ) { - array_pop( $namespace_stack ); - return; - } - - // Tries to decode the `data-wp-interactive` attribute value. - $attribute_value = $p->get_attribute( 'data-wp-interactive' ); - - /* - * Pushes the newly defined namespace or the current one if the - * `data-wp-interactive` definition was invalid or does not contain a - * namespace. It does so because the function pops out the current namespace - * from the stack whenever it finds a `data-wp-interactive`'s closing tag, - * independently of whether the previous `data-wp-interactive` definition - * contained a valid namespace. - */ - $new_namespace = null; - if ( is_string( $attribute_value ) && ! empty( $attribute_value ) ) { - $decoded_json = json_decode( $attribute_value, true ); - if ( is_array( $decoded_json ) ) { - $new_namespace = $decoded_json['namespace'] ?? null; - } else { - $new_namespace = $attribute_value; - } - } - $namespace_stack[] = ( $new_namespace && 1 === preg_match( '/^([\w\-_\/]+)/', $new_namespace ) ) - ? $new_namespace - : end( $namespace_stack ); - } - - /** - * Processes the `data-wp-context` directive. - * - * It adds the context defined in the directive value to the stack so that - * it's available for the nested interactivity elements. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_context_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - // When exiting tags, it removes the last context from the stack. - if ( 'exit' === $mode ) { - array_pop( $context_stack ); - return; - } - - $attribute_value = $p->get_attribute( 'data-wp-context' ); - $namespace_value = end( $namespace_stack ); - - // Separates the namespace from the context JSON object. - list( $namespace_value, $decoded_json ) = is_string( $attribute_value ) && ! empty( $attribute_value ) - ? $this->extract_directive_value( $attribute_value, $namespace_value ) - : array( $namespace_value, null ); - - /* - * If there is a namespace, it adds a new context to the stack merging the - * previous context with the new one. - */ - if ( is_string( $namespace_value ) ) { - $context_stack[] = array_replace_recursive( - end( $context_stack ) !== false ? end( $context_stack ) : array(), - array( $namespace_value => is_array( $decoded_json ) ? $decoded_json : array() ) - ); - } else { - /* - * If there is no namespace, it pushes the current context to the stack. - * It needs to do so because the function pops out the current context - * from the stack whenever it finds a `data-wp-context`'s closing tag. - */ - $context_stack[] = end( $context_stack ); - } - } - - /** - * Processes the `data-wp-bind` directive. - * - * It updates or removes the bound attributes based on the evaluation of its - * associated reference. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_bind_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - if ( 'enter' === $mode ) { - $all_bind_directives = $p->get_attribute_names_with_prefix( 'data-wp-bind--' ); - - foreach ( $all_bind_directives as $attribute_name ) { - list( , $bound_attribute ) = $this->extract_prefix_and_suffix( $attribute_name ); - if ( empty( $bound_attribute ) ) { - return; - } - - $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); - - if ( null !== $result && ( - false !== $result || - ( strlen( $bound_attribute ) > 5 && '-' === $bound_attribute[4] ) - ) ) { - /* - * If the result of the evaluation is a boolean and the attribute is - * `aria-` or `data-, convert it to a string "true" or "false". It - * follows the exact same logic as Preact because it needs to - * replicate what Preact will later do in the client: - * https://github.com/preactjs/preact/blob/ea49f7a0f9d1ff2c98c0bdd66aa0cbc583055246/src/diff/props.js#L131C24-L136 - */ - if ( - is_bool( $result ) && - ( strlen( $bound_attribute ) > 5 && '-' === $bound_attribute[4] ) - ) { - $result = $result ? 'true' : 'false'; - } - $p->set_attribute( $bound_attribute, $result ); - } else { - $p->remove_attribute( $bound_attribute ); - } - } - } - } - - /** - * Processes the `data-wp-class` directive. - * - * It adds or removes CSS classes in the current HTML element based on the - * evaluation of its associated references. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_class_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - if ( 'enter' === $mode ) { - $all_class_directives = $p->get_attribute_names_with_prefix( 'data-wp-class--' ); - - foreach ( $all_class_directives as $attribute_name ) { - list( , $class_name ) = $this->extract_prefix_and_suffix( $attribute_name ); - if ( empty( $class_name ) ) { - return; - } - - $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); - - if ( $result ) { - $p->add_class( $class_name ); - } else { - $p->remove_class( $class_name ); - } - } - } - } - - /** - * Processes the `data-wp-style` directive. - * - * It updates the style attribute value of the current HTML element based on - * the evaluation of its associated references. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_style_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - if ( 'enter' === $mode ) { - $all_style_attributes = $p->get_attribute_names_with_prefix( 'data-wp-style--' ); - - foreach ( $all_style_attributes as $attribute_name ) { - list( , $style_property ) = $this->extract_prefix_and_suffix( $attribute_name ); - if ( empty( $style_property ) ) { - continue; - } - - $directive_attribute_value = $p->get_attribute( $attribute_name ); - $style_property_value = $this->evaluate( $directive_attribute_value, end( $namespace_stack ), end( $context_stack ) ); - $style_attribute_value = $p->get_attribute( 'style' ); - $style_attribute_value = ( $style_attribute_value && ! is_bool( $style_attribute_value ) ) ? $style_attribute_value : ''; - - /* - * Checks first if the style property is not falsy and the style - * attribute value is not empty because if it is, it doesn't need to - * update the attribute value. - */ - if ( $style_property_value || $style_attribute_value ) { - $style_attribute_value = $this->merge_style_property( $style_attribute_value, $style_property, $style_property_value ); - /* - * If the style attribute value is not empty, it sets it. Otherwise, - * it removes it. - */ - if ( ! empty( $style_attribute_value ) ) { - $p->set_attribute( 'style', $style_attribute_value ); - } else { - $p->remove_attribute( 'style' ); - } - } - } - } - } - - /** - * Merges an individual style property in the `style` attribute of an HTML - * element, updating or removing the property when necessary. - * - * If a property is modified, the old one is removed and the new one is added - * at the end of the list. - * - * @since 6.5.0 - * - * Example: - * - * merge_style_property( 'color:green;', 'color', 'red' ) => 'color:red;' - * merge_style_property( 'background:green;', 'color', 'red' ) => 'background:green;color:red;' - * merge_style_property( 'color:green;', 'color', null ) => '' - * - * @param string $style_attribute_value The current style attribute value. - * @param string $style_property_name The style property name to set. - * @param string|false|null $style_property_value The value to set for the style property. With false, null or an - * empty string, it removes the style property. - * @return string The new style attribute value after the specified property has been added, updated or removed. - */ - private function merge_style_property( string $style_attribute_value, string $style_property_name, $style_property_value ): string { - $style_assignments = explode( ';', $style_attribute_value ); - $result = array(); - $style_property_value = ! empty( $style_property_value ) ? rtrim( trim( $style_property_value ), ';' ) : null; - $new_style_property = $style_property_value ? $style_property_name . ':' . $style_property_value . ';' : ''; - - // Generates an array with all the properties but the modified one. - foreach ( $style_assignments as $style_assignment ) { - if ( empty( trim( $style_assignment ) ) ) { - continue; - } - list( $name, $value ) = explode( ':', $style_assignment ); - if ( trim( $name ) !== $style_property_name ) { - $result[] = trim( $name ) . ':' . trim( $value ) . ';'; - } - } - - // Adds the new/modified property at the end of the list. - $result[] = $new_style_property; - - return implode( '', $result ); - } - - /** - * Processes the `data-wp-text` directive. - * - * It updates the inner content of the current HTML element based on the - * evaluation of its associated reference. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - */ - private function data_wp_text_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack ) { - if ( 'enter' === $mode ) { - $attribute_value = $p->get_attribute( 'data-wp-text' ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); - - /* - * Follows the same logic as Preact in the client and only changes the - * content if the value is a string or a number. Otherwise, it removes the - * content. - */ - if ( is_string( $result ) || is_numeric( $result ) ) { - $p->set_content_between_balanced_tags( esc_html( $result ) ); - } else { - $p->set_content_between_balanced_tags( '' ); - } - } - } - - /** - * Returns the CSS styles for animating the top loading bar in the router. - * - * @since 6.5.0 - * - * @return string The CSS styles for the router's top loading bar animation. - */ - private function get_router_animation_styles(): string { - return <<
-
-HTML; - } - - /** - * Processes the `data-wp-router-region` directive. - * - * It renders in the footer a set of HTML elements to notify users about - * client-side navigations. More concretely, the elements added are 1) a - * top loading bar to visually inform that a navigation is in progress - * and 2) an `aria-live` region for accessible navigation announcements. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - */ - private function data_wp_router_region_processor( WP_Interactivity_API_Directives_Processor $p, string $mode ) { - if ( 'enter' === $mode && ! $this->has_processed_router_region ) { - $this->has_processed_router_region = true; - - // Initialize the `core/router` store. - $this->state( - 'core/router', - array( - 'navigation' => array( - 'texts' => array( - 'loading' => __( 'Loading page, please wait.' ), - 'loaded' => __( 'Page Loaded.' ), - ), - ), - ) - ); - - // Enqueues as an inline style. - wp_register_style( 'wp-interactivity-router-animations', false ); - wp_add_inline_style( 'wp-interactivity-router-animations', $this->get_router_animation_styles() ); - wp_enqueue_style( 'wp-interactivity-router-animations' ); - - // Adds the necessary markup to the footer. - add_action( 'wp_footer', array( $this, 'print_router_loading_and_screen_reader_markup' ) ); - } - } - - /** - * Processes the `data-wp-each` directive. - * - * This directive gets an array passed as reference and iterates over it - * generating new content for each item based on the inner markup of the - * `template` tag. - * - * @since 6.5.0 - * - * @param WP_Interactivity_API_Directives_Processor $p The directives processor instance. - * @param string $mode Whether the processing is entering or exiting the tag. - * @param array $context_stack The reference to the context stack. - * @param array $namespace_stack The reference to the store namespace stack. - * @param array $tag_stack The reference to the tag stack. - */ - private function data_wp_each_processor( WP_Interactivity_API_Directives_Processor $p, string $mode, array &$context_stack, array &$namespace_stack, array &$tag_stack ) { - if ( 'enter' === $mode && 'TEMPLATE' === $p->get_tag() ) { - $attribute_name = $p->get_attribute_names_with_prefix( 'data-wp-each' )[0]; - $extracted_suffix = $this->extract_prefix_and_suffix( $attribute_name ); - $item_name = isset( $extracted_suffix[1] ) ? $this->kebab_to_camel_case( $extracted_suffix[1] ) : 'item'; - $attribute_value = $p->get_attribute( $attribute_name ); - $result = $this->evaluate( $attribute_value, end( $namespace_stack ), end( $context_stack ) ); - - // Gets the content between the template tags and leaves the cursor in the closer tag. - $inner_content = $p->get_content_between_balanced_template_tags(); - - // Checks if there is a manual server-side directive processing. - $template_end = 'data-wp-each: template end'; - $p->set_bookmark( $template_end ); - $p->next_tag(); - $manual_sdp = $p->get_attribute( 'data-wp-each-child' ); - $p->seek( $template_end ); // Rewinds to the template closer tag. - $p->release_bookmark( $template_end ); - - /* - * It doesn't process in these situations: - * - Manual server-side directive processing. - * - Empty or non-array values. - * - Associative arrays because those are deserialized as objects in JS. - * - Templates that contain top-level texts because those texts can't be - * identified and removed in the client. - */ - if ( - $manual_sdp || - empty( $result ) || - ! is_array( $result ) || - ! array_is_list( $result ) || - ! str_starts_with( trim( $inner_content ), '<' ) || - ! str_ends_with( trim( $inner_content ), '>' ) - ) { - array_pop( $tag_stack ); - return; - } - - // Extracts the namespace from the directive attribute value. - $namespace_value = end( $namespace_stack ); - list( $namespace_value ) = is_string( $attribute_value ) && ! empty( $attribute_value ) - ? $this->extract_directive_value( $attribute_value, $namespace_value ) - : array( $namespace_value, null ); - - // Processes the inner content for each item of the array. - $processed_content = ''; - foreach ( $result as $item ) { - // Creates a new context that includes the current item of the array. - $context_stack[] = array_replace_recursive( - end( $context_stack ) !== false ? end( $context_stack ) : array(), - array( $namespace_value => array( $item_name => $item ) ) - ); - - // Processes the inner content with the new context. - $processed_item = $this->process_directives_args( $inner_content, $context_stack, $namespace_stack ); - - if ( null === $processed_item ) { - // If the HTML is unbalanced, stop processing it. - array_pop( $context_stack ); - return; - } - - // Adds the `data-wp-each-child` to each top-level tag. - $i = new WP_Interactivity_API_Directives_Processor( $processed_item ); - while ( $i->next_tag() ) { - $i->set_attribute( 'data-wp-each-child', true ); - $i->next_balanced_tag_closer_tag(); - } - $processed_content .= $i->get_updated_html(); - - // Removes the current context from the stack. - array_pop( $context_stack ); - } - - // Appends the processed content after the tag closer of the template. - $p->append_content_after_template_tag_closer( $processed_content ); - - // Pops the last tag because it skipped the closing tag of the template tag. - array_pop( $tag_stack ); - } - } - } -} diff --git a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php b/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php deleted file mode 100644 index 20e4365a48b6a..0000000000000 --- a/lib/compat/wordpress-6.5/interactivity-api/interactivity-api.php +++ /dev/null @@ -1,205 +0,0 @@ -get_registered( $block_name ); - - if ( - isset( $block_name ) && - ( ( isset( $block_type->supports['interactivity'] ) && true === $block_type->supports['interactivity'] ) || - ( isset( $block_type->supports['interactivity']['interactive'] ) && true === $block_type->supports['interactivity']['interactive'] ) ) - ) { - // Annotates the root interactive block for processing. - $root_interactive_block = array( $block_name, $parsed_block ); - - /* - * Adds a filter to process the root interactive block once it has - * finished rendering. - */ - $process_interactive_blocks = static function ( string $content, array $parsed_block ) use ( &$root_interactive_block, &$process_interactive_blocks ): string { - // Checks whether the current block is the root interactive block. - list($root_block_name, $root_parsed_block) = $root_interactive_block; - if ( $root_block_name === $parsed_block['blockName'] && $parsed_block === $root_parsed_block ) { - // The root interactive blocks has finished rendering, process it. - $content = wp_interactivity_process_directives( $content ); - // Removes the filter and reset the root interactive block. - remove_filter( 'render_block_' . $parsed_block['blockName'], $process_interactive_blocks ); - $root_interactive_block = null; - } - return $content; - }; - - /* - * Uses a priority of 100 to ensure that other filters can add additional - * directives before the processing starts. - */ - add_filter( 'render_block_' . $block_name, $process_interactive_blocks, 100, 2 ); - } - } - - return $parsed_block; - } - /* - * Uses a priority of 100 to ensure that other filters can edit $parsed_block - * without crashing the SSR. - */ - add_filter( 'render_block_data', 'wp_interactivity_process_directives_of_interactive_blocks', 100 ); -} - -if ( ! function_exists( 'wp_interactivity' ) ) { - /** - * Retrieves the main WP_Interactivity_API instance. - * - * It provides access to the WP_Interactivity_API instance, creating one if it - * doesn't exist yet. - * - * @since 6.5.0 - * - * @global WP_Interactivity_API $wp_interactivity - * - * @return WP_Interactivity_API The main WP_Interactivity_API instance. - */ - function wp_interactivity(): WP_Interactivity_API { - global $wp_interactivity; - if ( ! ( $wp_interactivity instanceof WP_Interactivity_API ) ) { - $wp_interactivity = new WP_Interactivity_API(); - } - return $wp_interactivity; - } - - wp_interactivity()->add_hooks(); -} - -if ( ! function_exists( 'wp_interactivity_process_directives' ) ) { - /** - * Processes the interactivity directives contained within the HTML content - * and updates the markup accordingly. - * - * @since 6.5.0 - * - * @param string $html The HTML content to process. - * @return string The processed HTML content. It returns the original content when the HTML contains unbalanced tags. - */ - function wp_interactivity_process_directives( string $html ): string { - return wp_interactivity()->process_directives( $html ); - } -} - -if ( ! function_exists( 'wp_interactivity_state' ) ) { - /** - * Gets and/or sets the initial state of an Interactivity API store for a - * given namespace. - * - * If state for that store namespace already exists, it merges the new - * provided state with the existing one. - * - * @since 6.5.0 - * - * @param string $store_namespace The unique store namespace identifier. - * @param array $state Optional. The array that will be merged with the existing state for the specified - * store namespace. - * @return array The state for the specified store namespace. This will be the updated state if a $state argument was - * provided. - */ - function wp_interactivity_state( string $store_namespace, array $state = array() ): array { - return wp_interactivity()->state( $store_namespace, $state ); - } -} - -if ( ! function_exists( 'wp_interactivity_config' ) ) { - /** - * Gets and/or sets the configuration of the Interactivity API for a given - * store namespace. - * - * If configuration for that store namespace exists, it merges the new - * provided configuration with the existing one. - * - * @since 6.5.0 - * - * @param string $store_namespace The unique store namespace identifier. - * @param array $config Optional. The array that will be merged with the existing configuration for the - * specified store namespace. - * @return array The configuration for the specified store namespace. This will be the updated configuration if a - * $config argument was provided. - */ - function wp_interactivity_config( string $store_namespace, array $config = array() ): array { - return wp_interactivity()->config( $store_namespace, $config ); - } -} - -if ( ! function_exists( 'wp_interactivity_data_wp_context' ) ) { - /** - * Generates a `data-wp-context` directive attribute by encoding a context - * array. - * - * This helper function simplifies the creation of `data-wp-context` directives - * by providing a way to pass an array of data, which encodes into a JSON string - * safe for direct use as a HTML attribute value. - * - * Example: - * - *
true, 'count' => 0 ) ); ?>> - * - * @since 6.5.0 - * - * @param array $context The array of context data to encode. - * @param string $store_namespace Optional. The unique store namespace identifier. - * @return string A complete `data-wp-context` directive with a JSON encoded value representing the context array and - * the store namespace if specified. - */ - function wp_interactivity_data_wp_context( array $context, string $store_namespace = '' ): string { - return 'data-wp-context=\'' . - ( $store_namespace ? $store_namespace . '::' : '' ) . - ( empty( $context ) ? '{}' : wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ) ) . - '\''; - } -} - -if ( ! function_exists( 'data_wp_context' ) ) { - /** - * `data_wp_context()` was renamed to follow WordPress Core naming schemes. - * - * @link https://github.com/WordPress/gutenberg/pull/59465/ - * @link https://core.trac.wordpress.org/ticket/60575 - * - * @since 6.5.0 - * @deprecated 6.5.0 - * - * @param array $context The array of context data to encode. - * @param string $store_namespace Optional. The unique store namespace identifier. - * @return string A complete `data-wp-context` directive with a JSON encoded value representing the context array and - * the store namespace if specified. - */ - function data_wp_context( array $context, string $store_namespace = '' ): string { - _deprecated_function( __FUNCTION__, '6.5', 'wp_interactivity_data_wp_context()' ); - return wp_interactivity_data_wp_context( $context, $store_namespace ); - } -} diff --git a/lib/compat/wordpress-6.5/kses.php b/lib/compat/wordpress-6.5/kses.php deleted file mode 100644 index 038d78645786f..0000000000000 --- a/lib/compat/wordpress-6.5/kses.php +++ /dev/null @@ -1,18 +0,0 @@ -show_in_nav_menus ) { - $variation = build_variation_for_navigation_link( $post_type_object, 'post-type' ); - gutenberg_block_core_navigation_link_register_variation( $variation ); - } -} - -/** - * Registers a custom taxonomy variation for navigation link on taxonomy registration - * Handles all taxonomies registered after the block is registered in register_navigation_link_post_type_variations - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param string $taxonomy Taxonomy slug. - * @param array|string $object_type Object type or array of object types. - * @param array $args Array of taxonomy registration arguments. - */ -function gutenberg_block_core_navigation_link_register_taxonomy_variation( $taxonomy, $object_type, $args ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - if ( isset( $args['show_in_nav_menus'] ) && $args['show_in_nav_menus'] ) { - $variation = build_variation_for_navigation_link( (object) $args, 'post-type' ); - gutenberg_block_core_navigation_link_register_variation( $variation ); - } -} - -/** - * Unregisters a custom post type variation for navigation link on post type unregistration. - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param string $post_type The post type name passed from unregistered_post_type action hook. - */ -function gutenberg_block_core_navigation_link_unregister_post_type_variation( $post_type ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - gutenberg_block_core_navigation_link_unregister_variation( $post_type ); -} - -/** - * Unregisters a custom taxonomy variation for navigation link on taxonomy unregistration. - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param string $taxonomy The taxonomy name passed from unregistered_taxonomy action hook. - */ -function gutenberg_block_core_navigation_link_unregister_taxonomy_variation( $taxonomy ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - gutenberg_block_core_navigation_link_unregister_variation( $taxonomy ); -} - -/** - * Registers a variation for a post type / taxonomy for the navigation link block. - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param array $variation Variation array from build_variation_for_navigation_link. - */ -function gutenberg_block_core_navigation_link_register_variation( $variation ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - /* - * Directly set the variations on the registered block type - * because there's no server side registration for variations (see #47170). - */ - $navigation_block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/navigation-link' ); - /* - * If the block is not registered yet, bail early. - * Variation will be registered in register_block_core_navigation_link then. - */ - if ( ! $navigation_block_type ) { - return; - } - - $navigation_block_type->variations = array_merge( - $navigation_block_type->variations, - array( $variation ) - ); -} - -/** - * Unregisters a variation for a post type / taxonomy for the navigation link block. - * - * @since 6.5.0 - * @deprecated 6.5.0 Use WP_Block_Type::get_variations / get_block_type_variations filter instead. - * - * @param string $name Name of the post type / taxonomy (which was used as variation name). - */ -function gutenberg_block_core_navigation_link_unregister_variation( $name ) { - _deprecated_function( __FUNCTION__, '6.5.0', 'WP_Block_Type::get_variations' ); - /* - * Directly get the variations from the registered block type - * because there's no server side (un)registration for variations (see #47170). - */ - $navigation_block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/navigation-link' ); - // If the block is not registered (yet), there's no need to remove a variation. - if ( ! $navigation_block_type || empty( $navigation_block_type->variations ) ) { - return; - } - $variations = $navigation_block_type->variations; - // Search for the variation and remove it from the array. - foreach ( $variations as $i => $variation ) { - if ( $variation['name'] === $name ) { - unset( $variations[ $i ] ); - break; - } - } - // Reindex array after removing one variation. - $navigation_block_type->variations = array_values( $variations ); -} diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php deleted file mode 100644 index d18756844cc91..0000000000000 --- a/lib/compat/wordpress-6.5/rest-api.php +++ /dev/null @@ -1,136 +0,0 @@ -get( 'Name' ); - return empty( $theme_name ) ? $template_object['theme'] : $theme_name; - case 'plugin': - $plugins = get_plugins(); - $plugin = $plugins[ plugin_basename( sanitize_text_field( $template_object['theme'] . '.php' ) ) ]; - return empty( $plugin['Name'] ) ? $template_object['theme'] : $plugin['Name']; - case 'site': - return get_bloginfo( 'name' ); - case 'user': - $author = get_user_by( 'id', $template_object['author'] ); - if ( ! $author ) { - return __( 'Unknown author', 'gutenberg' ); - } - return $author->get( 'display_name' ); - } -} - -/** - * Registers additional fields for wp_template and wp_template_part rest api. - * - * @access private - * @internal - */ -function _gutenberg_register_wp_templates_additional_fields() { - register_rest_field( - array( 'wp_template', 'wp_template_part' ), - 'author_text', - array( - 'get_callback' => '_gutenberg_get_wp_templates_author_text_field', - 'update_callback' => null, - 'schema' => array( - 'type' => 'string', - 'description' => __( 'Human readable text for the author.', 'gutenberg' ), - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - ) - ); - - register_rest_field( - array( 'wp_template', 'wp_template_part' ), - 'original_source', - array( - 'get_callback' => '_gutenberg_get_wp_templates_original_source_field', - 'update_callback' => null, - 'schema' => array( - 'description' => __( 'Where the template originally comes from e.g. \'theme\'', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - 'enum' => array( - 'theme', - 'plugin', - 'site', - 'user', - ), - ), - ) - ); -} - -add_action( 'rest_api_init', '_gutenberg_register_wp_templates_additional_fields' ); diff --git a/lib/compat/wordpress-6.5/script-loader.php b/lib/compat/wordpress-6.5/script-loader.php deleted file mode 100644 index a77134d9182f9..0000000000000 --- a/lib/compat/wordpress-6.5/script-loader.php +++ /dev/null @@ -1,207 +0,0 @@ -query( 'wp-date', 'registered' ) ) { - global $wp_locale; - // Calculate the timezone abbr (EDT, PST) if possible. - $timezone_string = get_option( 'timezone_string', 'UTC' ); - $timezone_abbr = ''; - - if ( ! empty( $timezone_string ) ) { - $timezone_date = new DateTime( 'now', new DateTimeZone( $timezone_string ) ); - $timezone_abbr = $timezone_date->format( 'T' ); - } - - $gmt_offset = get_option( 'gmt_offset', 0 ); - - $scripts->registered['wp-date']->extra['after'] = array( - false, - sprintf( - 'wp.date.setSettings( %s );', - wp_json_encode( - array( - 'l10n' => array( - 'locale' => get_user_locale(), - 'months' => array_values( $wp_locale->month ), - 'monthsShort' => array_values( $wp_locale->month_abbrev ), - 'weekdays' => array_values( $wp_locale->weekday ), - 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), - 'meridiem' => (object) $wp_locale->meridiem, - 'relative' => array( - /* translators: %s: Duration. */ - 'future' => __( '%s from now', 'gutenberg' ), - /* translators: %s: Duration. */ - 'past' => __( '%s ago', 'gutenberg' ), - /* translators: One second from or to a particular datetime, e.g., "a second ago" or "a second from now". */ - 's' => __( 'a second', 'gutenberg' ), - /* translators: %d: Duration in seconds from or to a particular datetime, e.g., "4 seconds ago" or "4 seconds from now". */ - 'ss' => __( '%d seconds', 'gutenberg' ), - /* translators: One minute from or to a particular datetime, e.g., "a minute ago" or "a minute from now". */ - 'm' => __( 'a minute', 'gutenberg' ), - /* translators: %d: Duration in minutes from or to a particular datetime, e.g., "4 minutes ago" or "4 minutes from now". */ - 'mm' => __( '%d minutes', 'gutenberg' ), - /* translators: One hour from or to a particular datetime, e.g., "an hour ago" or "an hour from now". */ - 'h' => __( 'an hour', 'gutenberg' ), - /* translators: %d: Duration in hours from or to a particular datetime, e.g., "4 hours ago" or "4 hours from now". */ - 'hh' => __( '%d hours', 'gutenberg' ), - /* translators: One day from or to a particular datetime, e.g., "a day ago" or "a day from now". */ - 'd' => __( 'a day', 'gutenberg' ), - /* translators: %d: Duration in days from or to a particular datetime, e.g., "4 days ago" or "4 days from now". */ - 'dd' => __( '%d days', 'gutenberg' ), - /* translators: One month from or to a particular datetime, e.g., "a month ago" or "a month from now". */ - 'M' => __( 'a month', 'gutenberg' ), - /* translators: %d: Duration in months from or to a particular datetime, e.g., "4 months ago" or "4 months from now". */ - 'MM' => __( '%d months', 'gutenberg' ), - /* translators: One year from or to a particular datetime, e.g., "a year ago" or "a year from now". */ - 'y' => __( 'a year', 'gutenberg' ), - /* translators: %d: Duration in years from or to a particular datetime, e.g., "4 years ago" or "4 years from now". */ - 'yy' => __( '%d years', 'gutenberg' ), - ), - 'startOfWeek' => (int) get_option( 'start_of_week', 0 ), - ), - 'formats' => array( - /* translators: Time format, see https://www.php.net/manual/datetime.format.php */ - 'time' => get_option( 'time_format', __( 'g:i a', 'default' ) ), - /* translators: Date format, see https://www.php.net/manual/datetime.format.php */ - 'date' => get_option( 'date_format', __( 'F j, Y', 'default' ) ), - /* translators: Date/Time format, see https://www.php.net/manual/datetime.format.php */ - 'datetime' => __( 'F j, Y g:i a', 'default' ), - /* translators: Abbreviated date/time format, see https://www.php.net/manual/datetime.format.php */ - 'datetimeAbbreviated' => __( 'M j, Y g:i a', 'default' ), - ), - 'timezone' => array( - 'offset' => (float) $gmt_offset, - 'offsetFormatted' => str_replace( array( '.25', '.5', '.75' ), array( ':15', ':30', ':45' ), (string) $gmt_offset ), - 'string' => $timezone_string, - 'abbr' => $timezone_abbr, - ), - ) - ) - ), - ); - } -} - -add_action( 'wp_default_scripts', 'gutenberg_update_wp_date_settings' ); - -/** - * Prints inline JavaScript wrapped in ` - * - * In an HTML document this would print "…" to the console, - * but in an XHTML document it would print "…" to the console. - * - * - * - * In an HTML document this would print "An image is in HTML", - * but it's an invalid XHTML document because it interprets the `` - * as an empty tag missing its closing `/`. - * - * @see https://www.w3.org/TR/xhtml1/#h-4.8 - */ - if ( - ! $is_html5 && - ( - ! isset( $attributes['type'] ) || - 'module' === $attributes['type'] || - str_contains( $attributes['type'], 'javascript' ) || - str_contains( $attributes['type'], 'ecmascript' ) || - str_contains( $attributes['type'], 'jscript' ) || - str_contains( $attributes['type'], 'livescript' ) - ) - ) { - /* - * If the string `]]>` exists within the JavaScript it would break - * out of any wrapping CDATA section added here, so to start, it's - * necessary to escape that sequence which requires splitting the - * content into two CDATA sections wherever it's found. - * - * Note: it's only necessary to escape the closing `]]>` because - * an additional `', ']]]]>', $data ); - - // Wrap the entire escaped script inside a CDATA section. - $data = sprintf( "/* */", $data ); - } - - $data = "\n" . trim( $data, "\n\r " ) . "\n"; - - /** - * Filters attributes to be added to a script tag. - * - * @since 5.7.0 - * - * @param array $attributes Key-value pairs representing `\n", wp_sanitize_script_attributes( $attributes ), $data ); -} diff --git a/lib/compat/wordpress-6.5/scripts-modules.php b/lib/compat/wordpress-6.5/scripts-modules.php deleted file mode 100644 index 110ef858eb8ce..0000000000000 --- a/lib/compat/wordpress-6.5/scripts-modules.php +++ /dev/null @@ -1,224 +0,0 @@ -add_hooks(); - - /** - * Add module fields from block metadata to WP_Block_Type settings. - * - * This filter allows us to register modules from block metadata and attach additional fields to - * WP_Block_Type instances. - * - * @param array $settings Array of determined settings for registering a block type. - * @param array $metadata Metadata provided for registering a block type. - */ - function gutenberg_filter_block_type_metadata_settings_register_modules( $settings, $metadata = null ) { - $module_fields = array( - 'viewScriptModule' => 'view_script_module_ids', - ); - foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { - if ( ! empty( $settings[ $metadata_field_name ] ) ) { - $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; - } - if ( ! empty( $metadata[ $metadata_field_name ] ) ) { - $modules = $metadata[ $metadata_field_name ]; - $processed_modules = array(); - if ( is_array( $modules ) ) { - for ( $index = 0; $index < count( $modules ); $index++ ) { - $processed_modules[] = gutenberg_register_block_module_id( - $metadata, - $metadata_field_name, - $index - ); - } - } else { - $processed_modules[] = gutenberg_register_block_module_id( - $metadata, - $metadata_field_name - ); - } - $settings[ $settings_field_name ] = $processed_modules; - } - } - - return $settings; - } - - add_filter( 'block_type_metadata_settings', 'gutenberg_filter_block_type_metadata_settings_register_modules', 10, 2 ); - - /** - * Enqueue modules associated with the block. - * - * @param string $block_content The block content. - * @param array $parsed_block The full block, including name and attributes. - * @param WP_Block $block_instance The block instance. - */ - function gutenberg_filter_render_block_enqueue_view_script_modules( $block_content, $parsed_block, $block_instance ) { - $block_type = $block_instance->block_type; - - if ( ! empty( $block_type->view_script_module_ids ) ) { - foreach ( $block_type->view_script_module_ids as $module_id ) { - wp_enqueue_script_module( $module_id ); - } - } - - return $block_content; - } - - add_filter( 'render_block', 'gutenberg_filter_render_block_enqueue_view_script_modules', 10, 3 ); - - /** - * Registers a REST field for block types to provide view script module IDs. - * - * Adds the `view_script_module_ids` and `view_module_ids` (deprecated) field to block type objects in the REST API, which - * lists the script module IDs for any script modules associated with the - * block's viewScriptModule key. - */ - function gutenberg_register_view_script_module_ids_rest_field() { - register_rest_field( - 'block-type', - 'view_script_module_ids', - array( - 'get_callback' => function ( $item ) { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $item['name'] ); - if ( isset( $block_type->view_script_module_ids ) ) { - return $block_type->view_script_module_ids; - } - return array(); - }, - ) - ); - } - - add_action( 'rest_api_init', 'gutenberg_register_view_script_module_ids_rest_field' ); -} - -if ( ! function_exists( 'wp_register_script_module' ) ) { - /** - * Registers the script module if no script module with that script module - * identifier has already been registered. - * - * @since 6.5.0 - * - * @param string $id The identifier of the script module. Should be unique. It will be used in the - * final import map. - * @param string $src Optional. Full URL of the script module, or path of the script module relative - * to the WordPress root directory. If it is provided and the script module has - * not been registered yet, it will be registered. - * @param array $deps { - * Optional. List of dependencies. - * - * @type string|array $0... { - * An array of script module identifiers of the dependencies of this script - * module. The dependencies can be strings or arrays. If they are arrays, - * they need an `id` key with the script module identifier, and can contain - * an `import` key with either `static` or `dynamic`. By default, - * dependencies that don't contain an `import` key are considered static. - * - * @type string $id The script module identifier. - * @type string $import Optional. Import type. May be either `static` or - * `dynamic`. Defaults to `static`. - * } - * } - * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. - * It is added to the URL as a query string for cache busting purposes. If $version - * is set to false, the version number is the currently installed WordPress version. - * If $version is set to null, no version is added. - */ - function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false ) { - wp_script_modules()->register( $id, $src, $deps, $version ); - } -} - -if ( ! function_exists( 'wp_enqueue_script_module' ) ) { - /** - * Marks the script module to be enqueued in the page. - * - * If a src is provided and the script module has not been registered yet, it - * will be registered. - * - * @since 6.5.0 - * - * @param string $id The identifier of the script module. Should be unique. It will be used in the - * final import map. - * @param string $src Optional. Full URL of the script module, or path of the script module relative - * to the WordPress root directory. If it is provided and the script module has - * not been registered yet, it will be registered. - * @param array $deps { - * Optional. List of dependencies. - * - * @type string|array $0... { - * An array of script module identifiers of the dependencies of this script - * module. The dependencies can be strings or arrays. If they are arrays, - * they need an `id` key with the script module identifier, and can contain - * an `import` key with either `static` or `dynamic`. By default, - * dependencies that don't contain an `import` key are considered static. - * - * @type string $id The script module identifier. - * @type string $import Optional. Import type. May be either `static` or - * `dynamic`. Defaults to `static`. - * } - * } - * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false. - * It is added to the URL as a query string for cache busting purposes. If $version - * is set to false, the version number is the currently installed WordPress version. - * If $version is set to null, no version is added. - */ - function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false ) { - wp_script_modules()->enqueue( $id, $src, $deps, $version ); - } -} - -if ( ! function_exists( 'wp_dequeue_script_module' ) ) { - /** - * Unmarks the script module so it is no longer enqueued in the page. - * - * @since 6.5.0 - * - * @param string $id The identifier of the script module. - */ - function wp_dequeue_script_module( string $id ) { - wp_script_modules()->dequeue( $id ); - } -} - -if ( ! function_exists( 'wp_deregister_script_module' ) ) { - /** - * Deregisters the script module. - * - * @since 6.5.0 - * - * @param string $id The identifier of the script module. - */ - function wp_deregister_script_module( string $id ) { - wp_script_modules()->deregister( $id ); - } -} diff --git a/lib/load.php b/lib/load.php index ef1b5cfe50b6a..1c89de4eceab8 100644 --- a/lib/load.php +++ b/lib/load.php @@ -35,17 +35,6 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/experimental/class-wp-rest-block-editor-settings-controller.php'; } - // WordPress 6.4 compat. - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-templates-controller-6-4.php'; - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php'; - require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php'; - require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php'; - require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php'; - - // WordPress 6.5 compat. - require_once __DIR__ . '/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php'; - require_once __DIR__ . '/compat/wordpress-6.5/rest-api.php'; - // WordPress 6.6 compat. require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-global-styles-revisions-controller-6-6.php'; require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php'; @@ -74,26 +63,11 @@ function gutenberg_is_experiment_enabled( $name ) { // Gutenberg plugin compat. require __DIR__ . '/compat/plugin/edit-site-routes-backwards-compat.php'; -require __DIR__ . '/compat/plugin/footnotes.php'; // The Token Map was created during 6.6 in order to support the HTML API. It must be loaded before it. require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-token-map-6-6.php'; require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-token-map-6-7.php'; -/* - * There are upstream updates to the Tag Processor that may not appear if Gutenberg is running - * a version of WordPress newer than 6.4 and older than the latest `trunk`. This file should - * always be loaded so that Gutenberg code can run the newest version of the Tag Processor. - */ -require __DIR__ . '/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-open-elements-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-state-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php'; -require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-processor-6-5.php'; - require __DIR__ . '/compat/wordpress-6.6/html-api/gutenberg-html5-named-character-references-6-6.php'; require __DIR__ . '/compat/wordpress-6.6/html-api/class-gutenberg-html-decoder-6-6.php'; require __DIR__ . '/compat/wordpress-6.6/html-api/class-gutenberg-html-tag-processor-6-6.php'; @@ -116,46 +90,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-state-6-7.php'; require __DIR__ . '/compat/wordpress-6.7/html-api/class-gutenberg-html-processor-6-7.php'; -/* - * The HTML Processor appeared after WordPress 6.3. If Gutenberg is running on a version of - * WordPress before it was introduced, these verbatim Core files will be missing. - */ -if ( ! class_exists( 'WP_HTML_Processor' ) ) { - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-active-formatting-elements.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-open-elements.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-processor-state.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-token.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-unsupported-exception.php'; - require __DIR__ . '/compat/wordpress-6.4/html-api/class-wp-html-processor.php'; -} - -// WordPress 6.4 compat. -require __DIR__ . '/compat/wordpress-6.4/blocks.php'; -require __DIR__ . '/compat/wordpress-6.4/block-hooks.php'; -require __DIR__ . '/compat/wordpress-6.4/script-loader.php'; -require __DIR__ . '/compat/wordpress-6.4/kses.php'; - -// WordPress 6.5 compat. -require __DIR__ . '/compat/wordpress-6.5/compat.php'; -require __DIR__ . '/compat/wordpress-6.5/blocks.php'; -require __DIR__ . '/compat/wordpress-6.5/block-patterns.php'; -require __DIR__ . '/compat/wordpress-6.5/kses.php'; -require __DIR__ . '/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api.php'; -require __DIR__ . '/compat/wordpress-6.5/interactivity-api/class-wp-interactivity-api-directives-processor.php'; -require __DIR__ . '/compat/wordpress-6.5/interactivity-api/interactivity-api.php'; -require __DIR__ . '/compat/wordpress-6.5/class-wp-script-modules.php'; -require __DIR__ . '/compat/wordpress-6.5/scripts-modules.php'; -require __DIR__ . '/compat/wordpress-6.5/navigation-block-variations.php'; -if ( ! class_exists( 'WP_Block_Bindings_Source' ) ) { - require __DIR__ . '/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-source.php'; -} -if ( ! class_exists( 'WP_Block_Bindings_Registry' ) ) { - require __DIR__ . '/compat/wordpress-6.5/block-bindings/class-wp-block-bindings-registry.php'; -} -require __DIR__ . '/compat/wordpress-6.5/block-bindings/block-bindings.php'; -require __DIR__ . '/compat/wordpress-6.5/block-bindings/post-meta.php'; -require __DIR__ . '/compat/wordpress-6.5/script-loader.php'; - // WordPress 6.6 compat. require __DIR__ . '/compat/wordpress-6.6/admin-bar.php'; require __DIR__ . '/compat/wordpress-6.6/blocks.php'; @@ -188,28 +122,6 @@ function gutenberg_is_experiment_enabled( $name ) { // Fonts API / Font Face. remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler. -// Loads the Font Library. -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-collection.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-library.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-font-utils.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-families-controller.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-faces-controller.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/class-wp-rest-font-collections-controller.php'; -require __DIR__ . '/compat/wordpress-6.5/fonts/fonts.php'; - -// Load the Font Face and Font Face Resolver, if not already loaded by WordPress Core. -if ( ! class_exists( 'WP_Font_Face' ) ) { - require __DIR__ . '/compat/wordpress-6.4/fonts/font-face/class-wp-font-face.php'; - require __DIR__ . '/compat/wordpress-6.4/fonts/font-face/class-wp-font-face-resolver.php'; -} - -/* - * As _gutenberg_get_iframed_editor_assets_6_4() overrides Core's _wp_get_iframed_editor_assets(), - * load this file to ensure wp_print_font_faces() is invoked to load the styles into the - * iframed editor. - */ -require __DIR__ . '/compat/wordpress-6.4/fonts/fonts.php'; - // Load the BC Layer to avoid fatal errors of extenders using the Fonts API. // @core-merge: do not merge the BC layer files into WordPress Core. require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-provider.php'; From e9dee90f5c442d146f2f14a6d6758f5f7a9eee37 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 30 Jul 2024 17:08:06 +0400 Subject: [PATCH 3/9] Remove global styles revisions controller test --- ...lobal-styles-revisions-controller-test.php | 351 ------------------ 1 file changed, 351 deletions(-) delete mode 100644 phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php diff --git a/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php b/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php deleted file mode 100644 index 30780c50f1863..0000000000000 --- a/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php +++ /dev/null @@ -1,351 +0,0 @@ -user->create( - array( - 'role' => 'administrator', - ) - ); - - wp_set_current_user( self::$admin_id ); - // This creates the global styles for the current theme. - self::$global_styles_id = $factory->post->create( - array( - 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', - 'post_status' => 'publish', - 'post_title' => __( 'Custom Styles', 'default' ), - 'post_type' => 'wp_global_styles', - 'post_name' => 'wp-global-styles-tt1-blocks-revisions', - 'tax_input' => array( - 'wp_theme' => 'tt1-blocks', - ), - ) - ); - - // This creates another global styles post for the current theme. - self::$global_styles_id_2 = $factory->post->create( - array( - 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', - 'post_status' => 'publish', - 'post_title' => __( 'Custom Styles', 'default' ), - 'post_type' => 'wp_global_styles', - 'post_name' => 'wp-global-styles-tt1-blocks-revisions-2', - 'tax_input' => array( - 'wp_theme' => 'tt1-blocks', - ), - ) - ); - - // Update post to create a new revisions. - $new_styles_post = array( - 'ID' => self::$global_styles_id, - 'post_content' => wp_json_encode( - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'isGlobalStylesUserThemeJSON' => true, - 'styles' => array( - 'color' => array( - 'background' => 'hotpink', - ), - ), - 'settings' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Ghost', - 'slug' => 'ghost', - 'color' => 'ghost', - ), - ), - ), - ), - ), - ) - ), - ); - - wp_update_post( $new_styles_post, true, true ); - - $new_styles_post = array( - 'ID' => self::$global_styles_id, - 'post_content' => wp_json_encode( - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'isGlobalStylesUserThemeJSON' => true, - 'styles' => array( - 'color' => array( - 'background' => 'lemonchiffon', - ), - ), - 'settings' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Gwanda', - 'slug' => 'gwanda', - 'color' => 'gwanda', - ), - ), - ), - ), - ), - ) - ), - ); - - wp_update_post( $new_styles_post, true, true ); - - $new_styles_post = array( - 'ID' => self::$global_styles_id, - 'post_content' => wp_json_encode( - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'isGlobalStylesUserThemeJSON' => true, - 'styles' => array( - 'color' => array( - 'background' => 'chocolate', - ), - ), - 'settings' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Stacy', - 'slug' => 'stacy', - 'color' => 'stacy', - ), - ), - ), - ), - ), - ) - ), - ); - - wp_update_post( $new_styles_post, true, true ); - wp_set_current_user( 0 ); - } - - /** - * Removes users after our tests run. - */ - public static function wpTearDownAfterClass() { - self::delete_user( self::$admin_id ); - } - - /** - * Sets up before tests. - */ - public function set_up() { - parent::set_up(); - switch_theme( 'emptytheme' ); - $revisions = wp_get_post_revisions( self::$global_styles_id ); - $this->revision_1 = array_pop( $revisions ); - $this->revision_1_id = $this->revision_1->ID; - - /* - * For some reason the `rest_api_init` doesn't run early enough to ensure an overwritten `get_item_schema()` - * is used. So we manually call it here. - * See: https://github.com/WordPress/gutenberg/pull/52370#issuecomment-1643331655. - */ - $global_styles_revisions_controller = new Gutenberg_REST_Global_Styles_Revisions_Controller_6_5(); - $global_styles_revisions_controller->register_routes(); - } - - /** - * @ticket 58524 - * - * @covers WP_REST_Global_Styles_Controller::register_routes - */ - public function test_register_routes() { - $routes = rest_get_server()->get_routes(); - $this->assertArrayHasKey( - '/wp/v2/global-styles/(?P[\d]+)/revisions/(?P[\d]+)', - $routes, - 'Single global style revisions based on the given parentID and revision ID route does not exist.' - ); - } - - /** - * @ticket 59810 - * - * @covers WP_REST_Global_Styles_Controller::get_items - */ - public function test_get_item_valid_parent_id() { - wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/' . $this->revision_1_id ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertSame( self::$global_styles_id, $data['parent'], "The returned revision's id should match the parent id." ); - $this->check_get_revision_response( $data, $this->revision_1 ); - } - - /** - * @ticket 59810 - * - * @covers WP_REST_Global_Styles_Controller::get_items - */ - public function test_get_item_invalid_parent_id() { - wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id_2 . '/revisions/' . $this->revision_1_id ); - $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_revision_parent_id_mismatch', $response, 404 ); - - $expected_message = 'The revision does not belong to the specified parent with id of "' . self::$global_styles_id_2 . '"'; - $this->assertSame( $expected_message, $response->as_error()->get_error_messages()[0], 'The message must contain the correct parent ID.' ); - } - - /** - * Utility function to check the items in WP_REST_Global_Styles_Controller::get_items - * against the expected values. - * - * @ticket 58524 - */ - protected function check_get_revision_response( $response_revision_item, $revision_expected_item ) { - $this->assertSame( (int) $revision_expected_item->post_author, $response_revision_item['author'], 'Check that the revision item `author` exists.' ); - $this->assertSame( mysql_to_rfc3339( $revision_expected_item->post_date ), $response_revision_item['date'], 'Check that the revision item `date` exists.' ); - $this->assertSame( mysql_to_rfc3339( $revision_expected_item->post_date_gmt ), $response_revision_item['date_gmt'], 'Check that the revision item `date_gmt` exists.' ); - $this->assertSame( mysql_to_rfc3339( $revision_expected_item->post_modified ), $response_revision_item['modified'], 'Check that the revision item `modified` exists.' ); - $this->assertSame( mysql_to_rfc3339( $revision_expected_item->post_modified_gmt ), $response_revision_item['modified_gmt'], 'Check that the revision item `modified_gmt` exists.' ); - $this->assertSame( $revision_expected_item->post_parent, $response_revision_item['parent'], 'Check that an id for the parent exists.' ); - - // Global styles. - $config = ( new WP_Theme_JSON_Gutenberg( json_decode( $revision_expected_item->post_content, true ), 'custom' ) )->get_raw_data(); - $this->assertEquals( - $config['settings'], - $response_revision_item['settings'], - 'Check that the revision settings exist in the response.' - ); - $this->assertEquals( - $config['styles'], - $response_revision_item['styles'], - 'Check that the revision styles match the updated styles.' - ); - } - - /** - * @ticket 59810 - * - * @covers Gutenberg_REST_Global_Styles_Revisions_Controller_6_4::get_item - */ - public function test_get_item() { - wp_set_current_user( self::$admin_id ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/' . $this->revision_1_id ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); - $this->check_get_revision_response( $data, $this->revision_1 ); - } - - /** - * @ticket 59810 - * - * @covers Gutenberg_REST_Global_Styles_Revisions_Controller_6_4::get_revision - */ - public function test_get_item_invalid_revision_id_should_error() { - wp_set_current_user( self::$admin_id ); - - $expected_error = 'rest_post_invalid_id'; - $expected_status = 404; - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/20000001' ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertErrorResponse( $expected_error, $response, $expected_status ); - } - - /** - * @doesNotPerformAssertions - */ - public function test_get_items() { - // Unit tests have been more to WordPress Core for test_get_items(). - // No unique compat unit tests exist. - } - - /** - * @doesNotPerformAssertions - */ - public function test_get_item_schema() { - // Unit tests have been more to WordPress Core for test_get_item_schema(). - // No unique compat unit tests exist. - } - - /** - * @doesNotPerformAssertions - */ - public function test_context_param() { - // Controller does not implement test_context_param(). - } - - /** - * @doesNotPerformAssertions - */ - public function test_create_item() { - // Controller does not implement create_item(). - } - - /** - * @doesNotPerformAssertions - */ - public function test_delete_item() { - // Controller does not implement delete_item(). - } - - /** - * @doesNotPerformAssertions - */ - public function test_prepare_item() { - // Controller does not implement prepare_item(). - } - - /** - * @doesNotPerformAssertions - */ - public function test_update_item() { - // Controller does not implement update_item(). - } -} From c28fbec63d61983126148f4d979e61d8413421f9 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 30 Jul 2024 20:27:51 +0400 Subject: [PATCH 4/9] Remove legacy font library PHPUnit test --- .../fontFamilyBackwardsCompatibility.php | 196 ------------------ 1 file changed, 196 deletions(-) delete mode 100644 phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php diff --git a/phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php b/phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php deleted file mode 100644 index dc720b6b7db70..0000000000000 --- a/phpunit/tests/fonts/font-library/fontFamilyBackwardsCompatibility.php +++ /dev/null @@ -1,196 +0,0 @@ -post_ids_to_delete = array(); - delete_option( 'gutenberg_font_family_format_converted' ); - } - - public function tear_down() { - foreach ( $this->post_ids_to_delete as $post_id ) { - wp_delete_post( $post_id, true ); - } - - delete_option( 'gutenberg_font_family_format_converted' ); - - parent::tear_down(); - } - - public function test_font_faces_with_remote_src() { - $legacy_content = '{"fontFace":[{"fontFamily":"Open Sans","fontStyle":"normal","fontWeight":"400","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0C4nY1M2xLER.ttf"},{"fontFamily":"Open Sans","fontStyle":"italic","fontWeight":"400","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-italic.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0Rk8ZkaVcUwaERZjA.ttf"},{"fontFamily":"Open Sans","fontStyle":"normal","fontWeight":"700","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-700-normal.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1y4nY1M2xLER.ttf"}],"fontFamily":"\'Open Sans\', sans-serif","name":"Open Sans","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans.svg","slug":"open-sans"}'; - - $font_family_id = $this->create_font_family( $legacy_content ); - - gutenberg_convert_legacy_font_family_format(); - - $font_family = $this->get_post( $font_family_id ); - $font_faces = $this->get_font_faces( $font_family_id ); - - list( $font_face1, $font_face2, $font_face3 ) = $font_faces; - - // Updated font family post. - $this->assertSame( 'wp_font_family', $font_family->post_type, 'The font family post type should be wp_font_family.' ); - $this->assertSame( 'publish', $font_family->post_status, 'The font family post status should be publish.' ); - - $font_family_title = 'Open Sans'; - $this->assertSame( $font_family_title, $font_family->post_title, 'The font family post title should be Open Sans.' ); - - $font_family_slug = 'open-sans'; - $this->assertSame( $font_family_slug, $font_family->post_name, 'The font family post name should be open-sans.' ); - - $font_family_content = wp_json_encode( json_decode( '{"fontFamily":"\'Open Sans\', sans-serif","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans.svg"}', true ) ); - $this->assertSame( $font_family_content, $font_family->post_content, 'The font family post content should match.' ); - - $meta = get_post_meta( $font_family_id, '_gutenberg_legacy_font_family', true ); - $this->assertSame( $legacy_content, $meta, 'The _gutenberg_legacy_font_family post meta content should match.' ); - - // First font face post. - $this->assertSame( 'wp_font_face', $font_face1->post_type, 'The 1st font face post type should be wp_font_face.' ); - $this->assertSame( $font_family_id, $font_face1->post_parent, 'The 1st font face post parent should match.' ); - $this->assertSame( 'publish', $font_face1->post_status, 'The 1st font face post status should be publish.' ); - - $font_face1_title = 'open sans;normal;400;100%;U+0-10FFFF'; - $this->assertSame( $font_face1_title, $font_face1->post_title, 'The 1st font face post title should match.' ); - $this->assertSame( sanitize_title( $font_face1_title ), $font_face1->post_name, 'The 1st font face post name should match.' ); - - $font_face1_content = wp_json_encode( json_decode( '{"fontFamily":"Open Sans","fontStyle":"normal","fontWeight":"400","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0C4nY1M2xLER.ttf"}' ) ); - $this->assertSame( $font_face1_content, $font_face1->post_content, 'The 1st font face post content should match.' ); - - // With a remote url, file post meta should not be set. - $meta = get_post_meta( $font_face1->ID, '_wp_font_face_file', true ); - $this->assertSame( '', $meta, 'The _wp_font_face_file post meta for the 1st font face should be an empty string.' ); - - // Second font face post. - $this->assertSame( 'wp_font_face', $font_face2->post_type, 'The 2nd font face post type should be wp_font_face.' ); - $this->assertSame( $font_family_id, $font_face2->post_parent, 'The 2md font face post type should be wp_font_face.' ); - $this->assertSame( 'publish', $font_face2->post_status, 'The 2nd font face post status should be publish.' ); - - $font_face2_title = 'open sans;italic;400;100%;U+0-10FFFF'; - $this->assertSame( $font_face2_title, $font_face2->post_title, 'The 2nd font face post title should match.' ); - $this->assertSame( sanitize_title( $font_face2_title ), $font_face2->post_name, 'The 2nd font face post name should match.' ); - - $font_face2_content = wp_json_encode( json_decode( '{"fontFamily":"Open Sans","fontStyle":"italic","fontWeight":"400","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-italic.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memQYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWq8tWZ0Pw86hd0Rk8ZkaVcUwaERZjA.ttf"}' ) ); - $this->assertSame( $font_face2_content, $font_face2->post_content, 'The 2nd font face post content should match.' ); - - // With a remote url, file post meta should not be set. - $meta = get_post_meta( $font_face2->ID, '_wp_font_face_file', true ); - $this->assertSame( '', $meta, 'The _wp_font_face_file post meta for the 2nd font face should be an empty string.' ); - - // Third font face post. - $this->assertSame( 'wp_font_face', $font_face3->post_type, 'The 3rd font face post type should be wp_font_face.' ); - $this->assertSame( $font_family_id, $font_face3->post_parent, 'The 3rd font face post type should be wp_font_face.' ); - $this->assertSame( 'publish', $font_face3->post_status, 'The 3rd font face post status should be publish.' ); - - $font_face3_title = 'open sans;normal;700;100%;U+0-10FFFF'; - $this->assertSame( $font_face3_title, $font_face3->post_title, 'The 3rd font face post title should match.' ); - $this->assertSame( sanitize_title( $font_face3_title ), $font_face3->post_name, 'The 3rd font face post name should match.' ); - - $font_face3_content = wp_json_encode( json_decode( '{"fontFamily":"Open Sans","fontStyle":"normal","fontWeight":"700","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-700-normal.svg","src":"https://fonts.gstatic.com/s/opensans/v35/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsg-1y4nY1M2xLER.ttf"}' ) ); - $this->assertSame( $font_face3_content, $font_face3->post_content, 'The 3rd font face post content should match.' ); - - // With a remote url, file post meta should not be set. - $meta = get_post_meta( $font_face3->ID, '_wp_font_face_file', true ); - $this->assertSame( '', $meta, 'The _wp_font_face_file post meta for the 3rd font face should be an empty string.' ); - } - - public function test_font_faces_with_local_src() { - $legacy_content = '{"fontFace":[{"fontFamily":"Open Sans","fontStyle":"normal","fontWeight":"400","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans-400-normal.svg","src":"' . site_url() . '/wp-content/fonts/open-sans_normal_400.ttf"}],"fontFamily":"\'Open Sans\', sans-serif","name":"Open Sans","preview":"https://s.w.org/images/fonts/16.7/previews/open-sans/open-sans.svg","slug":"open-sans"}'; - - $font_family_id = $this->create_font_family( $legacy_content ); - - gutenberg_convert_legacy_font_family_format(); - - $font_faces = $this->get_font_faces( $font_family_id ); - - $this->assertCount( 1, $font_faces, 'There should be 1 font face.' ); - $font_face = reset( $font_faces ); - - // Check that file meta is present. - $file_path = 'open-sans_normal_400.ttf'; - $meta = get_post_meta( $font_face->ID, '_wp_font_face_file', true ); - $this->assertSame( $file_path, $meta, 'The _wp_font_face_file should match.' ); - } - - public function test_migration_only_runs_once() { - $legacy_content = '{"fontFace":[],"fontFamily":"\'Open Sans\', sans-serif","name":"Open Sans","preview":"","slug":"open-sans"}'; - - // Simulate that the migration has already run. - update_option( 'gutenberg_font_family_format_converted', true ); - - $font_family_id = $this->create_font_family( $legacy_content ); - - gutenberg_convert_legacy_font_family_format(); - - // Meta with backup content will not be present if migration isn't triggered. - $meta = get_post_meta( $font_family_id, '_gutenberg_legacy_font_family', true ); - $this->assertSame( '', $meta ); - } - - protected function create_font_family( $content ) { - $post_id = wp_insert_post( - array( - 'post_type' => 'wp_font_family', - 'post_status' => 'publish', - 'post_title' => 'Open Sans', - 'post_name' => 'open-sans', - 'post_content' => $content, - ) - ); - - $this->store_id_for_cleanup_in_teardown( $post_id ); - - return $post_id; - } - - private function get_post( $post_id ) { - $post = get_post( $post_id ); - - $this->store_id_for_cleanup_in_teardown( $post ); - - return $post; - } - - protected function get_font_faces( $font_family_id ) { - $posts = get_posts( - array( - 'post_parent' => $font_family_id, - 'post_type' => 'wp_font_face', - 'order' => 'ASC', - 'orderby' => 'id', - ) - ); - - $this->store_id_for_cleanup_in_teardown( $posts ); - - return $posts; - } - - private function store_id_for_cleanup_in_teardown( $post ) { - if ( null === $post ) { - return; - } - - $posts = is_array( $post ) ? $post : array( $post ); - - foreach ( $posts as $post ) { - if ( null === $post ) { - continue; - } - $this->post_ids_to_delete[] = is_int( $post ) ? $post : $post->ID; - } - } -} From fac3b3db9e51059485744111f0a8b338f5511c1d Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 30 Jul 2024 22:48:27 +0400 Subject: [PATCH 5/9] Remove deprecated hook handler --- lib/load.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/load.php b/lib/load.php index 1c89de4eceab8..aefcfc4df830e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -119,9 +119,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/disable-tinymce.php'; } -// Fonts API / Font Face. -remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler. - // Load the BC Layer to avoid fatal errors of extenders using the Fonts API. // @core-merge: do not merge the BC layer files into WordPress Core. require __DIR__ . '/experimental/font-face/bc-layer/class-wp-fonts-provider.php'; From a09c28384f4811c1c053001a4639497b261c96ab Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 30 Jul 2024 23:40:21 +0400 Subject: [PATCH 6/9] Restore 'gutenberg_before_delete_font_face' callback --- lib/compat/plugin/fonts.php | 43 +++++++++++++++++++++++++++++++++++++ lib/load.php | 1 + 2 files changed, 44 insertions(+) create mode 100644 lib/compat/plugin/fonts.php diff --git a/lib/compat/plugin/fonts.php b/lib/compat/plugin/fonts.php new file mode 100644 index 0000000000000..f427f6110f610 --- /dev/null +++ b/lib/compat/plugin/fonts.php @@ -0,0 +1,43 @@ +post_type ) { + return; + } + + $font_files = get_post_meta( $post_id, '_wp_font_face_file', false ); + + if ( empty( $font_files ) ) { + return; + } + + $site_path = ''; + if ( is_multisite() && ! ( is_main_network() && is_main_site() ) ) { + $site_path = '/sites/' . get_current_blog_id(); + } + + $font_dir = path_join( WP_CONTENT_DIR, 'fonts' ) . $site_path; + + foreach ( $font_files as $font_file ) { + $font_path = $font_dir . '/' . $font_file; + + if ( file_exists( $font_path ) ) { + wp_delete_file( $font_path ); + } + } +} +add_action( 'before_delete_post', 'gutenberg_before_delete_font_face', 10, 2 ); diff --git a/lib/load.php b/lib/load.php index aefcfc4df830e..244b6a2d9e685 100644 --- a/lib/load.php +++ b/lib/load.php @@ -63,6 +63,7 @@ function gutenberg_is_experiment_enabled( $name ) { // Gutenberg plugin compat. require __DIR__ . '/compat/plugin/edit-site-routes-backwards-compat.php'; +require __DIR__ . '/compat/plugin/fonts.php'; // The Token Map was created during 6.6 in order to support the HTML API. It must be loaded before it. require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-token-map-6-6.php'; From 1c70e3d3abe95ae164c9da6c38b3fdfc014fb9b2 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Wed, 31 Jul 2024 11:01:49 +0800 Subject: [PATCH 7/9] Remove button block back compat for 6.4 --- packages/block-library/src/button/index.php | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/block-library/src/button/index.php b/packages/block-library/src/button/index.php index 4a53952ef537f..f272fa9eb62b1 100644 --- a/packages/block-library/src/button/index.php +++ b/packages/block-library/src/button/index.php @@ -17,20 +17,7 @@ * @return string The block content. */ function render_block_core_button( $attributes, $content ) { - /* - * The current Gutenberg plugin supports WordPress 6.4, but the next_token() - * method does not exist in WordPress 6.4. Therefore, if Gutenberg is used - * as a plugin, use the Gutenberg class that has the `next_token()` method. - * - * TODO: After the Gutenberg plugin drops support for WordPress 6.4, this - * conditional statement will be removed and the core class `WP_HTML_Tag_Processor` - * should be used. - */ - if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN && class_exists( 'Gutenberg_HTML_Tag_Processor_6_7' ) ) { - $p = new Gutenberg_HTML_Tag_Processor_6_7( $content ); - } else { - $p = new WP_HTML_Tag_Processor( $content ); - } + $p = new WP_HTML_Tag_Processor( $content ); /* * The button block can render an `` or `