ion that enqueues the CSS Custom Properties coming from theme.json. * * @since 5.9.0 */ function wp_enqueue_global_styles_css_custom_properties() { wp_register_style( 'global-styles-css-custom-properties', false ); wp_add_inline_style( 'global-styles-css-custom-properties', wp_get_global_stylesheet( array( 'variables' ) ) ); wp_enqueue_style( 'global-styles-css-custom-properties' ); } /** * Hooks inline styles in the proper place, depending on the active theme. * * @since 5.9.1 * @since 6.1.0 Added the `$priority` parameter. * * For block themes, styles are loaded in the head. * For classic ones, styles are loaded in the body because the wp_head action happens before render_block. * * @link https://core.trac.wordpress.org/ticket/53494. * * @param string $style String containing the CSS styles to be added. * @param int $priority To set the priority for the add_action. */ function wp_enqueue_block_support_styles( $style, $priority = 10 ) { $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 ); } /** * Fetches, processes and compiles stored core styles, then combines and renders them to the page. * Styles are stored via the style engine API. * * @link https://developer.wordpress.org/block-editor/reference-guides/packages/packages-style-engine/ * * @since 6.1.0 * * @param array $options { * Optional. An array of options to pass to wp_style_engine_get_stylesheet_from_context(). * Default empty array. * * @type bool $optimize Whether to optimize the CSS output, e.g., combine rules. * Default false. * @type bool $prettify Whether to add new lines and indents to output. * Default to whether the `SCRIPT_DEBUG` constant is defined. * } */ function wp_enqueue_stored_styles( $options = array() ) { $is_block_theme = wp_is_block_theme(); $is_classic_theme = ! $is_block_theme; /* * For block themes, this function prints stored styles in the header. * For classic themes, in the footer. */ if ( ( $is_block_theme && doing_action( 'wp_footer' ) ) || ( $is_classic_theme && doing_action( 'wp_enqueue_scripts' ) ) ) { return; } $core_styles_keys = array( 'block-supports' ); $compiled_core_stylesheet = ''; $style_tag_id = 'core'; // Adds comment if code is prettified to identify core styles sections in debugging. $should_prettify = isset( $options['prettify'] ) ? true === $options['prettify'] : defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG; foreach ( $core_styles_keys as $style_key ) { if ( $should_prettify ) { $compiled_core_stylesheet .= "/**\n * Core styles: $style_key\n */\n"; } // Chains core store ids to signify what the styles contain. $style_tag_id .= '-' . $style_key; $compiled_core_stylesheet .= wp_style_engine_get_stylesheet_from_context( $style_key, $options ); } // Combines Core styles. if ( ! empty( $compiled_core_stylesheet ) ) { wp_register_style( $style_tag_id, false ); wp_add_inline_style( $style_tag_id, $compiled_core_stylesheet ); wp_enqueue_style( $style_tag_id ); } // Prints out any other stores registered by themes or otherwise. $additional_stores = WP_Style_Engine_CSS_Rules_Store::get_stores(); foreach ( array_keys( $additional_stores ) as $store_name ) { if ( in_array( $store_name, $core_styles_keys, true ) ) { continue; } $styles = wp_style_engine_get_stylesheet_from_context( $store_name, $options ); if ( ! empty( $styles ) ) { $key = "wp-style-engine-$store_name"; wp_register_style( $key, false ); wp_add_inline_style( $key, $styles ); wp_enqueue_style( $key ); } } } /** * Enqueues a stylesheet for a specific block. * * If the theme has opted-in to separate-styles loading, * then the stylesheet will be enqueued on-render, * otherwise when the block inits. * * @since 5.9.0 * * @param string $block_name The block-name, including namespace. * @param array $args { * An array of arguments. See wp_register_style() for full information about each argument. * * @type string $handle The handle for the stylesheet. * @type string|false $src The source URL of the stylesheet. * @type string[] $deps Array of registered stylesheet handles this stylesheet depends on. * @type string|bool|null $ver Stylesheet version number. * @type string $media The media for which this stylesheet has been defined. * @type string|null $path Absolute path to the stylesheet, so that it can potentially be inlined. * } */ function wp_enqueue_block_style( $block_name, $args ) { $args = wp_parse_args( $args, array( 'handle' => '', 'src' => '', 'deps' => array(), 'ver' => false, 'media' => 'all', ) ); /** * Callback function to register and enqueue styles. * * @param string $content When the callback is used for the render_block filter, * the content needs to be returned so the function parameter * is to ensure the content exists. * @return string Block content. */ $callback = static function ( $content ) use ( $args ) { // Register the stylesheet. if ( ! empty( $args['src'] ) ) { wp_register_style( $args['handle'], $args['src'], $args['deps'], $args['ver'], $args['media'] ); } // Add `path` data if provided. if ( isset( $args['path'] ) ) { wp_style_add_data( $args['handle'], 'path', $args['path'] ); // Get the RTL file path. $rtl_file_path = str_replace( '.css', '-rtl.css', $args['path'] ); // Add RTL stylesheet. if ( file_exists( $rtl_file_path ) ) { wp_style_add_data( $args['handle'], 'rtl', 'replace' ); if ( is_rtl() ) { wp_style_add_data( $args['handle'], 'path', $rtl_file_path ); } } } // Enqueue the stylesheet. wp_enqueue_style( $args['handle'] ); return $content; }; $hook = did_action( 'wp_enqueue_scripts' ) ? 'wp_footer' : 'wp_enqueue_scripts'; if ( wp_should_load_separate_core_block_assets() ) { /** * Callback function to register and enqueue styles. * * @param string $content The block content. * @param array $block The full block, including name and attributes. * @return string Block content. */ $callback_separate = static function ( $content, $block ) use ( $block_name, $callback ) { if ( ! empty( $block['blockName'] ) && $block_name === $block['blockName'] ) { return $callback( $content ); } return $content; }; /* * The filter's callback here is an anonymous function because * using a named function in this case is not possible. * * The function cannot be unhooked, however, users are still able * to dequeue the stylesheets registered/enqueued by the callback * which is why in this case, using an anonymous function * was deemed acceptable. */ add_filter( 'render_block', $callback_separate, 10, 2 ); return; } /* * The filter's callback here is an anonymous function because * using a named function in this case is not possible. * * The function cannot be unhooked, however, users are still able * to dequeue the stylesheets registered/enqueued by the callback * which is why in this case, using an anonymous function * was deemed acceptable. */ add_filter( $hook, $callback ); // Enqueue assets in the editor. add_action( 'enqueue_block_assets', $callback ); } /** * Loads classic theme styles on classic themes in the frontend. * * This is needed for backwards compatibility for button blocks specifically. * * @since 6.1.0 */ function wp_enqueue_classic_theme_styles() { if ( ! wp_theme_has_theme_json() ) { $suffix = wp_scripts_get_suffix(); wp_register_style( 'classic-theme-styles', '/' . WPINC . "/css/classic-themes$suffix.css" ); wp_style_add_data( 'classic-theme-styles', 'path', ABSPATH . WPINC . "/css/classic-themes$suffix.css" ); wp_enqueue_style( 'classic-theme-styles' ); } } /** * Loads classic theme styles on classic themes in the editor. * * This is needed for backwards compatibility for button blocks specifically. * * @since 6.1.0 * * @param array $editor_settings The array of editor settings. * @return array A filtered array of editor settings. */ function wp_add_editor_classic_theme_styles( $editor_settings ) { if ( wp_theme_has_theme_json() ) { return $editor_settings; } $suffix = wp_scripts_get_suffix(); $classic_theme_styles = ABSPATH . WPINC . "/css/classic-themes$suffix.css"; /* * This follows the pattern of get_block_editor_theme_styles, * but we can't use get_block_editor_theme_styles directly as it * only handles external files or theme files. */ $classic_theme_styles_settings = array( 'css' => file_get_contents( $classic_theme_styles ), '__unstableType' => 'core', 'isGlobalStyles' => false, ); // Add these settings to the start of the array so that themes can override them. array_unshift( $editor_settings['styles'], $classic_theme_styles_settings ); return $editor_settings; } /** * Removes leading and trailing _empty_ script tags. * * This is a helper meant to be used for literal script tag construction * within `wp_get_inline_script_tag()` or `wp_print_inline_script_tag()`. * It removes the literal values of "" from * around an inline script after trimming whitespace. Typlically this * is used in conjunction with output buffering, where `ob_get_clean()` * is passed as the `$contents` argument. * * Example: * * // Strips exact literal empty SCRIPT tags. * $js = '; * 'sayHello();' === wp_remove_surrounding_empty_script_tags( $js ); * * // Otherwise if anything is different it warns in the JS console. * $js = ''; * 'console.error( ... )' === wp_remove_surrounding_empty_script_tags( $js ); * * @private * @since 6.4.0 * * @see wp_print_inline_script_tag() * @see wp_get_inline_script_tag() * * @param string $contents Script body with manually created SCRIPT tag literals. * @return string Script body without surrounding script tag literals, or * original contents if both exact literals aren't present. */ function wp_remove_surrounding_empty_script_tags( $contents ) { $contents = trim( $contents ); $opener = ''; if ( strlen( $contents ) > strlen( $opener ) + strlen( $closer ) && strtoupper( substr( $contents, 0, strlen( $opener ) ) ) === $opener && strtoupper( substr( $contents, -strlen( $closer ) ) ) === $closer ) { return substr( $contents, strlen( $opener ), -strlen( $closer ) ); } else { $error_message = __( 'Expected string to start with script tag (without attributes) and end with script tag, with optional whitespace.' ); _doing_it_wrong( __FUNCTION__, $error_message, '6.4' ); return sprintf( 'console.error(%s)', wp_json_encode( __( 'Function wp_remove_surrounding_empty_script_tags() used incorrectly in PHP.' ) . ' ' . $error_message ) ); } } se; } $enqueued_text = $this->lexical_updates[ $comparable_name ]->text; // Removed attributes erase the entire span. if ( '' === $enqueued_text ) { return null; } /* * Boolean attribute updates are just the attribute name without a corresponding value. * * This value might differ from the given comparable name in that there could be leading * or trailing whitespace, and that the casing follows the name given in `set_attribute`. * * Example: * * $p->set_attribute( 'data-TEST-id', 'update' ); * 'update' === $p->get_enqueued_attribute_value( 'data-test-id' ); * * Detect this difference based on the absence of the `=`, which _must_ exist in any * attribute containing a value, e.g. ``. * ¹ ² * 1. Attribute with a string value. * 2. Boolean attribute whose value is `true`. */ $equals_at = strpos( $enqueued_text, '=' ); if ( false === $equals_at ) { return true; } /* * Finally, a normal update's value will appear after the `=` and * be double-quoted, as performed incidentally by `set_attribute`. * * e.g. `type="text"` * ¹² ³ * 1. Equals is here. * 2. Double-quoting starts one after the equals sign. * 3. Double-quoting ends at the last character in the update. */ $enqueued_value = substr( $enqueued_text, $equals_at + 2, -1 ); return html_entity_decode( $enqueued_value ); } /** * Returns the value of a requested attribute from a matched tag opener if that attribute exists. * * Example: * * $p = new WP_HTML_Tag_Processor( '
Test
' ); * $p->next_tag( array( 'class_name' => 'test' ) ) === true; * $p->get_attribute( 'data-test-id' ) === '14'; * $p->get_attribute( 'enabled' ) === true; * $p->get_attribute( 'aria-label' ) === null; * * $p->next_tag() === false; * $p->get_attribute( 'class' ) === null; * * @since 6.2.0 * * @param string $name Name of attribute whose value is requested. * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`. */ public function get_attribute( $name ) { if ( null === $this->tag_name_starts_at ) { return null; } $comparable = strtolower( $name ); /* * For every attribute other than `class` it's possible to perform a quick check if * there's an enqueued lexical update whose value takes priority over what's found in * the input document. * * The `class` attribute is special though because of the exposed helpers `add_class` * and `remove_class`. These form a builder for the `class` attribute, so an additional * check for enqueued class changes is required in addition to the check for any enqueued * attribute values. If any exist, those enqueued class changes must first be flushed out * into an attribute value update. */ if ( 'class' === $name ) { $this->class_name_updates_to_attributes_updates(); } // Return any enqueued attribute value updates if they exist. $enqueued_value = $this->get_enqueued_attribute_value( $comparable ); if ( false !== $enqueued_value ) { return $enqueued_value; } if ( ! isset( $this->attributes[ $comparable ] ) ) { return null; } $attribute = $this->attributes[ $comparable ]; /* * This flag distinguishes an attribute with no value * from an attribute with an empty string value. For * unquoted attributes this could look very similar. * It refers to whether an `=` follows the name. * * e.g.
* ¹ ² * 1. Attribute `boolean-attribute` is `true`. * 2. Attribute `empty-attribute` is `""`. */ if ( true === $attribute->is_true ) { return true; } $raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length ); return html_entity_decode( $raw_value ); } /** * Gets lowercase names of all attributes matching a given prefix in the current tag. * * Note that matching is case-insensitive. This is in accordance with the spec: * * > There must never be two or more attributes on * > the same start tag whose names are an ASCII * > case-insensitive match for each other. * - HTML 5 spec * * Example: * * $p = new WP_HTML_Tag_Processor( '
Test
' ); * $p->next_tag( array( 'class_name' => 'test' ) ) === true; * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); * * $p->next_tag() === false; * $p->get_attribute_names_with_prefix( 'data-' ) === null; * * @since 6.2.0 * * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive * * @param string $prefix Prefix of requested attribute names. * @return array|null List of attribute names, or `null` when no tag opener is matched. */ public function get_attribute_names_with_prefix( $prefix ) { if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { return null; } $comparable = strtolower( $prefix ); $matches = array(); foreach ( array_keys( $this->attributes ) as $attr_name ) { if ( str_starts_with( $attr_name, $comparable ) ) { $matches[] = $attr_name; } } return $matches; } /** * Returns the uppercase name of the matched tag. * * Example: * * $p = new WP_HTML_Tag_Processor( '
Test
' ); * $p->next_tag() === true; * $p->get_tag() === 'DIV'; * * $p->next_tag() === false; * $p->get_tag() === null; * * @since 6.2.0 * * @return string|null Name of currently matched tag in input HTML, or `null` if none found. */ public function get_tag() { if ( null === $this->tag_name_starts_at ) { return null; } $tag_name = substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length ); return strtoupper( $tag_name ); } /** * Indicates if the currently matched tag contains the self-closing flag. * * No HTML elements ought to have the self-closing flag and for those, the self-closing * flag will be ignored. For void elements this is benign because they "self close" * automatically. For non-void HTML elements though problems will appear if someone * intends to use a self-closing element in place of that element with an empty body. * For HTML foreign elements and custom elements the self-closing flag determines if * they self-close or not. * * This function does not determine if a tag is self-closing, * but only if the self-closing flag is present in the syntax. * * @since 6.3.0 * * @return bool Whether the currently matched tag contains the self-closing flag. */ public function has_self_closing_flag() { if ( ! $this->tag_name_starts_at ) { return false; } return '/' === $this->html[ $this->tag_ends_at - 1 ]; } /** * Indicates if the current tag token is a tag closer. * * Example: * * $p = new WP_HTML_Tag_Processor( '
' ); * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); * $p->is_tag_closer() === false; * * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); * $p->is_tag_closer() === true; * * @since 6.2.0 * * @return bool Whether the current tag is a tag closer. */ public function is_tag_closer() { return $this->is_closing_tag; } /** * Updates or creates a new attribute on the currently matched tag with the passed value. * * For boolean attributes special handling is provided: * - When `true` is passed as the value, then only the attribute name is added to the tag. * - When `false` is passed, the attribute gets removed if it existed before. * * For string attributes, the value is escaped using the `esc_attr` function. * * @since 6.2.0 * @since 6.2.1 Fix: Only create a single update for multiple calls with case-variant attribute names. * * @param string $name The attribute name to target. * @param string|bool $value The new attribute value. * @return bool Whether an attribute value was set. */ public function set_attribute( $name, $value ) { if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { return false; } /* * WordPress rejects more characters than are strictly forbidden * in HTML5. This is to prevent additional security risks deeper * in the WordPress and plugin stack. Specifically the * less-than (<) greater-than (>) and ampersand (&) aren't allowed. * * The use of a PCRE match enables looking for specific Unicode * code points without writing a UTF-8 decoder. Whereas scanning * for one-byte characters is trivial (with `strcspn`), scanning * for the longer byte sequences would be more complicated. Given * that this shouldn't be in the hot path for execution, it's a * reasonable compromise in efficiency without introducing a * noticeable impact on the overall system. * * @see https://html.spec.whatwg.org/#attributes-2 * * @TODO as the only regex pattern maybe we should take it out? are * Unicode patterns available broadly in Core? */ if ( preg_match( '~[' . // Syntax-like characters. '"\'>& 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, '' ); // Removes any duplicated attributes if they were also present. if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) { foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) { $this->lexical_updates[] = new WP_HTML_Text_Replacement( $attribute_token->start, $attribute_token->end, '' ); } } return true; } /** * Adds a new class name to the currently matched tag. * * @since 6.2.0 * * @param string $class_name The class name to add. * @return bool Whether the class was set to be added. */ public function add_class( $class_name ) { if ( $this->is_closing_tag ) { return false; } if ( null !== $this->tag_name_starts_at ) { $this->classname_updates[ $class_name ] = self::ADD_CLASS; } return true; } /** * Removes a class name from the currently matched tag. * * @since 6.2.0 * * @param string $class_name The class name to remove. * @return bool Whether the class was set to be removed. */ public function remove_class( $class_name ) { if ( $this->is_closing_tag ) { return false; } if ( null !== $this->tag_name_starts_at ) { $this->classname_updates[ $class_name ] = self::REMOVE_CLASS; } return true; } /** * Returns the string representation of the HTML Tag Processor. * * @since 6.2.0 * * @see WP_HTML_Tag_Processor::get_updated_html() * * @return string The processed HTML. */ public function __toString() { return $this->get_updated_html(); } /** * Returns the string representation of the HTML Tag Processor. * * @since 6.2.0 * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates. * @since 6.4.0 No longer calls subclass method `next_tag()` after updating HTML. * * @return string The processed HTML. */ public function get_updated_html() { $requires_no_updating = 0 === count( $this->classname_updates ) && 0 === count( $this->lexical_updates ); /* * When there is nothing more to update and nothing has already been * updated, return the original document and avoid a string copy. */ if ( $requires_no_updating ) { return $this->html; } /* * Keep track of the position right before the current tag. This will * be necessary for reparsing the current tag after updating the HTML. */ $before_current_tag = $this->tag_name_starts_at - 1; /* * 1. Apply the enqueued edits and update all the pointers to reflect those changes. */ $this->class_name_updates_to_attributes_updates(); $before_current_tag += $this->apply_attributes_updates( $before_current_tag ); /* * 2. Rewind to before the current tag and reparse to get updated attributes. * * At this point the internal cursor points to the end of the tag name. * Rewind before the tag name starts so that it's as if the cursor didn't * move; a call to `next_tag()` will reparse the recently-updated attributes * and additional calls to modify the attributes will apply at this same * location, but in order to avoid issues with subclasses that might add * behaviors to `next_tag()`, the internal methods should be called here * instead. * * It's important to note that in this specific place there will be no change * because the processor was already at a tag when this was called and it's * rewinding only to the beginning of this very tag before reprocessing it * and its attributes. * *

Previous HTMLMore HTML

* ↑ │ back up by the length of the tag name plus the opening < * └←─┘ back up by strlen("em") + 1 ==> 3 */ $this->bytes_already_parsed = $before_current_tag; $this->parse_next_tag(); // Reparse the attributes. while ( $this->parse_next_attribute() ) { continue; } $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); $this->tag_ends_at = $tag_ends_at; $this->bytes_already_parsed = $tag_ends_at; return $this->html; } /** * Parses tag query input into internal search criteria. * * @since 6.2.0 * * @param array|string|null $query { * Optional. Which tag name to find, having which class, etc. Default is to find any tag. * * @type string|null $tag_name Which tag to find, or `null` for "any tag." * @type int|null $match_offset Find the Nth tag matching all search criteria. * 1 for "first" tag, 3 for "third," etc. * Defaults to first tag. * @type string|null $class_name Tag must contain this class name to match. * @type string $tag_closers "visit" or "skip": whether to stop on tag closers, e.g.
. * } */ private function parse_query( $query ) { if ( null !== $query && $query === $this->last_query ) { return; } $this->last_query = $query; $this->sought_tag_name = null; $this->sought_class_name = null; $this->sought_match_offset = 1; $this->stop_on_tag_closers = false; // A single string value means "find the tag of this name". if ( is_string( $query ) ) { $this->sought_tag_name = $query; return; } // An empty query parameter applies no restrictions on the search. if ( null === $query ) { return; } // If not using the string interface, an associative array is required. if ( ! is_array( $query ) ) { _doing_it_wrong( __METHOD__, __( 'The query argument must be an array or a tag name.' ), '6.2.0' ); return; } if ( isset( $query['tag_name'] ) && is_string( $query['tag_name'] ) ) { $this->sought_tag_name = $query['tag_name']; } if ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) ) { $this->sought_class_name = $query['class_name']; } if ( isset( $query['match_offset'] ) && is_int( $query['match_offset'] ) && 0 < $query['match_offset'] ) { $this->sought_match_offset = $query['match_offset']; } if ( isset( $query['tag_closers'] ) ) { $this->stop_on_tag_closers = 'visit' === $query['tag_closers']; } } /** * Checks whether a given tag and its attributes match the search criteria. * * @since 6.2.0 * * @return bool Whether the given tag and its attribute match the search criteria. */ private function matches() { if ( $this->is_closing_tag && ! $this->stop_on_tag_closers ) { return false; } // Does the tag name match the requested tag name in a case-insensitive manner? if ( null !== $this->sought_tag_name ) { /* * String (byte) length lookup is fast. If they aren't the * same length then they can't be the same string values. */ if ( strlen( $this->sought_tag_name ) !== $this->tag_name_length ) { return false; } /* * Check each character to determine if they are the same. * Defer calls to `strtoupper()` to avoid them when possible. * Calling `strcasecmp()` here tested slowed than comparing each * character, so unless benchmarks show otherwise, it should * not be used. * * It's expected that most of the time that this runs, a * lower-case tag name will be supplied and the input will * contain lower-case tag names, thus normally bypassing * the case comparison code. */ for ( $i = 0; $i < $this->tag_name_length; $i++ ) { $html_char = $this->html[ $this->tag_name_starts_at + $i ]; $tag_char = $this->sought_tag_name[ $i ]; if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { return false; } } } if ( null !== $this->sought_class_name && ! $this->has_class( $this->sought_class_name ) ) { return false; } return true; } }