From 6433903ce4dbdb3b4518c64eb6d26df36e2d2404 Mon Sep 17 00:00:00 2001 From: ramon Date: Wed, 9 Aug 2023 16:16:12 +1000 Subject: [PATCH 1/7] Initial commit --- gutenberg.php | 2 +- ...erg-rest-block-patterns-controller-6-3.php | 2 +- ...berg-rest-global-styles-controller-6-3.php | 0 lib/load.php | 26 ------------------- 4 files changed, 2 insertions(+), 28 deletions(-) create mode 100644 lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php diff --git a/gutenberg.php b/gutenberg.php index 44146b0c57d30..3e9febccbe737 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. - * Requires at least: 6.1 + * Requires at least: 6.2 * Requires PHP: 7.0 * Version: 16.6.0-rc.1 * Author: Gutenberg Team diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php index ac40e6b842f52..a64724ff858c2 100644 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php @@ -13,7 +13,7 @@ * * @see WP_REST_Controller */ -class Gutenberg_REST_Block_Patterns_Controller_6_3 extends Gutenberg_REST_Block_Patterns_Controller_6_2 { +class Gutenberg_REST_Block_Patterns_Controller_6_3 extends WP_REST_Block_Patterns_Controller { /** * Prepare a raw block pattern before it gets output in a REST API response. * diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/lib/load.php b/lib/load.php index 72bc0ab66a99b..ef8da334debe6 100644 --- a/lib/load.php +++ b/lib/load.php @@ -35,13 +35,6 @@ function gutenberg_is_experiment_enabled( $name ) { require_once __DIR__ . '/experimental/class-wp-rest-block-editor-settings-controller.php'; } - // WordPress 6.2 compat. - require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php'; - require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php'; - require_once __DIR__ . '/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php'; - require_once __DIR__ . '/compat/wordpress-6.2/rest-api.php'; - require_once __DIR__ . '/compat/wordpress-6.2/block-patterns.php'; - // WordPress 6.3 compat. require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php'; require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php'; @@ -81,25 +74,6 @@ function gutenberg_is_experiment_enabled( $name ) { // Gutenberg plugin compat. require __DIR__ . '/compat/plugin/edit-site-routes-backwards-compat.php'; -// WordPress 6.2 compat. -require __DIR__ . '/compat/wordpress-6.2/blocks.php'; -require __DIR__ . '/compat/wordpress-6.2/script-loader.php'; -require __DIR__ . '/compat/wordpress-6.2/block-template-utils.php'; -require __DIR__ . '/compat/wordpress-6.2/get-global-styles-and-settings.php'; -require __DIR__ . '/compat/wordpress-6.2/default-filters.php'; -require __DIR__ . '/compat/wordpress-6.2/site-editor.php'; -require __DIR__ . '/compat/wordpress-6.2/block-editor.php'; -require __DIR__ . '/compat/wordpress-6.2/theme.php'; -require __DIR__ . '/compat/wordpress-6.2/widgets.php'; -require __DIR__ . '/compat/wordpress-6.2/menu.php'; - -if ( ! class_exists( 'WP_HTML_Tag_Processor' ) ) { - require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-attribute-token.php'; - require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-span.php'; - require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-text-replacement.php'; - require __DIR__ . '/compat/wordpress-6.2/html-api/class-wp-html-tag-processor.php'; -} - 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'; From 147d9215cf59026faef1869f614b0f3a81d20855 Mon Sep 17 00:00:00 2001 From: ramon Date: Wed, 9 Aug 2023 17:03:45 +1000 Subject: [PATCH 2/7] Swapping `gutenberg_` functions for wp equivalents: - wp_get_elements_class_name since 6.0.0 - _wp_get_presets_class_name since 6.2.0 Ensuring `build_query_vars_from_query_block` doesn't receive `gutenberg_` prefix during build --- lib/block-supports/elements.php | 14 ++------------ lib/block-supports/settings.php | 23 ++++++++--------------- tools/webpack/blocks.js | 1 - 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/lib/block-supports/elements.php b/lib/block-supports/elements.php index 4029c91aa40a3..328d371c3e4fd 100644 --- a/lib/block-supports/elements.php +++ b/lib/block-supports/elements.php @@ -5,16 +5,6 @@ * @package gutenberg */ -/** - * Get the elements class names. - * - * @param array $block Block object. - * @return string The unique class name. - */ -function gutenberg_get_elements_class_name( $block ) { - return 'wp-elements-' . md5( serialize( $block ) ); -} - /** * Update the block content with elements class names. * @@ -103,7 +93,7 @@ function gutenberg_render_elements_support( $block_content, $block ) { // Add the class name to the first element, presuming it's the wrapper, if it exists. $tags = new WP_HTML_Tag_Processor( $block_content ); if ( $tags->next_tag() ) { - $tags->add_class( gutenberg_get_elements_class_name( $block ) ); + $tags->add_class( wp_get_elements_class_name( $block ) ); } return $tags->get_updated_html(); @@ -140,7 +130,7 @@ function gutenberg_render_elements_support_styles( $pre_render, $block ) { return null; } - $class_name = gutenberg_get_elements_class_name( $block ); + $class_name = wp_get_elements_class_name( $block ); $element_types = array( 'button' => array( diff --git a/lib/block-supports/settings.php b/lib/block-supports/settings.php index 7c0dd719f41c3..aac4774b62fe8 100644 --- a/lib/block-supports/settings.php +++ b/lib/block-supports/settings.php @@ -5,18 +5,6 @@ * @package gutenberg */ -/** - * Get the class name used on block level presets. - * - * @access private - * - * @param array $block Block object. - * @return string The unique class name. - */ -function _gutenberg_get_presets_class_name( $block ) { - return 'wp-settings-' . md5( serialize( $block ) ); -} - /** * Update the block content with block level presets class name. * @@ -47,7 +35,7 @@ function _gutenberg_add_block_level_presets_class( $block_content, $block ) { // Add the class name to the first element, presuming it's the wrapper, if it exists. $tags = new WP_HTML_Tag_Processor( $block_content ); if ( $tags->next_tag() ) { - $tags->add_class( _gutenberg_get_presets_class_name( $block ) ); + $tags->add_class( _wp_get_presets_class_name( $block ) ); } return $tags->get_updated_html(); @@ -76,7 +64,7 @@ function _gutenberg_add_block_level_preset_styles( $pre_render, $block ) { return null; } - $class_name = '.' . _gutenberg_get_presets_class_name( $block ); + $class_name = '.' . _wp_get_presets_class_name( $block ); // the root selector for preset variables needs to target every possible block selector // in order for the general setting to override any bock specific setting of a parent block or @@ -129,7 +117,12 @@ function _gutenberg_add_block_level_preset_styles( $pre_render, $block ) { ); if ( ! empty( $styles ) ) { - gutenberg_enqueue_block_support_styles( $styles ); + /* + * This method is deprecated since WordPress 6.2. + * We could enqueue these styles separately, + * or print them out with other settings presets. + */ + wp_enqueue_block_support_styles( $styles ); } return null; diff --git a/tools/webpack/blocks.js b/tools/webpack/blocks.js index 88acc23da5941..4104a791f29c6 100644 --- a/tools/webpack/blocks.js +++ b/tools/webpack/blocks.js @@ -28,7 +28,6 @@ const blockViewRegex = new RegExp( * the block will still call the core function when updates are back ported. */ const prefixFunctions = [ - 'build_query_vars_from_query_block', 'wp_apply_colors_support', 'wp_enqueue_block_support_styles', 'wp_get_typography_font_size_value', From 416e61931332d13a206946744d0031ac69dc5d3b Mon Sep 17 00:00:00 2001 From: ramon Date: Thu, 10 Aug 2023 11:19:03 +1000 Subject: [PATCH 3/7] - Remove compat/6.2 folder completely - Add usages of wp_normalize_remote_block_pattern (since 6.2) --- lib/compat/wordpress-6.2/block-editor.php | 45 - lib/compat/wordpress-6.2/block-patterns.php | 333 --- .../wordpress-6.2/block-template-utils.php | 97 - lib/compat/wordpress-6.2/blocks.php | 173 -- ...st-block-pattern-categories-controller.php | 84 - ...erg-rest-block-patterns-controller-6-2.php | 235 -- ...-rest-pattern-directory-controller-6-2.php | 122 - lib/compat/wordpress-6.2/default-filters.php | 18 - .../get-global-styles-and-settings.php | 87 - .../class-wp-html-attribute-token.php | 93 - .../html-api/class-wp-html-span.php | 56 - .../html-api/class-wp-html-tag-processor.php | 2291 ----------------- .../class-wp-html-text-replacement.php | 63 - lib/compat/wordpress-6.2/menu.php | 35 - lib/compat/wordpress-6.2/rest-api.php | 145 -- lib/compat/wordpress-6.2/script-loader.php | 75 - lib/compat/wordpress-6.2/site-editor.php | 24 - lib/compat/wordpress-6.2/theme.php | 23 - lib/compat/wordpress-6.2/widgets.php | 35 - lib/compat/wordpress-6.3/block-patterns.php | 6 +- 20 files changed, 3 insertions(+), 4037 deletions(-) delete mode 100644 lib/compat/wordpress-6.2/block-editor.php delete mode 100644 lib/compat/wordpress-6.2/block-patterns.php delete mode 100644 lib/compat/wordpress-6.2/block-template-utils.php delete mode 100644 lib/compat/wordpress-6.2/blocks.php delete mode 100644 lib/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php delete mode 100644 lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php delete mode 100644 lib/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php delete mode 100644 lib/compat/wordpress-6.2/default-filters.php delete mode 100644 lib/compat/wordpress-6.2/get-global-styles-and-settings.php delete mode 100644 lib/compat/wordpress-6.2/html-api/class-wp-html-attribute-token.php delete mode 100644 lib/compat/wordpress-6.2/html-api/class-wp-html-span.php delete mode 100644 lib/compat/wordpress-6.2/html-api/class-wp-html-tag-processor.php delete mode 100644 lib/compat/wordpress-6.2/html-api/class-wp-html-text-replacement.php delete mode 100644 lib/compat/wordpress-6.2/menu.php delete mode 100644 lib/compat/wordpress-6.2/rest-api.php delete mode 100644 lib/compat/wordpress-6.2/script-loader.php delete mode 100644 lib/compat/wordpress-6.2/site-editor.php delete mode 100644 lib/compat/wordpress-6.2/theme.php delete mode 100644 lib/compat/wordpress-6.2/widgets.php diff --git a/lib/compat/wordpress-6.2/block-editor.php b/lib/compat/wordpress-6.2/block-editor.php deleted file mode 100644 index 4df2b6b2652e0..0000000000000 --- a/lib/compat/wordpress-6.2/block-editor.php +++ /dev/null @@ -1,45 +0,0 @@ - get_theme_support( 'disable-custom-colors' ), - 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ), - 'disableCustomGradients' => get_theme_support( 'disable-custom-gradients' ), - 'disableLayoutStyles' => get_theme_support( 'disable-layout-styles' ), - 'enableCustomLineHeight' => get_theme_support( 'custom-line-height' ), - 'enableCustomSpacing' => get_theme_support( 'custom-spacing' ), - 'enableCustomUnits' => get_theme_support( 'custom-units' ), - ); - - // Theme settings. - $color_palette = current( (array) get_theme_support( 'editor-color-palette' ) ); - if ( false !== $color_palette ) { - $theme_settings['colors'] = $color_palette; - } - - $font_sizes = current( (array) get_theme_support( 'editor-font-sizes' ) ); - if ( false !== $font_sizes ) { - $theme_settings['fontSizes'] = $font_sizes; - } - - $gradient_presets = current( (array) get_theme_support( 'editor-gradient-presets' ) ); - if ( false !== $gradient_presets ) { - $theme_settings['gradients'] = $gradient_presets; - } - - return $theme_settings; - } -} diff --git a/lib/compat/wordpress-6.2/block-patterns.php b/lib/compat/wordpress-6.2/block-patterns.php deleted file mode 100644 index 12b19bdf4c54e..0000000000000 --- a/lib/compat/wordpress-6.2/block-patterns.php +++ /dev/null @@ -1,333 +0,0 @@ - _x( 'Banners', 'Block pattern category', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'buttons', - array( - 'label' => _x( 'Buttons', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Patterns that contain buttons and call to actions.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'columns', - array( - 'label' => _x( 'Columns', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Multi-column patterns with more complex layouts.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'text', - array( - 'label' => _x( 'Text', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Patterns containing mostly text.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'query', - array( - 'label' => _x( 'Posts', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Display your latest posts in lists, grids or other layouts.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'featured', - array( - 'label' => _x( 'Featured', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'A set of high quality curated patterns.', 'gutenberg' ), - ) - ); - - // Register new core block pattern categories. - register_block_pattern_category( - 'call-to-action', - array( - 'label' => _x( 'Call to Action', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Sections whose purpose is to trigger a specific action.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'team', - array( - 'label' => _x( 'Team', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'A variety of designs to display your team members.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'testimonials', - array( - 'label' => _x( 'Testimonials', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Share reviews and feedback about your brand/business.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'services', - array( - 'label' => _x( 'Services', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Briefly describe what your business does and how you can help.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'contact', - array( - 'label' => _x( 'Contact', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Display your contact information.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'about', - array( - 'label' => _x( 'About', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Introduce yourself.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'portfolio', - array( - 'label' => _x( 'Portfolio', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Showcase your latest work.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'gallery', - array( - 'label' => _x( 'Gallery', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Different layouts for displaying images.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'media', - array( - 'label' => _x( 'Media', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Different layouts containing video or audio.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'posts', - array( - 'label' => _x( 'Posts', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'Display your latest posts in lists, grids or other layouts.', 'gutenberg' ), - ) - ); - // Site building pattern categories. - register_block_pattern_category( - 'footer', - array( - 'label' => _x( 'Footers', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'A variety of footer designs displaying information and site navigation.', 'gutenberg' ), - ) - ); - register_block_pattern_category( - 'header', - array( - 'label' => _x( 'Headers', 'Block pattern category', 'gutenberg' ), - 'description' => __( 'A variety of header designs displaying your site title and navigation.', 'gutenberg' ), - ) - ); -} -add_action( 'init', 'gutenberg_register_core_block_patterns_categories' ); - -/** - * Register any patterns that the active theme may provide under its - * `./patterns/` directory. Each pattern is defined as a PHP file and defines - * its metadata using plugin-style headers. The minimum required definition is: - * - * /** - * * Title: My Pattern - * * Slug: my-theme/my-pattern - * * - * - * The output of the PHP source corresponds to the content of the pattern, e.g.: - * - *

- * - * If applicable, this will collect from both parent and child theme. - * - * Other settable fields include: - * - * - Description - * - Viewport Width - * - Categories (comma-separated values) - * - Keywords (comma-separated values) - * - Block Types (comma-separated values) - * - Post Types (comma-separated values) - * - Inserter (yes/no) - * - * @since 6.0.0 - * @access private - */ -function gutenberg_register_theme_block_patterns() { - $default_headers = array( - 'title' => 'Title', - 'slug' => 'Slug', - 'description' => 'Description', - 'viewportWidth' => 'Viewport Width', - 'categories' => 'Categories', - 'keywords' => 'Keywords', - 'blockTypes' => 'Block Types', - 'postTypes' => 'Post Types', - 'inserter' => 'Inserter', - 'templateTypes' => 'Template Types', - ); - - /* - * Register patterns for the active theme. If the theme is a child theme, - * let it override any patterns from the parent theme that shares the same slug. - */ - $themes = array(); - $wp_theme = wp_get_theme(); - if ( $wp_theme->parent() ) { - $themes[] = $wp_theme->parent(); - } - $themes[] = $wp_theme; - - foreach ( $themes as $theme ) { - $dirpath = $theme->get_stylesheet_directory() . '/patterns/'; - if ( ! is_dir( $dirpath ) || ! is_readable( $dirpath ) ) { - continue; - } - if ( file_exists( $dirpath ) ) { - $files = glob( $dirpath . '*.php' ); - if ( $files ) { - foreach ( $files as $file ) { - $pattern_data = get_file_data( $file, $default_headers ); - - if ( empty( $pattern_data['slug'] ) ) { - _doing_it_wrong( - '_register_theme_block_patterns', - sprintf( - /* translators: %s: file name. */ - __( 'Could not register file "%s" as a block pattern ("Slug" field missing)', 'gutenberg' ), - $file - ), - '6.0.0' - ); - continue; - } - - if ( ! preg_match( '/^[A-z0-9\/_-]+$/', $pattern_data['slug'] ) ) { - _doing_it_wrong( - '_register_theme_block_patterns', - sprintf( - /* translators: %1s: file name; %2s: slug value found. */ - __( 'Could not register file "%1$s" as a block pattern (invalid slug "%2$s")', 'gutenberg' ), - $file, - $pattern_data['slug'] - ), - '6.0.0' - ); - } - - if ( WP_Block_Patterns_Registry::get_instance()->is_registered( $pattern_data['slug'] ) ) { - continue; - } - - // Title is a required property. - if ( ! $pattern_data['title'] ) { - _doing_it_wrong( - '_register_theme_block_patterns', - sprintf( - /* translators: %1s: file name; %2s: slug value found. */ - __( 'Could not register file "%s" as a block pattern ("Title" field missing)', 'gutenberg' ), - $file - ), - '6.0.0' - ); - continue; - } - - // For properties of type array, parse data as comma-separated. - foreach ( array( 'categories', 'keywords', 'blockTypes', 'postTypes', 'templateTypes' ) as $property ) { - if ( ! empty( $pattern_data[ $property ] ) ) { - $pattern_data[ $property ] = array_filter( - preg_split( - '/[\s,]+/', - (string) $pattern_data[ $property ] - ) - ); - } else { - unset( $pattern_data[ $property ] ); - } - } - - // Parse properties of type int. - foreach ( array( 'viewportWidth' ) as $property ) { - if ( ! empty( $pattern_data[ $property ] ) ) { - $pattern_data[ $property ] = (int) $pattern_data[ $property ]; - } else { - unset( $pattern_data[ $property ] ); - } - } - - // Parse properties of type bool. - foreach ( array( 'inserter' ) as $property ) { - if ( ! empty( $pattern_data[ $property ] ) ) { - $pattern_data[ $property ] = in_array( - strtolower( $pattern_data[ $property ] ), - array( 'yes', 'true' ), - true - ); - } else { - unset( $pattern_data[ $property ] ); - } - } - - // Translate the pattern metadata. - $text_domain = $theme->get( 'TextDomain' ); - //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralContext, WordPress.WP.I18n.NonSingularStringLiteralDomain, WordPress.WP.I18n.LowLevelTranslationFunction - $pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain ); - if ( ! empty( $pattern_data['description'] ) ) { - //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralContext, WordPress.WP.I18n.NonSingularStringLiteralDomain, WordPress.WP.I18n.LowLevelTranslationFunction - $pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain ); - } - - // The actual pattern content is the output of the file. - ob_start(); - include $file; - $pattern_data['content'] = ob_get_clean(); - if ( ! $pattern_data['content'] ) { - continue; - } - - register_block_pattern( $pattern_data['slug'], $pattern_data ); - } - } - } - } -} -remove_action( 'init', '_register_theme_block_patterns' ); -add_action( 'init', 'gutenberg_register_theme_block_patterns' ); - -/** - * Normalize the pattern from the API (snake_case) to the format expected by `register_block_pattern` (camelCase). - * - * @since 6.2.0 - * - * @param array $pattern Pattern as returned from the Pattern Directory API. - */ -function gutenberg_normalize_remote_pattern( $pattern ) { - if ( isset( $pattern['block_types'] ) ) { - $pattern['blockTypes'] = $pattern['block_types']; - unset( $pattern['block_types'] ); - } - - if ( isset( $pattern['viewport_width'] ) ) { - $pattern['viewportWidth'] = $pattern['viewport_width']; - unset( $pattern['viewport_width'] ); - } - - return (array) $pattern; -} diff --git a/lib/compat/wordpress-6.2/block-template-utils.php b/lib/compat/wordpress-6.2/block-template-utils.php deleted file mode 100644 index db9bf427e6b47..0000000000000 --- a/lib/compat/wordpress-6.2/block-template-utils.php +++ /dev/null @@ -1,97 +0,0 @@ - The template hierarchy. - */ -function gutenberg_get_template_hierarchy( $slug, $is_custom = false, $template_prefix = '' ) { - if ( 'index' === $slug ) { - return array( 'index' ); - } - if ( $is_custom ) { - return array( 'page', 'singular', 'index' ); - } - if ( 'front-page' === $slug ) { - return array( 'front-page', 'home', 'index' ); - } - - $matches = array(); - - $template_hierarchy = array( $slug ); - // Most default templates don't have `$template_prefix` assigned. - if ( ! empty( $template_prefix ) ) { - list( $type ) = explode( '-', $template_prefix ); - // We need these checks because we always add the `$slug` above. - if ( ! in_array( $template_prefix, array( $slug, $type ), true ) ) { - $template_hierarchy[] = $template_prefix; - } - if ( $slug !== $type ) { - $template_hierarchy[] = $type; - } - } elseif ( preg_match( '/^(author|category|archive|tag|page)-.+$/', $slug, $matches ) ) { - $template_hierarchy[] = $matches[1]; - } elseif ( preg_match( '/^(taxonomy|single)-(.+)$/', $slug, $matches ) ) { - $type = $matches[1]; - $slug_remaining = $matches[2]; - - $items = 'single' === $type ? get_post_types() : get_taxonomies(); - foreach ( $items as $item ) { - if ( ! str_starts_with( $slug_remaining, $item ) ) { - continue; - } - - // If $slug_remaining is equal to $post_type or $taxonomy we have - // the single-$post_type template or the taxonomy-$taxonomy template. - if ( $slug_remaining === $item ) { - $template_hierarchy[] = $type; - break; - } - - // If $slug_remaining is single-$post_type-$slug template. - if ( strlen( $slug_remaining ) > strlen( $item ) + 1 ) { - $template_hierarchy[] = "$type-$item"; - $template_hierarchy[] = $type; - break; - } - } - } - // Handle `archive` template. - if ( - str_starts_with( $slug, 'author' ) || - str_starts_with( $slug, 'taxonomy' ) || - str_starts_with( $slug, 'category' ) || - str_starts_with( $slug, 'tag' ) || - 'date' === $slug - ) { - $template_hierarchy[] = 'archive'; - } - // Handle `single` template. - if ( 'attachment' === $slug ) { - $template_hierarchy[] = 'single'; - } - // Handle `singular` template. - if ( - str_starts_with( $slug, 'single' ) || - str_starts_with( $slug, 'page' ) || - 'attachment' === $slug - ) { - $template_hierarchy[] = 'singular'; - } - $template_hierarchy[] = 'index'; - return $template_hierarchy; -} diff --git a/lib/compat/wordpress-6.2/blocks.php b/lib/compat/wordpress-6.2/blocks.php deleted file mode 100644 index 94c6eaabcef9b..0000000000000 --- a/lib/compat/wordpress-6.2/blocks.php +++ /dev/null @@ -1,173 +0,0 @@ -= 6.2. - * - * @param string[] $attrs Array of allowed CSS attributes. - * @return string[] CSS attributes. - */ -function gutenberg_safe_style_attrs_6_2( $attrs ) { - $attrs[] = 'position'; - $attrs[] = 'top'; - $attrs[] = 'right'; - $attrs[] = 'bottom'; - $attrs[] = 'left'; - $attrs[] = 'z-index'; - $attrs[] = 'box-shadow'; - $attrs[] = 'aspect-ratio'; - - return $attrs; -} -add_filter( 'safe_style_css', 'gutenberg_safe_style_attrs_6_2' ); - -/** - * Helper function that constructs a WP_Query args array from - * a `Query` block properties. - * - * It's used in QueryLoop, QueryPaginationNumbers and QueryPaginationNext blocks. - * - * `build_query_vars_from_query_block` was introduced in 5.8, for 6.1 we just need - * to update that function and not create a new one. - * - * @param WP_Block $block Block instance. - * @param int $page Current query's page. - * - * @return array Returns the constructed WP_Query arguments. - */ -function gutenberg_build_query_vars_from_query_block( $block, $page ) { - $query = array( - 'post_type' => 'post', - 'order' => 'DESC', - 'orderby' => 'date', - 'post__not_in' => array(), - ); - - if ( isset( $block->context['query'] ) ) { - if ( ! empty( $block->context['query']['postType'] ) ) { - $post_type_param = $block->context['query']['postType']; - if ( is_post_type_viewable( $post_type_param ) ) { - $query['post_type'] = $post_type_param; - } - } - if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { - $sticky = get_option( 'sticky_posts' ); - if ( 'only' === $block->context['query']['sticky'] ) { - /** - * Passing an empty array to post__in will return have_posts() as true (and all posts will be returned). - * Logic should be used before hand to determine if WP_Query should be used in the event that the array - * being passed to post__in is empty. - * - * @see https://core.trac.wordpress.org/ticket/28099 - */ - $query['post__in'] = ! empty( $sticky ) ? $sticky : array( 0 ); - $query['ignore_sticky_posts'] = 1; - } else { - $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); - } - } - if ( ! empty( $block->context['query']['exclude'] ) ) { - $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); - $excluded_post_ids = array_filter( $excluded_post_ids ); - $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); - } - if ( - isset( $block->context['query']['perPage'] ) && - is_numeric( $block->context['query']['perPage'] ) - ) { - $per_page = absint( $block->context['query']['perPage'] ); - $offset = 0; - - if ( - isset( $block->context['query']['offset'] ) && - is_numeric( $block->context['query']['offset'] ) - ) { - $offset = absint( $block->context['query']['offset'] ); - } - - $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; - $query['posts_per_page'] = $per_page; - } - - // Migrate `categoryIds` and `tagIds` to `tax_query` for backwards compatibility. - if ( ! empty( $block->context['query']['categoryIds'] ) || ! empty( $block->context['query']['tagIds'] ) ) { - $tax_query = array(); - if ( ! empty( $block->context['query']['categoryIds'] ) ) { - $tax_query[] = array( - 'taxonomy' => 'category', - 'terms' => array_filter( array_map( 'intval', $block->context['query']['categoryIds'] ) ), - 'include_children' => false, - ); - } - if ( ! empty( $block->context['query']['tagIds'] ) ) { - $tax_query[] = array( - 'taxonomy' => 'post_tag', - 'terms' => array_filter( array_map( 'intval', $block->context['query']['tagIds'] ) ), - 'include_children' => false, - ); - } - $query['tax_query'] = $tax_query; - } - if ( ! empty( $block->context['query']['taxQuery'] ) ) { - $query['tax_query'] = array(); - foreach ( $block->context['query']['taxQuery'] as $taxonomy => $terms ) { - if ( is_taxonomy_viewable( $taxonomy ) && ! empty( $terms ) ) { - $query['tax_query'][] = array( - 'taxonomy' => $taxonomy, - 'terms' => array_filter( array_map( 'intval', $terms ) ), - 'include_children' => false, - ); - } - } - } - if ( - isset( $block->context['query']['order'] ) && - in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) - ) { - $query['order'] = strtoupper( $block->context['query']['order'] ); - } - if ( isset( $block->context['query']['orderBy'] ) ) { - $query['orderby'] = $block->context['query']['orderBy']; - } - if ( - isset( $block->context['query']['author'] ) && - (int) $block->context['query']['author'] > 0 - ) { - $query['author'] = (int) $block->context['query']['author']; - } - if ( ! empty( $block->context['query']['search'] ) ) { - $query['s'] = $block->context['query']['search']; - } - if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) { - $query['post_parent__in'] = array_filter( array_map( 'intval', $block->context['query']['parents'] ) ); - } - } - - /** - * Filters the arguments which will be passed to `WP_Query` for the Query Loop Block. - * - * Anything to this filter should be compatible with the `WP_Query` API to form - * the query context which will be passed down to the Query Loop Block's children. - * This can help, for example, to include additional settings or meta queries not - * directly supported by the core Query Loop Block, and extend its capabilities. - * - * Please note that this will only influence the query that will be rendered on the - * front-end. The editor preview is not affected by this filter. Also, worth noting - * that the editor preview uses the REST API, so, ideally, one should aim to provide - * attributes which are also compatible with the REST API, in order to be able to - * implement identical queries on both sides. - * - * @since 6.1.0 - * - * @param array $query Array containing parameters for `WP_Query` as parsed by the block context. - * @param WP_Block $block Block instance. - * @param int $page Current query's page. - */ - return apply_filters( 'query_loop_block_query_vars', $query, $block, $page ); -} diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php deleted file mode 100644 index 3897a8945a5f9..0000000000000 --- a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-pattern-categories-controller.php +++ /dev/null @@ -1,84 +0,0 @@ -get_fields_for_response( $request ); - $keys = array( 'name', 'label', 'description' ); - $data = array(); - foreach ( $keys as $key ) { - if ( isset( $item[ $key ] ) && rest_is_field_included( $key, $fields ) ) { - $data[ $key ] = $item[ $key ]; - } - } - - $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 block pattern category schema, conforming to JSON Schema. - * - * @since 6.0.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' => 'block-pattern-category', - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'description' => __( 'The category name.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'label' => array( - 'description' => __( 'The category label, in human readable format.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'description' => array( - 'description' => __( 'The category description, in human readable format.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } -} diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php deleted file mode 100644 index 80de88aa31a81..0000000000000 --- a/lib/compat/wordpress-6.2/class-gutenberg-rest-block-patterns-controller-6-2.php +++ /dev/null @@ -1,235 +0,0 @@ - 'call-to-action', - 'columns' => 'text', - 'query' => 'posts', - ); - - /** - * Prepare a raw block pattern before it gets output in a REST API response. - * - * @since 6.0.0 - * - * @param array $item Raw pattern as registered, before any changes. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function prepare_item_for_response( $item, $request ) { - $fields = $this->get_fields_for_response( $request ); - $keys = array( - 'name' => 'name', - 'title' => 'title', - 'description' => 'description', - 'viewportWidth' => 'viewport_width', - 'blockTypes' => 'block_types', - 'postTypes' => 'post_types', - 'categories' => 'categories', - 'keywords' => 'keywords', - 'content' => 'content', - 'inserter' => 'inserter', - 'templateTypes' => 'template_types', - ); - $data = array(); - foreach ( $keys as $item_key => $rest_key ) { - if ( isset( $item[ $item_key ] ) && rest_is_field_included( $rest_key, $fields ) ) { - $data[ $rest_key ] = $item[ $item_key ]; - } - } - $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 block pattern schema, conforming to JSON Schema. - * - * @since 6.0.0 - * @since 6.1.0 Added `post_types` property. - * - * @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' => 'block-pattern', - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'description' => __( 'The pattern name.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'title' => array( - 'description' => __( 'The pattern title, in human readable format.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'description' => array( - 'description' => __( 'The pattern detailed description.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'viewport_width' => array( - 'description' => __( 'The pattern viewport width for inserter preview.', 'gutenberg' ), - 'type' => 'number', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'block_types' => array( - 'description' => __( 'Block types that the pattern is intended to be used with.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'post_types' => array( - 'description' => __( 'An array of post types that the pattern is restricted to be used with.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'categories' => array( - 'description' => __( 'The pattern category slugs.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'keywords' => array( - 'description' => __( 'The pattern keywords.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'template_types' => array( - 'description' => __( 'An array of template types where the pattern fits.', 'gutenberg' ), - 'type' => 'array', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'content' => array( - 'description' => __( 'The pattern content.', 'gutenberg' ), - 'type' => 'string', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'inserter' => array( - 'description' => __( 'Determines whether the pattern is visible in inserter.', 'gutenberg' ), - 'type' => 'boolean', - 'readonly' => true, - 'context' => array( 'view', 'edit', 'embed' ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Registers the routes for the objects of the controller. - * - * @since 6.0.0 - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ), - true - ); - } - /** - * Retrieves all block patterns. - * - * @since 6.0.0 - * @since 6.2.0 Added migration for old core pattern categories to the new ones. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function get_items( $request ) { - if ( ! $this->remote_patterns_loaded ) { - // Load block patterns from w.org. - gutenberg_load_remote_block_patterns(); // Patterns with the `core` keyword. - gutenberg_load_remote_featured_patterns(); // Patterns in the `featured` category. - gutenberg_register_remote_theme_patterns(); // Patterns requested by current theme. - - $this->remote_patterns_loaded = true; - } - - $response = array(); - $patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered(); - foreach ( $patterns as $pattern ) { - $migrated_pattern = $this->migrate_pattern_categories( $pattern ); - $prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request ); - $response[] = $this->prepare_response_for_collection( $prepared_pattern ); - } - return rest_ensure_response( $response ); - } - - /** - * Migrates old core pattern categories to new ones. - * - * Core pattern categories are being revamped and we need to handle the migration - * to the new ones and ensure backwards compatibility. - * - * @since 6.2.0 - * - * @param array $pattern Raw pattern as registered, before applying any changes. - * @return array Migrated pattern. - */ - protected function migrate_pattern_categories( $pattern ) { - if ( isset( $pattern['categories'] ) && is_array( $pattern['categories'] ) ) { - foreach ( $pattern['categories'] as $i => $category ) { - if ( array_key_exists( $category, static::$categories_migration ) ) { - $pattern['categories'][ $i ] = static::$categories_migration[ $category ]; - } - } - } - return $pattern; - } -} diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php deleted file mode 100644 index 7043140c23fcf..0000000000000 --- a/lib/compat/wordpress-6.2/class-gutenberg-rest-pattern-directory-controller-6-2.php +++ /dev/null @@ -1,122 +0,0 @@ - true, - 'order' => true, - 'orderby' => true, - 'page' => true, - 'per_page' => true, - 'search' => true, - 'slug' => true, - ); - - $query_args = array_intersect_key( $request->get_params(), $valid_query_args ); - - $query_args['locale'] = get_user_locale(); - $query_args['wp-version'] = $wp_version; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable -- it's defined in `version.php` above. - $query_args['gutenberg-version'] = $gutenberg_data['Version']; - $query_args['pattern-categories'] = isset( $request['category'] ) ? $request['category'] : false; - $query_args['pattern-keywords'] = isset( $request['keyword'] ) ? $request['keyword'] : false; - - $query_args = array_filter( $query_args ); - - $transient_key = $this->get_transient_key( $query_args ); - - /* - * Use network-wide transient to improve performance. The locale is the only site - * configuration that affects the response, and it's included in the transient key. - */ - $raw_patterns = get_site_transient( $transient_key ); - - if ( ! $raw_patterns ) { - $api_url = 'http://api.wordpress.org/patterns/1.0/?' . build_query( $query_args ); - if ( wp_http_supports( array( 'ssl' ) ) ) { - $api_url = set_url_scheme( $api_url, 'https' ); - } - - /* - * Default to a short TTL, to mitigate cache stampedes on high-traffic sites. - * This assumes that most errors will be short-lived, e.g., packet loss that causes the - * first request to fail, but a follow-up one will succeed. The value should be high - * enough to avoid stampedes, but low enough to not interfere with users manually - * re-trying a failed request. - */ - $cache_ttl = 5; - $wporg_response = wp_remote_get( $api_url ); - $raw_patterns = json_decode( wp_remote_retrieve_body( $wporg_response ) ); - - if ( is_wp_error( $wporg_response ) ) { - $raw_patterns = $wporg_response; - - } elseif ( ! is_array( $raw_patterns ) ) { - // HTTP request succeeded, but response data is invalid. - $raw_patterns = new WP_Error( - 'pattern_api_failed', - sprintf( - /* translators: %s: Support forums URL. */ - __( 'An unexpected error occurred. Something may be wrong with WordPress.org or this server’s configuration. If you continue to have problems, please try the support forums.', 'gutenberg' ), - __( 'https://wordpress.org/support/forums/', 'gutenberg' ) - ), - array( - 'response' => wp_remote_retrieve_body( $wporg_response ), - ) - ); - - } else { - // Response has valid data. - $cache_ttl = HOUR_IN_SECONDS; - } - - set_site_transient( $transient_key, $raw_patterns, $cache_ttl ); - } - - if ( is_wp_error( $raw_patterns ) ) { - $raw_patterns->add_data( array( 'status' => 500 ) ); - - return $raw_patterns; - } - - $response = array(); - - if ( $raw_patterns ) { - foreach ( $raw_patterns as $pattern ) { - $response[] = $this->prepare_response_for_collection( - $this->prepare_item_for_response( $pattern, $request ) - ); - } - } - - return new WP_REST_Response( $response ); - } -} diff --git a/lib/compat/wordpress-6.2/default-filters.php b/lib/compat/wordpress-6.2/default-filters.php deleted file mode 100644 index 32a7a33157a74..0000000000000 --- a/lib/compat/wordpress-6.2/default-filters.php +++ /dev/null @@ -1,18 +0,0 @@ -name = $name; - $this->value_starts_at = $value_start; - $this->value_length = $value_length; - $this->start = $start; - $this->end = $end; - $this->is_true = $is_true; - } -} diff --git a/lib/compat/wordpress-6.2/html-api/class-wp-html-span.php b/lib/compat/wordpress-6.2/html-api/class-wp-html-span.php deleted file mode 100644 index e38bc55192317..0000000000000 --- a/lib/compat/wordpress-6.2/html-api/class-wp-html-span.php +++ /dev/null @@ -1,56 +0,0 @@ -start = $start; - $this->end = $end; - } -} diff --git a/lib/compat/wordpress-6.2/html-api/class-wp-html-tag-processor.php b/lib/compat/wordpress-6.2/html-api/class-wp-html-tag-processor.php deleted file mode 100644 index d61180074f608..0000000000000 --- a/lib/compat/wordpress-6.2/html-api/class-wp-html-tag-processor.php +++ /dev/null @@ -1,2291 +0,0 @@ - "c" not " c". - * This would increase the size of the changes for some operations but leave more - * natural-looking output HTML. - * - Decode HTML character references within class names when matching. E.g. match having - * class `1<"2` needs to recognize `class="1<"2"`. Currently the Tag Processor - * will fail to find the right tag if the class name is encoded as such. - * - Properly decode HTML character references in `get_attribute()`. PHP's - * `html_entity_decode()` is wrong in a couple ways: it doesn't account for the - * no-ambiguous-ampersand rule, and it improperly handles the way semicolons may - * or may not terminate a character reference. - * - * @package WordPress - * @subpackage HTML-API - * @since 6.2.0 - */ - -if ( class_exists( 'WP_HTML_Tag_Processor' ) ) { - return; -} - -/** - * Modifies attributes in an HTML document for tags matching a query. - * - * ## Usage - * - * Use of this class requires three steps: - * - * 1. Create a new class instance with your input HTML document. - * 2. Find the tag(s) you are looking for. - * 3. Request changes to the attributes in those tag(s). - * - * Example: - * - * $tags = new WP_HTML_Tag_Processor( $html ); - * if ( $tags->next_tag( 'option' ) ) { - * $tags->set_attribute( 'selected', true ); - * } - * - * ### Finding tags - * - * The `next_tag()` function moves the internal cursor through - * your input HTML document until it finds a tag meeting any of - * the supplied restrictions in the optional query argument. If - * no argument is provided then it will find the next HTML tag, - * regardless of what kind it is. - * - * If you want to _find whatever the next tag is_: - * - * $tags->next_tag(); - * - * | Goal | Query | - * |-----------------------------------------------------------|---------------------------------------------------------------------------------| - * | Find any tag. | `$tags->next_tag();` | - * | Find next image tag. | `$tags->next_tag( array( 'tag_name' => 'img' ) );` | - * | Find next image tag (without passing the array). | `$tags->next_tag( 'img' );` | - * | Find next tag containing the `fullwidth` CSS class. | `$tags->next_tag( array( 'class_name' => 'fullwidth' ) );` | - * | Find next image tag containing the `fullwidth` CSS class. | `$tags->next_tag( array( 'tag_name' => 'img', 'class_name' => 'fullwidth' ) );` | - * - * If a tag was found meeting your criteria then `next_tag()` - * will return `true` and you can proceed to modify it. If it - * returns `false`, however, it failed to find the tag and - * moved the cursor to the end of the file. - * - * Once the cursor reaches the end of the file the processor - * is done and if you want to reach an earlier tag you will - * need to recreate the processor and start over, as it's - * unable to back up or move in reverse. - * - * See the section on bookmarks for an exception to this - * no-backing-up rule. - * - * #### Custom queries - * - * Sometimes it's necessary to further inspect an HTML tag than - * the query syntax here permits. In these cases one may further - * inspect the search results using the read-only functions - * provided by the processor or external state or variables. - * - * Example: - * - * // Paint up to the first five DIV or SPAN tags marked with the "jazzy" style. - * $remaining_count = 5; - * while ( $remaining_count > 0 && $tags->next_tag() ) { - * if ( - * ( 'DIV' === $tags->get_tag() || 'SPAN' === $tags->get_tag() ) && - * 'jazzy' === $tags->get_attribute( 'data-style' ) - * ) { - * $tags->add_class( 'theme-style-everest-jazz' ); - * $remaining_count--; - * } - * } - * - * `get_attribute()` will return `null` if the attribute wasn't present - * on the tag when it was called. It may return `""` (the empty string) - * in cases where the attribute was present but its value was empty. - * For boolean attributes, those whose name is present but no value is - * given, it will return `true` (the only way to set `false` for an - * attribute is to remove it). - * - * ### 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' => '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++; - * } - * } - * } - * - * ## 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 �. 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. - * - * @since 6.2.0 - */ -class WP_HTML_Tag_Processor { - /** - * 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; - - /** - * 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 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 in input document where current tag token ends. - * - * Example: - * - *
... - * 0 1 | - * 01234567890123456 - * --- tag name ends at 14 - * - * @since 6.2.0 - * @var int|null - */ - private $tag_ends_at; - - /** - * 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_Match( 'id', null, 6, 17 ) - * ); - * - * // 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_Match( 'id', null, 6, 17 ), - * 'class' => new WP_HTML_Attribute_Match( 'class', 'outline', 18, 32 ) - * ); - * - * // 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(); - - /** - * 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; - * $end = $attributes['src']->end; - * $modifications[] = new WP_HTML_Text_Replacement( $start, $end, $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 - * - * @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 boolean Whether a tag was matched. - */ - public function next_tag( $query = null ) { - $this->parse_query( $query ); - $already_found = 0; - - do { - if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { - return false; - } - - // Find the next tag if it exists. - if ( false === $this->parse_next_tag() ) { - $this->bytes_already_parsed = strlen( $this->html ); - - return false; - } - - // Parse all of its attributes. - while ( $this->parse_next_attribute() ) { - continue; - } - - // Ensure that the tag closes before the end of the document. - $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); - if ( false === $tag_ends_at ) { - return false; - } - $this->tag_ends_at = $tag_ends_at; - $this->bytes_already_parsed = $tag_ends_at; - - // Finally, check if the parsed tag and its attributes match the search query. - if ( $this->matches() ) { - ++$already_found; - } - - /* - * 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 && ( 's' === $t || 'S' === $t || 't' === $t || 'T' === $t ) ) { - $tag_name = $this->get_tag(); - - if ( 'SCRIPT' === $tag_name && ! $this->skip_script_data() ) { - $this->bytes_already_parsed = strlen( $this->html ); - return false; - } elseif ( - ( 'TEXTAREA' === $tag_name || 'TITLE' === $tag_name ) && - ! $this->skip_rcdata( $tag_name ) - ) { - $this->bytes_already_parsed = strlen( $this->html ); - return false; - } - } - } while ( $already_found < $this->sought_match_offset ); - - return true; - } - - - /** - * 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 ) { - if ( null === $this->tag_name_starts_at ) { - return false; - } - - if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= self::MAX_BOOKMARKS ) { - _doing_it_wrong( - __METHOD__, - __( 'Too many bookmarks: cannot create any more.' ), - '6.2.0' - ); - return false; - } - - $this->bookmarks[ $name ] = new WP_HTML_Span( - $this->tag_name_starts_at - ( $this->is_closing_tag ? 2 : 1 ), - $this->tag_ends_at - ); - - 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 title and textarea tags. - * - * @since 6.2.0 - * - * @see https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state - * - * @param string $tag_name The lowercase 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, '= $doc_length ) { - $this->bytes_already_parsed = $doc_length; - return false; - } - - $closer_potentially_starts_at = $at; - $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; - - /* - * 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 ] || '/' === $html[ $at ] ) { - $this->bytes_already_parsed = $closer_potentially_starts_at; - 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 " - * 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; - } - - /* - * - * 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; - } - - /** - * Move the internal cursor past any immediate successive whitespace. - * - * @since 6.2.0 - * - * @return void - */ - 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 - * - * @return void - */ - 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(); - } - - /** - * 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 - * - * @return void - */ - 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. - * - * @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 ) { - /* - * 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 ) { - $update_head = $bookmark->start >= $diff->start; - $update_tail = $bookmark->end >= $diff->start; - - if ( ! $update_head && ! $update_tail ) { - break; - } - - $delta = strlen( $diff->text ) - ( $diff->end - $diff->start ); - - if ( $update_head ) { - $head_delta += $delta; - } - - if ( $update_tail ) { - $tail_delta += $delta; - } - } - - $bookmark->start += $head_delta; - $bookmark->end += $tail_delta; - } - - $this->lexical_updates = array(); - - return $accumulated_shift_for_given_point; - } - - /** - * 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 > self::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. - */ - 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 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. - '"\'>& 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 { - $escaped_new_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, - '' - ); - - 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. - * - * @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. - * - *

Previous HTMLMore HTML

- * ^ | back up by the length of the tag name plus the opening < - * \<-/ back up by strlen("em") + 1 ==> 3 - */ - - // Store existing state so it can be restored after reparsing. - $previous_parsed_byte_count = $this->bytes_already_parsed; - $previous_query = $this->last_query; - - // Reparse attributes. - $this->bytes_already_parsed = $before_current_tag; - $this->next_tag(); - - // Restore previous state. - $this->bytes_already_parsed = $previous_parsed_byte_count; - $this->parse_query( $previous_query ); - - 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.
. - * } - * @return void - */ - 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 boolean 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; - } - } - } - - $needs_class_name = null !== $this->sought_class_name; - - if ( $needs_class_name && ! isset( $this->attributes['class'] ) ) { - return false; - } - - /* - * Match byte-for-byte (case-sensitive and encoding-form-sensitive) on the class name. - * - * This will overlook certain classes that exist in other lexical variations - * than was supplied to the search query, but requires more complicated searching. - */ - if ( $needs_class_name ) { - $class_start = $this->attributes['class']->value_starts_at; - $class_end = $class_start + $this->attributes['class']->value_length; - $class_at = $class_start; - - /* - * Ensure that boundaries surround the class name to avoid matching on - * substrings of a longer name. For example, the sequence "not-odd" - * should not match for the class "odd" even though "odd" is found - * within the class attribute text. - * - * See https://html.spec.whatwg.org/#attributes-3 - * See https://html.spec.whatwg.org/#space-separated-tokens - */ - while ( - // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition - false !== ( $class_at = strpos( $this->html, $this->sought_class_name, $class_at ) ) && - $class_at < $class_end - ) { - /* - * Verify this class starts at a boundary. - */ - if ( $class_at > $class_start ) { - $character = $this->html[ $class_at - 1 ]; - - if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) { - $class_at += strlen( $this->sought_class_name ); - continue; - } - } - - /* - * Verify this class ends at a boundary as well. - */ - if ( $class_at + strlen( $this->sought_class_name ) < $class_end ) { - $character = $this->html[ $class_at + strlen( $this->sought_class_name ) ]; - - if ( ' ' !== $character && "\t" !== $character && "\f" !== $character && "\r" !== $character && "\n" !== $character ) { - $class_at += strlen( $this->sought_class_name ); - continue; - } - } - - return true; - } - - return false; - } - - return true; - } -} diff --git a/lib/compat/wordpress-6.2/html-api/class-wp-html-text-replacement.php b/lib/compat/wordpress-6.2/html-api/class-wp-html-text-replacement.php deleted file mode 100644 index b3f70c8e7c57f..0000000000000 --- a/lib/compat/wordpress-6.2/html-api/class-wp-html-text-replacement.php +++ /dev/null @@ -1,63 +0,0 @@ -start = $start; - $this->end = $end; - $this->text = $text; - } -} diff --git a/lib/compat/wordpress-6.2/menu.php b/lib/compat/wordpress-6.2/menu.php deleted file mode 100644 index e0d582ad3dc98..0000000000000 --- a/lib/compat/wordpress-6.2/menu.php +++ /dev/null @@ -1,35 +0,0 @@ - $menu_item ) { - if ( str_contains( $menu_item[2], 'site-editor.php?postType=wp_template_part' ) && ! str_contains( $menu_item[2], 'path=' ) ) { - $submenu['themes.php'][ $index ][2] = 'site-editor.php?postType=wp_template_part&path=/wp_template_part/all'; - break; - } - } -} -add_action( 'admin_menu', 'gutenberg_update_template_parts_menu_url' ); diff --git a/lib/compat/wordpress-6.2/rest-api.php b/lib/compat/wordpress-6.2/rest-api.php deleted file mode 100644 index 97f7daecdff2f..0000000000000 --- a/lib/compat/wordpress-6.2/rest-api.php +++ /dev/null @@ -1,145 +0,0 @@ -register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_block_pattern_categories' ); - -/** - * Add extra collection params to pattern directory requests. - * - * @param array $query_params JSON Schema-formatted collection parameters. - * @return array Updated parameters. - */ -function gutenberg_pattern_directory_collection_params_6_2( $query_params ) { - $query_params['page'] = array( - 'description' => __( 'Current page of the collection.', 'gutenberg' ), - 'type' => 'integer', - 'default' => 1, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - 'minimum' => 1, - ); - - $query_params['per_page'] = array( - 'description' => __( 'Maximum number of items to be returned in result set.', 'gutenberg' ), - 'type' => 'integer', - 'default' => 100, - 'minimum' => 1, - 'maximum' => 100, - 'sanitize_callback' => 'absint', - 'validate_callback' => 'rest_validate_request_arg', - ); - - $query_params['offset'] = array( - 'description' => __( 'Offset the result set by a specific number of items.', 'gutenberg' ), - 'type' => 'integer', - ); - - $query_params['order'] = array( - 'description' => __( 'Order sort attribute ascending or descending.', 'gutenberg' ), - 'type' => 'string', - 'default' => 'desc', - 'enum' => array( 'asc', 'desc' ), - ); - - $query_params['orderby'] = array( - 'description' => __( 'Sort collection by post attribute.', 'gutenberg' ), - 'type' => 'string', - 'default' => 'date', - 'enum' => array( - 'author', - 'date', - 'id', - 'include', - 'modified', - 'parent', - 'relevance', - 'slug', - 'include_slugs', - 'title', - 'favorite_count', - ), - ); - - return $query_params; -} -add_filter( 'rest_pattern_directory_collection_params', 'gutenberg_pattern_directory_collection_params_6_2' ); - -/** - * Updates REST API response for the sidebars and marks them as 'inactive'. - * - * Note: This can be a part of the `prepare_item_for_response` in `class-wp-rest-sidebars-controller.php`. - * - * @param WP_REST_Response $response The sidebar response object. - * @return WP_REST_Response $response Updated response object. - */ -function gutenberg_modify_rest_sidebars_response( $response ) { - if ( wp_is_block_theme() ) { - $response->data['status'] = 'inactive'; - } - return $response; -} -add_filter( 'rest_prepare_sidebar', 'gutenberg_modify_rest_sidebars_response' ); - -if ( ! function_exists( 'add_block_pattern_block_types_schema' ) ) { - /** - * Add the `block_types` value to the `pattern-directory-item` schema. - * - * @since 6.2.0 Added 'block_types' property. - */ - function add_block_pattern_block_types_schema() { - register_rest_field( - 'pattern-directory-item', - 'block_types', - array( - 'schema' => array( - 'description' => __( 'The block types which can use this pattern.', 'gutenberg' ), - 'type' => 'array', - 'uniqueItems' => true, - 'items' => array( 'type' => 'string' ), - 'context' => array( 'view', 'embed' ), - ), - ) - ); - } -} -add_filter( 'rest_api_init', 'add_block_pattern_block_types_schema' ); - - -if ( ! function_exists( 'filter_block_pattern_response' ) ) { - /** - * Add the `block_types` value into the API response. - * - * @since 6.2.0 Added 'block_types' property. - * - * @param WP_REST_Response $response The response object. - * @param object $raw_pattern The unprepared pattern. - */ - function filter_block_pattern_response( $response, $raw_pattern ) { - $data = $response->get_data(); - $data['block_types'] = array_map( 'sanitize_text_field', $raw_pattern->meta->wpop_block_types ); - $response->set_data( $data ); - return $response; - } -} -add_filter( 'rest_prepare_block_pattern', 'filter_block_pattern_response', 10, 2 ); - - -/** - * Registers the block pattern directory. - */ -function gutenberg_register_rest_pattern_directory() { - $pattern_directory_controller = new Gutenberg_REST_Pattern_Directory_Controller_6_2(); - $pattern_directory_controller->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_pattern_directory' ); diff --git a/lib/compat/wordpress-6.2/script-loader.php b/lib/compat/wordpress-6.2/script-loader.php deleted file mode 100644 index 37c1ced3c8cfc..0000000000000 --- a/lib/compat/wordpress-6.2/script-loader.php +++ /dev/null @@ -1,75 +0,0 @@ -query( 'wp-inert-polyfill', 'registered' ); - if ( ! $script ) { - $scripts->add( 'wp-inert-polyfill', gutenberg_url( 'build/vendors/inert-polyfill' . $extension ), array() ); - } - - $script = $scripts->query( 'wp-polyfill', 'registered' ); - $script->deps = array_merge( $script->deps, array( 'wp-inert-polyfill' ) ); -} -add_action( 'wp_default_scripts', 'gutenberg_register_vendor_scripts_62' ); - -/** - * This function takes care of adding inline styles - * in the proper place, depending on the theme in use. - * - * This method was added to core in 5.9.1, but with a single param ($style). The second param ($priority) was - * added post 6.0, so the 6.1 release needs to have wp_enqueue_block_support_styles updated to include this param. - * - * For block themes, it's loaded in the head. - * For classic ones, it's loaded in the body - * because the wp_head action happens before - * the render_block. - * - * @link https://core.trac.wordpress.org/ticket/53494. - * - * @deprecated 6.2 Block supports styles are now stored for enqueuing via the style engine API. See: packages/style-engine/README.md. - * - * @param string $style String containing the CSS styles to be added. - * @param int $priority To set the priority for the add_action. - */ -function gutenberg_enqueue_block_support_styles( $style, $priority = 10 ) { - _deprecated_function( __FUNCTION__, '6.2' ); - - $action_hook_name = 'wp_footer'; - if ( wp_is_block_theme() ) { - $action_hook_name = 'wp_head'; - } - add_action( - $action_hook_name, - static function () use ( $style ) { - echo "\n"; - }, - $priority - ); -} - -add_filter( - 'block_editor_settings_all', - static function( $settings ) { - // We must override what core is passing now. - $settings['__unstableIsBlockBasedTheme'] = wp_is_block_theme(); - return $settings; - }, - 100 -); diff --git a/lib/compat/wordpress-6.2/site-editor.php b/lib/compat/wordpress-6.2/site-editor.php deleted file mode 100644 index b6246e49c6d11..0000000000000 --- a/lib/compat/wordpress-6.2/site-editor.php +++ /dev/null @@ -1,24 +0,0 @@ -post ) ) { - return $settings; - } - - unset( $settings['__unstableHomeTemplate'] ); - - return $settings; -} -add_filter( 'block_editor_settings_all', 'gutenberg_site_editor_unset_homepage_setting', 10, 2 ); diff --git a/lib/compat/wordpress-6.2/theme.php b/lib/compat/wordpress-6.2/theme.php deleted file mode 100644 index 3e7b31109745a..0000000000000 --- a/lib/compat/wordpress-6.2/theme.php +++ /dev/null @@ -1,23 +0,0 @@ -is_block_theme() ) { - set_theme_mod( 'wp_classic_sidebars', $wp_registered_sidebars ); - } -} -add_action( 'switch_theme', 'gutenberg_set_classic_sidebars', 10, 2 ); diff --git a/lib/compat/wordpress-6.2/widgets.php b/lib/compat/wordpress-6.2/widgets.php deleted file mode 100644 index ce37f1bd9a34d..0000000000000 --- a/lib/compat/wordpress-6.2/widgets.php +++ /dev/null @@ -1,35 +0,0 @@ -is_registered( $pattern_name ) || $registry->is_registered( "core/$pattern_name" ); @@ -154,7 +154,7 @@ function gutenberg_register_remote_theme_patterns() { $patterns_registry = WP_Block_Patterns_Registry::get_instance(); foreach ( $patterns as $pattern ) { $pattern['source'] = 'pattern-directory/theme'; // Added in 6.3.0. - $normalized_pattern = gutenberg_normalize_remote_pattern( $pattern ); + $normalized_pattern = wp_normalize_remote_block_pattern( $pattern ); $pattern_name = sanitize_title( $normalized_pattern['title'] ); // Some patterns might be already registered as core patterns with the `core` prefix. $is_registered = $patterns_registry->is_registered( $pattern_name ) || $patterns_registry->is_registered( "core/$pattern_name" ); From ce5136ab3af41345aa3dc8ff1095bca9c6499785 Mon Sep 17 00:00:00 2001 From: ramon Date: Thu, 10 Aug 2023 11:36:40 +1000 Subject: [PATCH 4/7] - Using get_template_hierarchy (since 6.2) - Removed tests already covered in Core --- phpunit/block-template-utils-test.php | 62 ++-- ...rest-pattern-directory-controller-test.php | 289 ------------------ 2 files changed, 31 insertions(+), 320 deletions(-) delete mode 100644 phpunit/class-wp-rest-pattern-directory-controller-test.php diff --git a/phpunit/block-template-utils-test.php b/phpunit/block-template-utils-test.php index 08fb0c2c2c1e3..4b10e33deee93 100644 --- a/phpunit/block-template-utils-test.php +++ b/phpunit/block-template-utils-test.php @@ -26,23 +26,23 @@ public function tear_down() { } public function test_get_template_hierarchy() { - $hierarchy = gutenberg_get_template_hierarchy( 'front-page' ); + $hierarchy = get_template_hierarchy( 'front-page' ); $this->assertEquals( array( 'front-page', 'home', 'index' ), $hierarchy ); // Custom templates. - $hierarchy = gutenberg_get_template_hierarchy( 'whatever-slug', true ); + $hierarchy = get_template_hierarchy( 'whatever-slug', true ); $this->assertEquals( array( 'page', 'singular', 'index' ), $hierarchy ); // Single slug templates(ex. page, tag, author, etc.. - $hierarchy = gutenberg_get_template_hierarchy( 'page' ); + $hierarchy = get_template_hierarchy( 'page' ); $this->assertEquals( array( 'page', 'singular', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'tag' ); + $hierarchy = get_template_hierarchy( 'tag' ); $this->assertEquals( array( 'tag', 'archive', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'author' ); + $hierarchy = get_template_hierarchy( 'author' ); $this->assertEquals( array( 'author', 'archive', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'date' ); + $hierarchy = get_template_hierarchy( 'date' ); $this->assertEquals( array( 'date', 'archive', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'taxonomy' ); + $hierarchy = get_template_hierarchy( 'taxonomy' ); $this->assertEquals( array( 'taxonomy', 'archive', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'attachment' ); + $hierarchy = get_template_hierarchy( 'attachment' ); $this->assertEquals( array( 'attachment', @@ -52,23 +52,23 @@ public function test_get_template_hierarchy() { ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'singular' ); + $hierarchy = get_template_hierarchy( 'singular' ); $this->assertEquals( array( 'singular', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'single' ); + $hierarchy = get_template_hierarchy( 'single' ); $this->assertEquals( array( 'single', 'singular', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'archive' ); + $hierarchy = get_template_hierarchy( 'archive' ); $this->assertEquals( array( 'archive', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'index' ); + $hierarchy = get_template_hierarchy( 'index' ); $this->assertEquals( array( 'index' ), $hierarchy ); // Taxonomies. - $hierarchy = gutenberg_get_template_hierarchy( 'taxonomy-book_type', false ); + $hierarchy = get_template_hierarchy( 'taxonomy-book_type', false ); $this->assertEquals( array( 'taxonomy-book_type', 'taxonomy', 'archive', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'taxonomy-books', false, 'taxonomy-books' ); + $hierarchy = get_template_hierarchy( 'taxonomy-books', false, 'taxonomy-books' ); $this->assertEquals( array( 'taxonomy-books', 'taxonomy', 'archive', 'index' ), $hierarchy ); // Single word category. - $hierarchy = gutenberg_get_template_hierarchy( 'category-fruits', false ); + $hierarchy = get_template_hierarchy( 'category-fruits', false ); $this->assertEquals( array( 'category-fruits', @@ -79,7 +79,7 @@ public function test_get_template_hierarchy() { $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'category-fruits', false, 'category' ); + $hierarchy = get_template_hierarchy( 'category-fruits', false, 'category' ); $this->assertEquals( array( 'category-fruits', @@ -90,7 +90,7 @@ public function test_get_template_hierarchy() { $hierarchy ); // Multi word category. - $hierarchy = gutenberg_get_template_hierarchy( 'category-fruits-yellow', false ); + $hierarchy = get_template_hierarchy( 'category-fruits-yellow', false ); $this->assertEquals( array( 'category-fruits-yellow', @@ -101,7 +101,7 @@ public function test_get_template_hierarchy() { $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'category-fruits-yellow', false, 'category' ); + $hierarchy = get_template_hierarchy( 'category-fruits-yellow', false, 'category' ); $this->assertEquals( array( 'category-fruits-yellow', @@ -112,7 +112,7 @@ public function test_get_template_hierarchy() { $hierarchy ); // Single word taxonomy. - $hierarchy = gutenberg_get_template_hierarchy( 'taxonomy-books-action', false, 'taxonomy-books' ); + $hierarchy = get_template_hierarchy( 'taxonomy-books-action', false, 'taxonomy-books' ); $this->assertEquals( array( 'taxonomy-books-action', @@ -124,10 +124,10 @@ public function test_get_template_hierarchy() { $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'taxonomy-book_type-adventure', false ); + $hierarchy = get_template_hierarchy( 'taxonomy-book_type-adventure', false ); $this->assertEquals( array( 'taxonomy-book_type-adventure', 'taxonomy-book_type', 'taxonomy', 'archive', 'index' ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'taxonomy-books-action-adventure', false, 'taxonomy-books' ); + $hierarchy = get_template_hierarchy( 'taxonomy-books-action-adventure', false, 'taxonomy-books' ); $this->assertEquals( array( 'taxonomy-books-action-adventure', @@ -139,7 +139,7 @@ public function test_get_template_hierarchy() { $hierarchy ); // Multi word taxonomy/terms. - $hierarchy = gutenberg_get_template_hierarchy( 'taxonomy-greek-books-action-adventure', false, 'taxonomy-greek-books' ); + $hierarchy = get_template_hierarchy( 'taxonomy-greek-books-action-adventure', false, 'taxonomy-greek-books' ); $this->assertEquals( array( 'taxonomy-greek-books-action-adventure', @@ -151,7 +151,7 @@ public function test_get_template_hierarchy() { $hierarchy ); // Post types. - $hierarchy = gutenberg_get_template_hierarchy( 'single-book', false, 'single-book' ); + $hierarchy = get_template_hierarchy( 'single-book', false, 'single-book' ); $this->assertEquals( array( 'single-book', @@ -161,7 +161,7 @@ public function test_get_template_hierarchy() { ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'single-art-project', false, 'single-art-project' ); + $hierarchy = get_template_hierarchy( 'single-art-project', false, 'single-art-project' ); $this->assertEquals( array( 'single-art-project', @@ -171,7 +171,7 @@ public function test_get_template_hierarchy() { ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'single-art-project-imagine', false, 'single-art-project' ); + $hierarchy = get_template_hierarchy( 'single-art-project-imagine', false, 'single-art-project' ); $this->assertEquals( array( 'single-art-project-imagine', @@ -183,7 +183,7 @@ public function test_get_template_hierarchy() { $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'single-custom_book', false ); + $hierarchy = get_template_hierarchy( 'single-custom_book', false ); $this->assertEquals( array( 'single-custom_book', @@ -194,7 +194,7 @@ public function test_get_template_hierarchy() { $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'single-custom_book-book-1', false ); + $hierarchy = get_template_hierarchy( 'single-custom_book-book-1', false ); $this->assertEquals( array( 'single-custom_book-book-1', @@ -206,7 +206,7 @@ public function test_get_template_hierarchy() { $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'page-hi', false, 'page' ); + $hierarchy = get_template_hierarchy( 'page-hi', false, 'page' ); $this->assertEquals( array( 'page-hi', @@ -216,7 +216,7 @@ public function test_get_template_hierarchy() { ), $hierarchy ); - $hierarchy = gutenberg_get_template_hierarchy( 'page-hi', false ); + $hierarchy = get_template_hierarchy( 'page-hi', false ); $this->assertEquals( array( 'page-hi', @@ -227,7 +227,7 @@ public function test_get_template_hierarchy() { $hierarchy ); // Authors. - $hierarchy = gutenberg_get_template_hierarchy( 'author-rigas', false, 'author' ); + $hierarchy = get_template_hierarchy( 'author-rigas', false, 'author' ); $this->assertEquals( array( 'author-rigas', @@ -238,7 +238,7 @@ public function test_get_template_hierarchy() { $hierarchy ); // Archive post types. - $hierarchy = gutenberg_get_template_hierarchy( 'archive-book', false ); + $hierarchy = get_template_hierarchy( 'archive-book', false ); $this->assertEquals( array( 'archive-book', diff --git a/phpunit/class-wp-rest-pattern-directory-controller-test.php b/phpunit/class-wp-rest-pattern-directory-controller-test.php deleted file mode 100644 index fd8e5246116f9..0000000000000 --- a/phpunit/class-wp-rest-pattern-directory-controller-test.php +++ /dev/null @@ -1,289 +0,0 @@ -user->create( - array( - 'role' => 'contributor', - ) - ); - - self::$http_request_urls = array(); - - static::$controller = new Gutenberg_REST_Pattern_Directory_Controller_6_2(); - } - - public static function wpTearDownAfterClass() { - self::delete_user( self::$contributor_id ); - } - - /** - * Clear the captured request URLs after each test. - */ - public function tear_down() { - self::$http_request_urls = array(); - parent::tear_down(); - } - - /** - * @covers WP_REST_Pattern_Directory_Controller::register_routes - * - * @since 5.8.0 - * @since 6.2.0 Added pattern directory categories endpoint. - */ - public function test_register_routes() { - $routes = rest_get_server()->get_routes(); - - $this->assertArrayHasKey( '/wp/v2/pattern-directory/patterns', $routes ); - } - - /** - * Tests if the provided query args are passed through to the wp.org API. - * - * @dataProvider data_get_items_query_args - * - * @covers WP_REST_Pattern_Directory_Controller::get_items - * - * @since 6.2.0 - * - * @param string $param Query parameter name (ex, page). - * @param mixed $value Query value to test. - * @param bool $is_error Whether this value should error or not. - * @param mixed $expected Expected value (or expected error code). - */ - public function test_get_items_query_args( $param, $value, $is_error, $expected ) { - wp_set_current_user( self::$contributor_id ); - self::capture_http_urls(); - - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); - if ( $value ) { - $request->set_query_params( array( $param => $value ) ); - } - - $response = rest_do_request( $request ); - $data = $response->get_data(); - if ( $is_error ) { - $this->assertSame( $expected, $data['code'] ); - $this->assertStringContainsString( $param, $data['message'] ); - } else { - $this->assertCount( 1, self::$http_request_urls ); - $this->assertStringContainsString( $param . '=' . $expected, self::$http_request_urls[0] ); - } - } - - /** - * @since 6.2.0 - */ - public function data_get_items_query_args() { - return array( - 'per_page default' => array( 'per_page', false, false, 100 ), - 'per_page custom-1' => array( 'per_page', 5, false, 5 ), - 'per_page custom-2' => array( 'per_page', 50, false, 50 ), - 'per_page invalid-1' => array( 'per_page', 200, true, 'rest_invalid_param' ), - 'per_page invalid-2' => array( 'per_page', 'abc', true, 'rest_invalid_param' ), - - 'page default' => array( 'page', false, false, 1 ), - 'page custom' => array( 'page', 5, false, 5 ), - 'page invalid' => array( 'page', 'abc', true, 'rest_invalid_param' ), - - 'offset custom' => array( 'offset', 5, false, 5 ), - 'offset invalid-1' => array( 'offset', 'abc', true, 'rest_invalid_param' ), - - 'order default' => array( 'order', false, false, 'desc' ), - 'order custom' => array( 'order', 'asc', false, 'asc' ), - 'order invalid-1' => array( 'order', 10, true, 'rest_invalid_param' ), - 'order invalid-2' => array( 'order', 'fake', true, 'rest_invalid_param' ), - - 'orderby default' => array( 'orderby', false, false, 'date' ), - 'orderby custom-1' => array( 'orderby', 'title', false, 'title' ), - 'orderby custom-2' => array( 'orderby', 'date', false, 'date' ), - 'orderby custom-3' => array( 'orderby', 'favorite_count', false, 'favorite_count' ), - 'orderby invalid-1' => array( 'orderby', 10, true, 'rest_invalid_param' ), - 'orderby invalid-2' => array( 'orderby', 'fake', true, 'rest_invalid_param' ), - ); - } - - /** - * Attach a filter to capture requested wp.org URL. - * - * @since 6.2.0 - */ - private static function capture_http_urls() { - add_filter( - 'pre_http_request', - static function ( $preempt, $args, $url ) { - if ( 'api.wordpress.org' !== wp_parse_url( $url, PHP_URL_HOST ) ) { - return $preempt; - } - - self::$http_request_urls[] = $url; - - // Return a response to prevent external API request. - $response = array( - 'headers' => array(), - 'response' => array( - 'code' => 200, - 'message' => 'OK', - ), - 'body' => '[]', - 'cookies' => array(), - 'filename' => null, - ); - - return $response; - }, - 10, - 3 - ); - } - - /** - * @covers WP_REST_Pattern_Directory_Controller::prepare_item_for_response - * - * @since 5.8.0 - * @since 6.2.0 Added `block_types` property. - */ - public function test_prepare_item() { - $raw_patterns = json_decode( self::get_raw_response( 'browse-all' ) ); - $raw_patterns[0]->extra_field = 'this should be removed'; - - $prepared_pattern = static::$controller->prepare_response_for_collection( - static::$controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() ) - ); - - $this->assertPatternMatchesSchema( $prepared_pattern ); - $this->assertArrayNotHasKey( 'extra_field', $prepared_pattern ); - } - - /** - * Asserts that the pattern matches the expected response schema. - * - * @param WP_REST_Response[] $pattern An individual pattern from the REST API response. - */ - public function assertPatternMatchesSchema( $pattern ) { - $schema = static::$controller->get_item_schema(); - $pattern_id = isset( $pattern->id ) ? $pattern->id : '{pattern ID is missing}'; - - $this->assertTrue( - rest_validate_value_from_schema( $pattern, $schema ), - "Pattern ID `$pattern_id` doesn't match the response schema." - ); - - $this->assertSame( - array_keys( $schema['properties'] ), - array_keys( $pattern ), - "Pattern ID `$pattern_id` doesn't contain all of the fields expected from the schema." - ); - } - - /** - * Get a mocked raw response from api.wordpress.org. - * - * @return string - */ - private static function get_raw_response( $action ) { - $fixtures_dir = __DIR__ . '/fixtures/pattern-directory'; - - switch ( $action ) { - default: - case 'browse-all': - // Response from https://api.wordpress.org/patterns/1.0/. - $response = file_get_contents( $fixtures_dir . '/browse-all.json' ); - break; - } - - return $response; - } - - /** - * @doesNotPerformAssertions - */ - public function test_context_param() { - // Covered by the core test. - } - - /** - * @doesNotPerformAssertions - */ - public function test_get_items() { - // Covered by the core test. - } - - /** - * @doesNotPerformAssertions - */ - public function test_get_item() { - // Controller does not implement get_item(). - } - - /** - * @doesNotPerformAssertions - */ - public function test_create_item() { - // Controller does not implement create_item(). - } - - /** - * @doesNotPerformAssertions - */ - public function test_update_item() { - // Controller does not implement update_item(). - } - - /** - * @doesNotPerformAssertions - */ - public function test_delete_item() { - // Controller does not implement delete_item(). - } - - /** - * @doesNotPerformAssertions - */ - public function test_get_item_schema() { - // The controller's schema is hardcoded, so tests would not be meaningful. - } -} From 6c9011f03267f6fb37031d86afa2678f9add74ea Mon Sep 17 00:00:00 2001 From: ramon Date: Mon, 14 Aug 2023 11:01:24 +1000 Subject: [PATCH 5/7] Moving get_items() method from Gutenberg_REST_Block_Patterns_Controller_6_2 to Gutenberg_REST_Block_Patterns_Controller_6_3 because the following methods were updated in 6.3: - gutenberg_load_remote_block_patterns - gutenberg_load_remote_featured_patterns - gutenberg_register_remote_theme_patterns --- ...erg-rest-block-patterns-controller-6-3.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php index a64724ff858c2..601c6239f7071 100644 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php @@ -158,4 +158,33 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } + + /** + * Retrieves all block patterns. + * + * @since 6.0.0 + * @since 6.2.0 Added migration for old core pattern categories to the new ones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + if ( ! $this->remote_patterns_loaded ) { + // Load block patterns from w.org. + gutenberg_load_remote_block_patterns(); // Patterns with the `core` keyword. + gutenberg_load_remote_featured_patterns(); // Patterns in the `featured` category. + gutenberg_register_remote_theme_patterns(); // Patterns requested by current theme. + + $this->remote_patterns_loaded = true; + } + + $response = array(); + $patterns = WP_Block_Patterns_Registry::get_instance()->get_all_registered(); + foreach ( $patterns as $pattern ) { + $migrated_pattern = $this->migrate_pattern_categories( $pattern ); + $prepared_pattern = $this->prepare_item_for_response( $migrated_pattern, $request ); + $response[] = $this->prepare_response_for_collection( $prepared_pattern ); + } + return rest_ensure_response( $response ); + } } From 9479a5387d30957c7af6ff6de22d3d37e4dcc95c Mon Sep 17 00:00:00 2001 From: ramon Date: Mon, 14 Aug 2023 11:28:41 +1000 Subject: [PATCH 6/7] Referencing private property --- ...class-gutenberg-rest-block-patterns-controller-6-3.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php index 601c6239f7071..0a5b026cded3b 100644 --- a/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php +++ b/lib/compat/wordpress-6.3/class-gutenberg-rest-block-patterns-controller-6-3.php @@ -14,6 +14,14 @@ * @see WP_REST_Controller */ class Gutenberg_REST_Block_Patterns_Controller_6_3 extends WP_REST_Block_Patterns_Controller { + /** + * Defines whether remote patterns should be loaded. + * + * @since 6.0.0 + * @var bool + */ + private $remote_patterns_loaded; + /** * Prepare a raw block pattern before it gets output in a REST API response. * From efadeeaab5a29be2c4215525c6914c96c4c884fb Mon Sep 17 00:00:00 2001 From: ramon Date: Tue, 15 Aug 2023 11:48:44 +1000 Subject: [PATCH 7/7] Delete lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php because it wanted to live after a less than ideal rebase --- .../class-gutenberg-rest-global-styles-controller-6-3.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php diff --git a/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php b/lib/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php deleted file mode 100644 index e69de29bb2d1d..0000000000000