Merged in feature/81-dev-dev01 (pull request #5)

auto-patch  81-dev-dev01-2023-12-05T22_45_26

* auto-patch  81-dev-dev01-2023-12-05T22_45_26
This commit is contained in:
Tony Volpe
2023-12-05 23:05:59 +00:00
parent ba16964e7a
commit 725d3043d5
1463 changed files with 142461 additions and 89421 deletions

View File

@@ -0,0 +1,187 @@
<?php
/**
* HTML API: WP_HTML_Active_Formatting_Elements class
*
* @package WordPress
* @subpackage HTML-API
* @since 6.4.0
*/
/**
* Core class used by the HTML processor during HTML parsing
* for managing the stack of active formatting elements.
*
* This class is designed for internal use by the HTML processor.
*
* > Initially, the list of active formatting elements is empty.
* > It is used to handle mis-nested formatting element tags.
* >
* > The list contains elements in the formatting category, and markers.
* > The markers are inserted when entering applet, object, marquee,
* > template, td, th, and caption elements, and are used to prevent
* > formatting from "leaking" into applet, object, marquee, template,
* > td, th, and caption elements.
* >
* > In addition, each element in the list of active formatting elements
* > is associated with the token for which it was created, so that
* > further elements can be created for that token if necessary.
*
* @since 6.4.0
*
* @access private
*
* @see https://html.spec.whatwg.org/#list-of-active-formatting-elements
* @see WP_HTML_Processor
*/
class WP_HTML_Active_Formatting_Elements {
/**
* Holds the stack of active formatting element references.
*
* @since 6.4.0
*
* @var WP_HTML_Token[]
*/
private $stack = array();
/**
* Reports if a specific node is in the stack of active formatting elements.
*
* @since 6.4.0
*
* @param WP_HTML_Token $token Look for this node in the stack.
* @return bool Whether the referenced node is in the stack of active formatting elements.
*/
public function contains_node( $token ) {
foreach ( $this->walk_up() as $item ) {
if ( $token->bookmark_name === $item->bookmark_name ) {
return true;
}
}
return false;
}
/**
* Returns how many nodes are currently in the stack of active formatting elements.
*
* @since 6.4.0
*
* @return int How many node are in the stack of active formatting elements.
*/
public function count() {
return count( $this->stack );
}
/**
* Returns the node at the end of the stack of active formatting elements,
* if one exists. If the stack is empty, returns null.
*
* @since 6.4.0
*
* @return WP_HTML_Token|null Last node in the stack of active formatting elements, if one exists, otherwise null.
*/
public function current_node() {
$current_node = end( $this->stack );
return $current_node ? $current_node : null;
}
/**
* Pushes a node onto the stack of active formatting elements.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#push-onto-the-list-of-active-formatting-elements
*
* @param WP_HTML_Token $token Push this node onto the stack.
*/
public function push( $token ) {
/*
* > If there are already three elements in the list of active formatting elements after the last marker,
* > if any, or anywhere in the list if there are no markers, that have the same tag name, namespace, and
* > attributes as element, then remove the earliest such element from the list of active formatting
* > elements. For these purposes, the attributes must be compared as they were when the elements were
* > created by the parser; two elements have the same attributes if all their parsed attributes can be
* > paired such that the two attributes in each pair have identical names, namespaces, and values
* > (the order of the attributes does not matter).
*
* @TODO: Implement the "Noah's Ark clause" to only add up to three of any given kind of formatting elements to the stack.
*/
// > Add element to the list of active formatting elements.
$this->stack[] = $token;
}
/**
* Removes a node from the stack of active formatting elements.
*
* @since 6.4.0
*
* @param WP_HTML_Token $token Remove this node from the stack, if it's there already.
* @return bool Whether the node was found and removed from the stack of active formatting elements.
*/
public function remove_node( $token ) {
foreach ( $this->walk_up() as $position_from_end => $item ) {
if ( $token->bookmark_name !== $item->bookmark_name ) {
continue;
}
$position_from_start = $this->count() - $position_from_end - 1;
array_splice( $this->stack, $position_from_start, 1 );
return true;
}
return false;
}
/**
* Steps through the stack of active formatting elements, starting with the
* top element (added first) and walking downwards to the one added last.
*
* This generator function is designed to be used inside a "foreach" loop.
*
* Example:
*
* $html = '<em><strong><a>We are here';
* foreach ( $stack->walk_down() as $node ) {
* echo "{$node->node_name} -> ";
* }
* > EM -> STRONG -> A ->
*
* To start with the most-recently added element and walk towards the top,
* see WP_HTML_Active_Formatting_Elements::walk_up().
*
* @since 6.4.0
*/
public function walk_down() {
$count = count( $this->stack );
for ( $i = 0; $i < $count; $i++ ) {
yield $this->stack[ $i ];
}
}
/**
* Steps through the stack of active formatting elements, starting with the
* bottom element (added last) and walking upwards to the one added first.
*
* This generator function is designed to be used inside a "foreach" loop.
*
* Example:
*
* $html = '<em><strong><a>We are here';
* foreach ( $stack->walk_up() as $node ) {
* echo "{$node->node_name} -> ";
* }
* > A -> STRONG -> EM ->
*
* To start with the first added element and walk towards the bottom,
* see WP_HTML_Active_Formatting_Elements::walk_down().
*
* @since 6.4.0
*/
public function walk_up() {
for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) {
yield $this->stack[ $i ];
}
}
}

View File

@@ -0,0 +1,432 @@
<?php
/**
* HTML API: WP_HTML_Open_Elements class
*
* @package WordPress
* @subpackage HTML-API
* @since 6.4.0
*/
/**
* Core class used by the HTML processor during HTML parsing
* for managing the stack of open elements.
*
* This class is designed for internal use by the HTML processor.
*
* > Initially, the stack of open elements is empty. The stack grows
* > downwards; the topmost node on the stack is the first one added
* > to the stack, and the bottommost node of the stack is the most
* > recently added node in the stack (notwithstanding when the stack
* > is manipulated in a random access fashion as part of the handling
* > for misnested tags).
*
* @since 6.4.0
*
* @access private
*
* @see https://html.spec.whatwg.org/#stack-of-open-elements
* @see WP_HTML_Processor
*/
class WP_HTML_Open_Elements {
/**
* Holds the stack of open element references.
*
* @since 6.4.0
*
* @var WP_HTML_Token[]
*/
public $stack = array();
/**
* Whether a P element is in button scope currently.
*
* This class optimizes scope lookup by pre-calculating
* this value when elements are added and removed to the
* stack of open elements which might change its value.
* This avoids frequent iteration over the stack.
*
* @since 6.4.0
*
* @var bool
*/
private $has_p_in_button_scope = false;
/**
* Reports if a specific node is in the stack of open elements.
*
* @since 6.4.0
*
* @param WP_HTML_Token $token Look for this node in the stack.
* @return bool Whether the referenced node is in the stack of open elements.
*/
public function contains_node( $token ) {
foreach ( $this->walk_up() as $item ) {
if ( $token->bookmark_name === $item->bookmark_name ) {
return true;
}
}
return false;
}
/**
* Returns how many nodes are currently in the stack of open elements.
*
* @since 6.4.0
*
* @return int How many node are in the stack of open elements.
*/
public function count() {
return count( $this->stack );
}
/**
* Returns the node at the end of the stack of open elements,
* if one exists. If the stack is empty, returns null.
*
* @since 6.4.0
*
* @return WP_HTML_Token|null Last node in the stack of open elements, if one exists, otherwise null.
*/
public function current_node() {
$current_node = end( $this->stack );
return $current_node ? $current_node : null;
}
/**
* Returns whether an element is in a specific scope.
*
* ## HTML Support
*
* This function skips checking for the termination list because there
* are no supported elements which appear in the termination list.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope
*
* @param string $tag_name Name of tag check.
* @param string[] $termination_list List of elements that terminate the search.
* @return bool Whether the element was found in a specific scope.
*/
public function has_element_in_specific_scope( $tag_name, $termination_list ) {
foreach ( $this->walk_up() as $node ) {
if ( $node->node_name === $tag_name ) {
return true;
}
switch ( $node->node_name ) {
case 'HTML':
return false;
}
if ( in_array( $node->node_name, $termination_list, true ) ) {
return true;
}
}
return false;
}
/**
* Returns whether a particular element is in scope.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#has-an-element-in-scope
*
* @param string $tag_name Name of tag to check.
* @return bool Whether given element is in scope.
*/
public function has_element_in_scope( $tag_name ) {
return $this->has_element_in_specific_scope(
$tag_name,
array(
/*
* Because it's not currently possible to encounter
* one of the termination elements, they don't need
* to be listed here. If they were, they would be
* unreachable and only waste CPU cycles while
* scanning through HTML.
*/
)
);
}
/**
* Returns whether a particular element is in list item scope.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope
*
* @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
*
* @param string $tag_name Name of tag to check.
* @return bool Whether given element is in scope.
*/
public function has_element_in_list_item_scope( $tag_name ) {
throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on list item scope.' );
return false; // The linter requires this unreachable code until the function is implemented and can return.
}
/**
* Returns whether a particular element is in button scope.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#has-an-element-in-button-scope
*
* @param string $tag_name Name of tag to check.
* @return bool Whether given element is in scope.
*/
public function has_element_in_button_scope( $tag_name ) {
return $this->has_element_in_specific_scope( $tag_name, array( 'BUTTON' ) );
}
/**
* Returns whether a particular element is in table scope.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#has-an-element-in-table-scope
*
* @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
*
* @param string $tag_name Name of tag to check.
* @return bool Whether given element is in scope.
*/
public function has_element_in_table_scope( $tag_name ) {
throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on table scope.' );
return false; // The linter requires this unreachable code until the function is implemented and can return.
}
/**
* Returns whether a particular element is in select scope.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#has-an-element-in-select-scope
*
* @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
*
* @param string $tag_name Name of tag to check.
* @return bool Whether given element is in scope.
*/
public function has_element_in_select_scope( $tag_name ) {
throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on select scope.' );
return false; // The linter requires this unreachable code until the function is implemented and can return.
}
/**
* Returns whether a P is in BUTTON scope.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#has-an-element-in-button-scope
*
* @return bool Whether a P is in BUTTON scope.
*/
public function has_p_in_button_scope() {
return $this->has_p_in_button_scope;
}
/**
* Pops a node off of the stack of open elements.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#stack-of-open-elements
*
* @return bool Whether a node was popped off of the stack.
*/
public function pop() {
$item = array_pop( $this->stack );
if ( null === $item ) {
return false;
}
$this->after_element_pop( $item );
return true;
}
/**
* Pops nodes off of the stack of open elements until one with the given tag name has been popped.
*
* @since 6.4.0
*
* @see WP_HTML_Open_Elements::pop
*
* @param string $tag_name Name of tag that needs to be popped off of the stack of open elements.
* @return bool Whether a tag of the given name was found and popped off of the stack of open elements.
*/
public function pop_until( $tag_name ) {
foreach ( $this->walk_up() as $item ) {
$this->pop();
if ( $tag_name === $item->node_name ) {
return true;
}
}
return false;
}
/**
* Pushes a node onto the stack of open elements.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#stack-of-open-elements
*
* @param WP_HTML_Token $stack_item Item to add onto stack.
*/
public function push( $stack_item ) {
$this->stack[] = $stack_item;
$this->after_element_push( $stack_item );
}
/**
* Removes a specific node from the stack of open elements.
*
* @since 6.4.0
*
* @param WP_HTML_Token $token The node to remove from the stack of open elements.
* @return bool Whether the node was found and removed from the stack of open elements.
*/
public function remove_node( $token ) {
foreach ( $this->walk_up() as $position_from_end => $item ) {
if ( $token->bookmark_name !== $item->bookmark_name ) {
continue;
}
$position_from_start = $this->count() - $position_from_end - 1;
array_splice( $this->stack, $position_from_start, 1 );
$this->after_element_pop( $item );
return true;
}
return false;
}
/**
* Steps through the stack of open elements, starting with the top element
* (added first) and walking downwards to the one added last.
*
* This generator function is designed to be used inside a "foreach" loop.
*
* Example:
*
* $html = '<em><strong><a>We are here';
* foreach ( $stack->walk_down() as $node ) {
* echo "{$node->node_name} -> ";
* }
* > EM -> STRONG -> A ->
*
* To start with the most-recently added element and walk towards the top,
* see WP_HTML_Open_Elements::walk_up().
*
* @since 6.4.0
*/
public function walk_down() {
$count = count( $this->stack );
for ( $i = 0; $i < $count; $i++ ) {
yield $this->stack[ $i ];
}
}
/**
* Steps through the stack of open elements, starting with the bottom element
* (added last) and walking upwards to the one added first.
*
* This generator function is designed to be used inside a "foreach" loop.
*
* Example:
*
* $html = '<em><strong><a>We are here';
* foreach ( $stack->walk_up() as $node ) {
* echo "{$node->node_name} -> ";
* }
* > A -> STRONG -> EM ->
*
* To start with the first added element and walk towards the bottom,
* see WP_HTML_Open_Elements::walk_down().
*
* @since 6.4.0
*/
public function walk_up() {
for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) {
yield $this->stack[ $i ];
}
}
/*
* Internal helpers.
*/
/**
* Updates internal flags after adding an element.
*
* Certain conditions (such as "has_p_in_button_scope") are maintained here as
* flags that are only modified when adding and removing elements. This allows
* the HTML Processor to quickly check for these conditions instead of iterating
* over the open stack elements upon each new tag it encounters. These flags,
* however, need to be maintained as items are added and removed from the stack.
*
* @since 6.4.0
*
* @param WP_HTML_Token $item Element that was added to the stack of open elements.
*/
public function after_element_push( $item ) {
/*
* When adding support for new elements, expand this switch to trap
* cases where the precalculated value needs to change.
*/
switch ( $item->node_name ) {
case 'BUTTON':
$this->has_p_in_button_scope = false;
break;
case 'P':
$this->has_p_in_button_scope = true;
break;
}
}
/**
* Updates internal flags after removing an element.
*
* Certain conditions (such as "has_p_in_button_scope") are maintained here as
* flags that are only modified when adding and removing elements. This allows
* the HTML Processor to quickly check for these conditions instead of iterating
* over the open stack elements upon each new tag it encounters. These flags,
* however, need to be maintained as items are added and removed from the stack.
*
* @since 6.4.0
*
* @param WP_HTML_Token $item Element that was removed from the stack of open elements.
*/
public function after_element_pop( $item ) {
/*
* When adding support for new elements, expand this switch to trap
* cases where the precalculated value needs to change.
*/
switch ( $item->node_name ) {
case 'BUTTON':
$this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
break;
case 'P':
$this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
break;
}
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* HTML API: WP_HTML_Processor_State class
*
* @package WordPress
* @subpackage HTML-API
* @since 6.4.0
*/
/**
* Core class used by the HTML processor during HTML parsing
* for managing the internal parsing state.
*
* This class is designed for internal use by the HTML processor.
*
* @since 6.4.0
*
* @access private
*
* @see WP_HTML_Processor
*/
class WP_HTML_Processor_State {
/*
* Insertion mode constants.
*
* These constants exist and are named to make it easier to
* discover and recognize the supported insertion modes in
* the parser.
*
* Out of all the possible insertion modes, only those
* supported by the parser are listed here. As support
* is added to the parser for more modes, add them here
* following the same naming and value pattern.
*
* @see https://html.spec.whatwg.org/#the-insertion-mode
*/
/**
* Initial insertion mode for full HTML parser.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#the-initial-insertion-mode
* @see WP_HTML_Processor_State::$insertion_mode
*
* @var string
*/
const INSERTION_MODE_INITIAL = 'insertion-mode-initial';
/**
* In body insertion mode for full HTML parser.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#parsing-main-inbody
* @see WP_HTML_Processor_State::$insertion_mode
*
* @var string
*/
const INSERTION_MODE_IN_BODY = 'insertion-mode-in-body';
/**
* Tracks open elements while scanning HTML.
*
* This property is initialized in the constructor and never null.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#stack-of-open-elements
*
* @var WP_HTML_Open_Elements
*/
public $stack_of_open_elements = null;
/**
* Tracks open formatting elements, used to handle mis-nested formatting element tags.
*
* This property is initialized in the constructor and never null.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#list-of-active-formatting-elements
*
* @var WP_HTML_Active_Formatting_Elements
*/
public $active_formatting_elements = null;
/**
* Refers to the currently-matched tag, if any.
*
* @since 6.4.0
*
* @var WP_HTML_Token|null
*/
public $current_token = null;
/**
* Tree construction insertion mode.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#insertion-mode
*
* @var string
*/
public $insertion_mode = self::INSERTION_MODE_INITIAL;
/**
* Context node initializing fragment parser, if created as a fragment parser.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#concept-frag-parse-context
*
* @var [string, array]|null
*/
public $context_node = null;
/**
* The frameset-ok flag indicates if a `FRAMESET` element is allowed in the current state.
*
* > The frameset-ok flag is set to "ok" when the parser is created. It is set to "not ok" after certain tokens are seen.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#frameset-ok-flag
*
* @var bool
*/
public $frameset_ok = true;
/**
* Constructor - creates a new and empty state value.
*
* @since 6.4.0
*
* @see WP_HTML_Processor
*/
public function __construct() {
$this->stack_of_open_elements = new WP_HTML_Open_Elements();
$this->active_formatting_elements = new WP_HTML_Active_Formatting_Elements();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -242,6 +242,8 @@
* unquoted values will appear in the output with double-quotes.
*
* @since 6.2.0
* @since 6.2.1 Fix: Support for various invalid comments; attribute updates are case-insensitive.
* @since 6.3.2 Fix: Skip HTML-like content inside rawtext elements such as STYLE.
*/
class WP_HTML_Tag_Processor {
/**
@@ -404,6 +406,16 @@ class WP_HTML_Tag_Processor {
*/
private $attributes = array();
/**
* Tracks spans of duplicate attributes on a given tag, used for removing
* all copies of an attribute when calling `remove_attribute()`.
*
* @since 6.3.2
*
* @var (WP_HTML_Span[])[]|null
*/
private $duplicate_attributes = null;
/**
* Which class names to add or remove from a tag.
*
@@ -522,7 +534,7 @@ class WP_HTML_Tag_Processor {
* @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. </div>.
* }
* @return boolean Whether a tag was matched.
* @return bool Whether a tag was matched.
*/
public function next_tag( $query = null ) {
$this->parse_query( $query );
@@ -568,7 +580,14 @@ class WP_HTML_Tag_Processor {
* 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 ) ) {
if (
! $this->is_closing_tag &&
(
'i' === $t || 'I' === $t ||
'n' === $t || 'N' === $t ||
's' === $t || 'S' === $t ||
't' === $t || 'T' === $t
) ) {
$tag_name = $this->get_tag();
if ( 'SCRIPT' === $tag_name && ! $this->skip_script_data() ) {
@@ -580,6 +599,25 @@ class WP_HTML_Tag_Processor {
) {
$this->bytes_already_parsed = strlen( $this->html );
return false;
} elseif (
(
'IFRAME' === $tag_name ||
'NOEMBED' === $tag_name ||
'NOFRAMES' === $tag_name ||
'NOSCRIPT' === $tag_name ||
'STYLE' === $tag_name
) &&
! $this->skip_rawtext( $tag_name )
) {
/*
* "XMP" should be here too but its rules are more complicated and require the
* complexity of the HTML Processor (it needs to close out any open P element,
* meaning it can't be skipped here or else the HTML Processor will lose its
* place). For now, it can be ignored as it's a rare HTML tag in practice and
* any normative HTML should be using PRE instead.
*/
$this->bytes_already_parsed = strlen( $this->html );
return false;
}
}
} while ( $already_found < $this->sought_match_offset );
@@ -588,6 +626,94 @@ class WP_HTML_Tag_Processor {
}
/**
* Generator for a foreach loop to step through each class name for the matched tag.
*
* This generator function is designed to be used inside a "foreach" loop.
*
* Example:
*
* $p = new WP_HTML_Tag_Processor( "<div class='free &lt;egg&lt;\tlang-en'>" );
* $p->next_tag();
* foreach ( $p->class_list() as $class_name ) {
* echo "{$class_name} ";
* }
* // Outputs: "free <egg> lang-en "
*
* @since 6.4.0
*/
public function class_list() {
/** @var string $class contains the string value of the class attribute, with character references decoded. */
$class = $this->get_attribute( 'class' );
if ( ! is_string( $class ) ) {
return;
}
$seen = array();
$at = 0;
while ( $at < strlen( $class ) ) {
// Skip past any initial boundary characters.
$at += strspn( $class, " \t\f\r\n", $at );
if ( $at >= strlen( $class ) ) {
return;
}
// Find the byte length until the next boundary.
$length = strcspn( $class, " \t\f\r\n", $at );
if ( 0 === $length ) {
return;
}
/*
* CSS class names are case-insensitive in the ASCII range.
*
* @see https://www.w3.org/TR/CSS2/syndata.html#x1
*/
$name = strtolower( substr( $class, $at, $length ) );
$at += $length;
/*
* It's expected that the number of class names for a given tag is relatively small.
* Given this, it is probably faster overall to scan an array for a value rather
* than to use the class name as a key and check if it's a key of $seen.
*/
if ( in_array( $name, $seen, true ) ) {
continue;
}
$seen[] = $name;
yield $name;
}
}
/**
* Returns if a matched tag contains the given ASCII case-insensitive class name.
*
* @since 6.4.0
*
* @param string $wanted_class Look for this CSS class name, ASCII case-insensitive.
* @return bool|null Whether the matched tag contains the given class name, or null if not matched.
*/
public function has_class( $wanted_class ) {
if ( ! $this->tag_name_starts_at ) {
return null;
}
$wanted_class = strtolower( $wanted_class );
foreach ( $this->class_list() as $class_name ) {
if ( $class_name === $wanted_class ) {
return true;
}
}
return false;
}
/**
* Sets a bookmark in the HTML document.
*
@@ -673,7 +799,7 @@ class WP_HTML_Tag_Processor {
return false;
}
if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= self::MAX_BOOKMARKS ) {
if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= static::MAX_BOOKMARKS ) {
_doing_it_wrong(
__METHOD__,
__( 'Too many bookmarks: cannot create any more.' ),
@@ -710,15 +836,33 @@ class WP_HTML_Tag_Processor {
return true;
}
/**
* Skips contents of generic rawtext elements.
*
* @since 6.3.2
*
* @see https://html.spec.whatwg.org/#generic-raw-text-element-parsing-algorithm
*
* @param string $tag_name The uppercase tag name which will close the RAWTEXT region.
* @return bool Whether an end to the RAWTEXT region was found before the end of the document.
*/
private function skip_rawtext( $tag_name ) {
/*
* These two functions distinguish themselves on whether character references are
* decoded, and since functionality to read the inner markup isn't supported, it's
* not necessary to implement these two functions separately.
*/
return $this->skip_rcdata( $tag_name );
}
/**
* Skips contents of title and textarea tags.
* Skips contents of RCDATA elements, namely title and textarea tags.
*
* @since 6.2.0
*
* @see https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state
*
* @param string $tag_name The lowercase tag name which will close the RCDATA region.
* @param string $tag_name The uppercase tag name which will close the RCDATA region.
* @return bool Whether an end to the RCDATA region was found before the end of the document.
*/
private function skip_rcdata( $tag_name ) {
@@ -951,7 +1095,7 @@ class WP_HTML_Tag_Processor {
if ( '/' === $this->html[ $at + 1 ] ) {
$this->is_closing_tag = true;
$at++;
++$at;
} else {
$this->is_closing_tag = false;
}
@@ -1020,7 +1164,7 @@ class WP_HTML_Tag_Processor {
*
* See https://html.spec.whatwg.org/#parse-error-incorrectly-closed-comment
*/
$closer_at--; // Pre-increment inside condition below reduces risk of accidental infinite looping.
--$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 ) {
@@ -1101,7 +1245,7 @@ class WP_HTML_Tag_Processor {
* See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name
*/
if ( '>' === $html[ $at + 1 ] ) {
$at++;
++$at;
continue;
}
@@ -1240,6 +1384,25 @@ class WP_HTML_Tag_Processor {
$attribute_end,
! $has_value
);
return true;
}
/*
* Track the duplicate attributes so if we remove it, all disappear together.
*
* While `$this->duplicated_attributes` could always be stored as an `array()`,
* which would simplify the logic here, storing a `null` and only allocating
* an array when encountering duplicates avoids needless allocations in the
* normative case of parsing tags with no duplicate attributes.
*/
$duplicate_span = new WP_HTML_Span( $attribute_start, $attribute_end );
if ( null === $this->duplicate_attributes ) {
$this->duplicate_attributes = array( $comparable_name => array( $duplicate_span ) );
} elseif ( ! array_key_exists( $comparable_name, $this->duplicate_attributes ) ) {
$this->duplicate_attributes[ $comparable_name ] = array( $duplicate_span );
} else {
$this->duplicate_attributes[ $comparable_name ][] = $duplicate_span;
}
return true;
@@ -1261,11 +1424,12 @@ class WP_HTML_Tag_Processor {
*/
private function after_tag() {
$this->get_updated_html();
$this->tag_name_starts_at = null;
$this->tag_name_length = null;
$this->tag_ends_at = null;
$this->is_closing_tag = null;
$this->attributes = array();
$this->tag_name_starts_at = null;
$this->tag_name_length = null;
$this->tag_ends_at = null;
$this->is_closing_tag = null;
$this->attributes = array();
$this->duplicate_attributes = null;
}
/**
@@ -1538,7 +1702,7 @@ class WP_HTML_Tag_Processor {
return false;
}
if ( ++$this->seek_count > self::MAX_SEEK_OPS ) {
if ( ++$this->seek_count > static::MAX_SEEK_OPS ) {
_doing_it_wrong(
__METHOD__,
__( 'Too many calls to seek() - this can lead to performance issues.' ),
@@ -1743,7 +1907,7 @@ class WP_HTML_Tag_Processor {
* @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 ) {
public function get_attribute_names_with_prefix( $prefix ) {
if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) {
return null;
}
@@ -2034,6 +2198,17 @@ class WP_HTML_Tag_Processor {
''
);
// 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;
}
@@ -2095,6 +2270,7 @@ class WP_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.
*/
@@ -2128,24 +2304,29 @@ class WP_HTML_Tag_Processor {
* 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.
* 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.
*
* <p>Previous HTML<em>More HTML</em></p>
* ^ | back up by the length of the tag name plus the opening <
* \<-/ back up by strlen("em") + 1 ==> 3
* 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();
$this->parse_next_tag();
// Reparse the attributes.
while ( $this->parse_next_attribute() ) {
continue;
}
// Restore previous state.
$this->bytes_already_parsed = $previous_parsed_byte_count;
$this->parse_query( $previous_query );
$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;
}
@@ -2221,7 +2402,7 @@ class WP_HTML_Tag_Processor {
*
* @since 6.2.0
*
* @return boolean Whether the given tag and its attribute match the search criteria.
* @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 ) {
@@ -2260,64 +2441,7 @@ class WP_HTML_Tag_Processor {
}
}
$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;
}
if ( null !== $this->sought_class_name && ! $this->has_class( $this->sought_class_name ) ) {
return false;
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* HTML API: WP_HTML_Token class
*
* @package WordPress
* @subpackage HTML-API
* @since 6.4.0
*/
/**
* Core class used by the HTML processor during HTML parsing
* for referring to tokens in the input HTML string.
*
* This class is designed for internal use by the HTML processor.
*
* @since 6.4.0
*
* @access private
*
* @see WP_HTML_Processor
*/
class WP_HTML_Token {
/**
* Name of bookmark corresponding to source of token in input HTML string.
*
* Having a bookmark name does not imply that the token still exists. It
* may be that the source token and underlying bookmark was wiped out by
* some modification to the source HTML.
*
* @since 6.4.0
*
* @var string
*/
public $bookmark_name = null;
/**
* Name of node; lowercase names such as "marker" are not HTML elements.
*
* For HTML elements/tags this value should come from WP_HTML_Processor::get_tag().
*
* @since 6.4.0
*
* @see WP_HTML_Processor::get_tag()
*
* @var string
*/
public $node_name = null;
/**
* Whether node contains the self-closing flag.
*
* A node may have a self-closing flag when it shouldn't. This value
* only reports if the flag is present in the original HTML.
*
* @since 6.4.0
*
* @see https://html.spec.whatwg.org/#self-closing-flag
*
* @var bool
*/
public $has_self_closing_flag = false;
/**
* Called when token is garbage-collected or otherwise destroyed.
*
* @var callable|null
*/
public $on_destroy = null;
/**
* Constructor - creates a reference to a token in some external HTML string.
*
* @since 6.4.0
*
* @param string $bookmark_name Name of bookmark corresponding to location in HTML where token is found.
* @param string $node_name Name of node token represents; if uppercase, an HTML element; if lowercase, a special value like "marker".
* @param bool $has_self_closing_flag Whether the source token contains the self-closing flag, regardless of whether it's valid.
* @param callable $on_destroy Function to call when destroying token, useful for releasing the bookmark.
*/
public function __construct( $bookmark_name, $node_name, $has_self_closing_flag, $on_destroy = null ) {
$this->bookmark_name = $bookmark_name;
$this->node_name = $node_name;
$this->has_self_closing_flag = $has_self_closing_flag;
$this->on_destroy = $on_destroy;
}
/**
* Destructor.
*
* @since 6.4.0
*/
public function __destruct() {
if ( is_callable( $this->on_destroy ) ) {
call_user_func( $this->on_destroy, $this->bookmark_name );
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* HTML API: WP_HTML_Unsupported_Exception class
*
* @package WordPress
* @subpackage HTML-API
* @since 6.4.0
*/
/**
* Core class used by the HTML processor during HTML parsing
* for indicating that a given operation is unsupported.
*
* This class is designed for internal use by the HTML processor.
*
* The HTML API aims to operate in compliance with the HTML5
* specification, but does not implement the full specification.
* In cases where it lacks support it should not cause breakage
* or unexpected behavior. In the cases where it recognizes that
* it cannot proceed, this class is used to abort from any
* operation and signify that the given HTML cannot be processed.
*
* @since 6.4.0
*
* @access private
*
* @see WP_HTML_Processor
*/
class WP_HTML_Unsupported_Exception extends Exception {
}