diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php
index 350d158cd6e24..ddbd1917c3054 100644
--- a/lib/block-supports/layout.php
+++ b/lib/block-supports/layout.php
@@ -1019,27 +1019,10 @@ function gutenberg_restore_group_inner_container( $block_content, $block ) {
$processor = new WP_HTML_Tag_Processor( $block_content );
if ( $processor->next_tag( array( 'class_name' => 'wp-block-group' ) ) ) {
- if ( method_exists( $processor, 'class_list' ) ) {
- foreach ( $processor->class_list() as $class_name ) {
- if ( str_contains( $class_name, 'layout' ) ) {
- array_push( $layout_classes, $class_name );
- $processor->remove_class( $class_name );
- }
- }
- } else {
- /*
- * The class_list method was only added in 6.4 so this needs a temporary fallback.
- * This fallback should be removed when the minimum supported version is 6.4.
- */
- $classes = $processor->get_attribute( 'class' );
- if ( $classes ) {
- $classes = explode( ' ', $classes );
- foreach ( $classes as $class_name ) {
- if ( str_contains( $class_name, 'is-layout-' ) ) {
- array_push( $layout_classes, $class_name );
- $processor->remove_class( $class_name );
- }
- }
+ foreach ( $processor->class_list() as $class_name ) {
+ if ( str_contains( $class_name, 'layout' ) ) {
+ array_push( $layout_classes, $class_name );
+ $processor->remove_class( $class_name );
}
}
}
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/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 tags; unless one of these appears next,
- * proceed scanning to the next potential token in the text.
- */
- if ( ! (
- $at + 6 < $doc_length &&
- ( 's' === $html[ $at ] || 'S' === $html[ $at ] ) &&
- ( 'c' === $html[ $at + 1 ] || 'C' === $html[ $at + 1 ] ) &&
- ( 'r' === $html[ $at + 2 ] || 'R' === $html[ $at + 2 ] ) &&
- ( 'i' === $html[ $at + 3 ] || 'I' === $html[ $at + 3 ] ) &&
- ( 'p' === $html[ $at + 4 ] || 'P' === $html[ $at + 4 ] ) &&
- ( 't' === $html[ $at + 5 ] || 'T' === $html[ $at + 5 ] )
- ) ) {
- ++$at;
- continue;
- }
-
- /*
- * Ensure that the script tag terminates to avoid matching on
- * substrings of a non-match. For example, the sequence
- * "= $doc_length ) {
- continue;
- }
- $at += 6;
- $c = $html[ $at ];
- if ( ' ' !== $c && "\t" !== $c && "\r" !== $c && "\n" !== $c && '/' !== $c && '>' !== $c ) {
- ++$at;
- continue;
- }
-
- if ( 'escaped' === $state && ! $is_closing ) {
- $state = 'double-escaped';
- continue;
- }
-
- if ( 'double-escaped' === $state && $is_closing ) {
- $state = 'escaped';
- continue;
- }
-
- if ( $is_closing ) {
- $this->bytes_already_parsed = $closer_potentially_starts_at;
- if ( $this->bytes_already_parsed >= $doc_length ) {
- return false;
- }
-
- while ( $this->parse_next_attribute() ) {
- continue;
- }
-
- if ( '>' === $html[ $this->bytes_already_parsed ] ) {
- $this->bytes_already_parsed = $closer_potentially_starts_at;
- return true;
- }
- }
-
- ++$at;
- }
-
- return false;
- }
-
- /**
- * Parses the next tag.
- *
- * This will find and start parsing the next tag, including
- * the opening `<`, the potential closer `/`, and the tag
- * name. It does not parse the attributes or scan to the
- * closing `>`; these are left for other methods.
- *
- * @since 6.2.0
- * @since 6.2.1 Support abruptly-closed comments, invalid-tag-closer-comments, and empty elements.
- *
- * @return bool Whether a tag was found before the end of the document.
- */
- private function parse_next_tag() {
- $this->after_tag();
-
- $html = $this->html;
- $doc_length = strlen( $html );
- $at = $this->bytes_already_parsed;
-
- while ( false !== $at && $at < $doc_length ) {
- $at = strpos( $html, '<', $at );
- if ( false === $at ) {
- return false;
- }
-
- if ( '/' === $this->html[ $at + 1 ] ) {
- $this->is_closing_tag = true;
- ++$at;
- } else {
- $this->is_closing_tag = false;
- }
-
- /*
- * HTML tag names must start with [a-zA-Z] otherwise they are not tags.
- * For example, "<3" is rendered as text, not a tag opener. If at least
- * one letter follows the "<" then _it is_ a tag, but if the following
- * character is anything else it _is not a tag_.
- *
- * It's not uncommon to find non-tags starting with `<` in an HTML
- * document, so it's good for performance to make this pre-check before
- * continuing to attempt to parse a tag name.
- *
- * Reference:
- * * https://html.spec.whatwg.org/multipage/parsing.html#data-state
- * * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- */
- $tag_name_prefix_length = strspn( $html, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', $at + 1 );
- if ( $tag_name_prefix_length > 0 ) {
- ++$at;
- $this->tag_name_length = $tag_name_prefix_length + strcspn( $html, " \t\f\r\n/>", $at + $tag_name_prefix_length );
- $this->tag_name_starts_at = $at;
- $this->bytes_already_parsed = $at + $this->tag_name_length;
- return true;
- }
-
- /*
- * Abort if no tag is found before the end of
- * the document. There is nothing left to parse.
- */
- if ( $at + 1 >= strlen( $html ) ) {
- return false;
- }
-
- /*
- *
- * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- */
- if (
- strlen( $html ) > $at + 3 &&
- '-' === $html[ $at + 2 ] &&
- '-' === $html[ $at + 3 ]
- ) {
- $closer_at = $at + 4;
- // If it's not possible to close the comment then there is nothing more to scan.
- if ( strlen( $html ) <= $closer_at ) {
- return false;
- }
-
- // Abruptly-closed empty comments are a sequence of dashes followed by `>`.
- $span_of_dashes = strspn( $html, '-', $closer_at );
- if ( '>' === $html[ $closer_at + $span_of_dashes ] ) {
- $at = $closer_at + $span_of_dashes + 1;
- continue;
- }
-
- /*
- * 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 < strlen( $html ) ) {
- $closer_at = strpos( $html, '--', $closer_at );
- if ( false === $closer_at ) {
- return false;
- }
-
- if ( $closer_at + 2 < strlen( $html ) && '>' === $html[ $closer_at + 2 ] ) {
- $at = $closer_at + 3;
- continue 2;
- }
-
- if ( $closer_at + 3 < strlen( $html ) && '!' === $html[ $closer_at + 2 ] && '>' === $html[ $closer_at + 3 ] ) {
- $at = $closer_at + 4;
- continue 2;
- }
- }
- }
-
- /*
- *
- * The CDATA is case-sensitive.
- * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- */
- if (
- strlen( $html ) > $at + 8 &&
- '[' === $html[ $at + 2 ] &&
- 'C' === $html[ $at + 3 ] &&
- 'D' === $html[ $at + 4 ] &&
- 'A' === $html[ $at + 5 ] &&
- 'T' === $html[ $at + 6 ] &&
- 'A' === $html[ $at + 7 ] &&
- '[' === $html[ $at + 8 ]
- ) {
- $closer_at = strpos( $html, ']]>', $at + 9 );
- if ( false === $closer_at ) {
- return false;
- }
-
- $at = $closer_at + 3;
- continue;
- }
-
- /*
- *
- * These are ASCII-case-insensitive.
- * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- */
- if (
- strlen( $html ) > $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 ) {
- return false;
- }
-
- $at = $closer_at + 1;
- continue;
- }
-
- /*
- * Anything else here is an incorrectly-opened comment and transitions
- * to the bogus comment state - skip to the nearest >.
- */
- $at = strpos( $html, '>', $at + 1 );
- continue;
- }
-
- /*
- * > is a missing end tag name, which is ignored.
- *
- * See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name
- */
- if ( '>' === $html[ $at + 1 ] ) {
- ++$at;
- continue;
- }
-
- /*
- * transitions to a bogus comment state – skip to the nearest >
- * 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 ) {
- return false;
- }
-
- $at = $closer_at + 1;
- continue;
- }
-
- /*
- * If a non-alpha starts the tag name in a tag closer it's a comment.
- * Find the first `>`, which closes the comment.
- *
- * See https://html.spec.whatwg.org/#parse-error-invalid-first-character-of-tag-name
- */
- if ( $this->is_closing_tag ) {
- $closer_at = strpos( $html, '>', $at + 3 );
- if ( false === $closer_at ) {
- return false;
- }
-
- $at = $closer_at + 1;
- continue;
- }
-
- ++$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 ) ) {
- 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 ) ) {
- return false;
- }
-
- $this->skip_whitespace();
- if ( $this->bytes_already_parsed >= strlen( $this->html ) ) {
- 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 ) ) {
- 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 ) ) {
- 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 WP_HTML_Attribute_Token(
- $attribute_name,
- $value_start,
- $value_length,
- $attribute_start,
- $attribute_end,
- ! $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 WP_HTML_Span( $attribute_start, $attribute_end );
- 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() {
- $this->get_updated_html();
- $this->tag_name_starts_at = null;
- $this->tag_name_length = null;
- $this->tag_ends_at = null;
- $this->is_closing_tag = null;
- $this->attributes = array();
- $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->end - $diff->start );
-
- // 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->end;
- }
-
- $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 ) {
- /*
- * 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 ) {
- 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->end - $diff->start );
-
- if ( $bookmark->start >= $diff->start ) {
- $head_delta += $delta;
- }
-
- if ( $bookmark->end >= $diff->end ) {
- $tail_delta += $delta;
- }
- }
-
- $bookmark->start += $head_delta;
- $bookmark->end += $tail_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;
- return $this->next_tag( array( 'tag_closers' => 'visit' ) );
- }
-
- /**
- * 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->end - $b->end;
- }
-
- /**
- * 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 ( ! 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 ( null === $this->tag_name_starts_at ) {
- 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 ( $this->is_closing_tag || null === $this->tag_name_starts_at ) {
- 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 );
-
- return strtoupper( $tag_name );
- }
-
- /**
- * 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 ( ! $this->tag_name_starts_at ) {
- return false;
- }
-
- return '/' === $this->html[ $this->tag_ends_at - 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 $this->is_closing_tag;
- }
-
- /**
- * 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 ( $this->is_closing_tag || null === $this->tag_name_starts_at ) {
- 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.
- '"\'>& =' .
- // Control characters.
- '\x{00}-\x{1F}' .
- // HTML noncharacters.
- '\x{FDD0}-\x{FDEF}' .
- '\x{FFFE}\x{FFFF}\x{1FFFE}\x{1FFFF}\x{2FFFE}\x{2FFFF}\x{3FFFE}\x{3FFFF}' .
- '\x{4FFFE}\x{4FFFF}\x{5FFFE}\x{5FFFF}\x{6FFFE}\x{6FFFF}\x{7FFFE}\x{7FFFF}' .
- '\x{8FFFE}\x{8FFFF}\x{9FFFE}\x{9FFFF}\x{AFFFE}\x{AFFFF}\x{BFFFE}\x{BFFFF}' .
- '\x{CFFFE}\x{CFFFF}\x{DFFFE}\x{DFFFF}\x{EFFFE}\x{EFFFF}\x{FFFFE}\x{FFFFF}' .
- '\x{10FFFE}\x{10FFFF}' .
- ']~Ssu',
- $name
- ) ) {
- _doing_it_wrong(
- __METHOD__,
- __( 'Invalid attribute name.' ),
- '6.2.0'
- );
-
- return false;
- }
-
- /*
- * > 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 WP_HTML_Text_Replacement(
- $existing_attribute->start,
- $existing_attribute->end,
- $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 WP_HTML_Text_Replacement(
- $this->tag_name_starts_at + $this->tag_name_length,
- $this->tag_name_starts_at + $this->tag_name_length,
- ' ' . $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 ( $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 WP_HTML_Text_Replacement(
- $this->attributes[ $name ]->start,
- $this->attributes[ $name ]->end,
- ''
- );
-
- // 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 WP_HTML_Text_Replacement(
- $attribute_token->start,
- $attribute_token->end,
- ''
- );
- }
- }
-
- 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 ( $this->is_closing_tag ) {
- return false;
- }
-
- if ( null !== $this->tag_name_starts_at ) {
- $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 ( $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->tag_name_starts_at - 1;
-
- /*
- * 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.
- *
- *