wp/wp-includes/html-api/class-wp-html-open-elements.php
changeset 21 48c4eec2b7e6
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     1 <?php
       
     2 /**
       
     3  * HTML API: WP_HTML_Open_Elements class
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage HTML-API
       
     7  * @since 6.4.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Core class used by the HTML processor during HTML parsing
       
    12  * for managing the stack of open elements.
       
    13  *
       
    14  * This class is designed for internal use by the HTML processor.
       
    15  *
       
    16  * > Initially, the stack of open elements is empty. The stack grows
       
    17  * > downwards; the topmost node on the stack is the first one added
       
    18  * > to the stack, and the bottommost node of the stack is the most
       
    19  * > recently added node in the stack (notwithstanding when the stack
       
    20  * > is manipulated in a random access fashion as part of the handling
       
    21  * > for misnested tags).
       
    22  *
       
    23  * @since 6.4.0
       
    24  *
       
    25  * @access private
       
    26  *
       
    27  * @see https://html.spec.whatwg.org/#stack-of-open-elements
       
    28  * @see WP_HTML_Processor
       
    29  */
       
    30 class WP_HTML_Open_Elements {
       
    31 	/**
       
    32 	 * Holds the stack of open element references.
       
    33 	 *
       
    34 	 * @since 6.4.0
       
    35 	 *
       
    36 	 * @var WP_HTML_Token[]
       
    37 	 */
       
    38 	public $stack = array();
       
    39 
       
    40 	/**
       
    41 	 * Whether a P element is in button scope currently.
       
    42 	 *
       
    43 	 * This class optimizes scope lookup by pre-calculating
       
    44 	 * this value when elements are added and removed to the
       
    45 	 * stack of open elements which might change its value.
       
    46 	 * This avoids frequent iteration over the stack.
       
    47 	 *
       
    48 	 * @since 6.4.0
       
    49 	 *
       
    50 	 * @var bool
       
    51 	 */
       
    52 	private $has_p_in_button_scope = false;
       
    53 
       
    54 	/**
       
    55 	 * A function that will be called when an item is popped off the stack of open elements.
       
    56 	 *
       
    57 	 * The function will be called with the popped item as its argument.
       
    58 	 *
       
    59 	 * @since 6.6.0
       
    60 	 *
       
    61 	 * @var Closure
       
    62 	 */
       
    63 	private $pop_handler = null;
       
    64 
       
    65 	/**
       
    66 	 * A function that will be called when an item is pushed onto the stack of open elements.
       
    67 	 *
       
    68 	 * The function will be called with the pushed item as its argument.
       
    69 	 *
       
    70 	 * @since 6.6.0
       
    71 	 *
       
    72 	 * @var Closure
       
    73 	 */
       
    74 	private $push_handler = null;
       
    75 
       
    76 	/**
       
    77 	 * Sets a pop handler that will be called when an item is popped off the stack of
       
    78 	 * open elements.
       
    79 	 *
       
    80 	 * The function will be called with the pushed item as its argument.
       
    81 	 *
       
    82 	 * @since 6.6.0
       
    83 	 *
       
    84 	 * @param Closure $handler The handler function.
       
    85 	 */
       
    86 	public function set_pop_handler( Closure $handler ) {
       
    87 		$this->pop_handler = $handler;
       
    88 	}
       
    89 
       
    90 	/**
       
    91 	 * Sets a push handler that will be called when an item is pushed onto the stack of
       
    92 	 * open elements.
       
    93 	 *
       
    94 	 * The function will be called with the pushed item as its argument.
       
    95 	 *
       
    96 	 * @since 6.6.0
       
    97 	 *
       
    98 	 * @param Closure $handler The handler function.
       
    99 	 */
       
   100 	public function set_push_handler( Closure $handler ) {
       
   101 		$this->push_handler = $handler;
       
   102 	}
       
   103 
       
   104 	/**
       
   105 	 * Reports if a specific node is in the stack of open elements.
       
   106 	 *
       
   107 	 * @since 6.4.0
       
   108 	 *
       
   109 	 * @param WP_HTML_Token $token Look for this node in the stack.
       
   110 	 * @return bool Whether the referenced node is in the stack of open elements.
       
   111 	 */
       
   112 	public function contains_node( $token ) {
       
   113 		foreach ( $this->walk_up() as $item ) {
       
   114 			if ( $token->bookmark_name === $item->bookmark_name ) {
       
   115 				return true;
       
   116 			}
       
   117 		}
       
   118 
       
   119 		return false;
       
   120 	}
       
   121 
       
   122 	/**
       
   123 	 * Returns how many nodes are currently in the stack of open elements.
       
   124 	 *
       
   125 	 * @since 6.4.0
       
   126 	 *
       
   127 	 * @return int How many node are in the stack of open elements.
       
   128 	 */
       
   129 	public function count() {
       
   130 		return count( $this->stack );
       
   131 	}
       
   132 
       
   133 	/**
       
   134 	 * Returns the node at the end of the stack of open elements,
       
   135 	 * if one exists. If the stack is empty, returns null.
       
   136 	 *
       
   137 	 * @since 6.4.0
       
   138 	 *
       
   139 	 * @return WP_HTML_Token|null Last node in the stack of open elements, if one exists, otherwise null.
       
   140 	 */
       
   141 	public function current_node() {
       
   142 		$current_node = end( $this->stack );
       
   143 
       
   144 		return $current_node ? $current_node : null;
       
   145 	}
       
   146 
       
   147 	/**
       
   148 	 * Returns whether an element is in a specific scope.
       
   149 	 *
       
   150 	 * ## HTML Support
       
   151 	 *
       
   152 	 * This function skips checking for the termination list because there
       
   153 	 * are no supported elements which appear in the termination list.
       
   154 	 *
       
   155 	 * @since 6.4.0
       
   156 	 *
       
   157 	 * @see https://html.spec.whatwg.org/#has-an-element-in-the-specific-scope
       
   158 	 *
       
   159 	 * @param string   $tag_name         Name of tag check.
       
   160 	 * @param string[] $termination_list List of elements that terminate the search.
       
   161 	 * @return bool Whether the element was found in a specific scope.
       
   162 	 */
       
   163 	public function has_element_in_specific_scope( $tag_name, $termination_list ) {
       
   164 		foreach ( $this->walk_up() as $node ) {
       
   165 			if ( $node->node_name === $tag_name ) {
       
   166 				return true;
       
   167 			}
       
   168 
       
   169 			if (
       
   170 				'(internal: H1 through H6 - do not use)' === $tag_name &&
       
   171 				in_array( $node->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
       
   172 			) {
       
   173 				return true;
       
   174 			}
       
   175 
       
   176 			switch ( $node->node_name ) {
       
   177 				case 'HTML':
       
   178 					return false;
       
   179 			}
       
   180 
       
   181 			if ( in_array( $node->node_name, $termination_list, true ) ) {
       
   182 				return false;
       
   183 			}
       
   184 		}
       
   185 
       
   186 		return false;
       
   187 	}
       
   188 
       
   189 	/**
       
   190 	 * Returns whether a particular element is in scope.
       
   191 	 *
       
   192 	 * @since 6.4.0
       
   193 	 *
       
   194 	 * @see https://html.spec.whatwg.org/#has-an-element-in-scope
       
   195 	 *
       
   196 	 * @param string $tag_name Name of tag to check.
       
   197 	 * @return bool Whether given element is in scope.
       
   198 	 */
       
   199 	public function has_element_in_scope( $tag_name ) {
       
   200 		return $this->has_element_in_specific_scope(
       
   201 			$tag_name,
       
   202 			array(
       
   203 
       
   204 				/*
       
   205 				 * Because it's not currently possible to encounter
       
   206 				 * one of the termination elements, they don't need
       
   207 				 * to be listed here. If they were, they would be
       
   208 				 * unreachable and only waste CPU cycles while
       
   209 				 * scanning through HTML.
       
   210 				 */
       
   211 			)
       
   212 		);
       
   213 	}
       
   214 
       
   215 	/**
       
   216 	 * Returns whether a particular element is in list item scope.
       
   217 	 *
       
   218 	 * @since 6.4.0
       
   219 	 * @since 6.5.0 Implemented: no longer throws on every invocation.
       
   220 	 *
       
   221 	 * @see https://html.spec.whatwg.org/#has-an-element-in-list-item-scope
       
   222 	 *
       
   223 	 * @param string $tag_name Name of tag to check.
       
   224 	 * @return bool Whether given element is in scope.
       
   225 	 */
       
   226 	public function has_element_in_list_item_scope( $tag_name ) {
       
   227 		return $this->has_element_in_specific_scope(
       
   228 			$tag_name,
       
   229 			array(
       
   230 				// There are more elements that belong here which aren't currently supported.
       
   231 				'OL',
       
   232 				'UL',
       
   233 			)
       
   234 		);
       
   235 	}
       
   236 
       
   237 	/**
       
   238 	 * Returns whether a particular element is in button scope.
       
   239 	 *
       
   240 	 * @since 6.4.0
       
   241 	 *
       
   242 	 * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope
       
   243 	 *
       
   244 	 * @param string $tag_name Name of tag to check.
       
   245 	 * @return bool Whether given element is in scope.
       
   246 	 */
       
   247 	public function has_element_in_button_scope( $tag_name ) {
       
   248 		return $this->has_element_in_specific_scope( $tag_name, array( 'BUTTON' ) );
       
   249 	}
       
   250 
       
   251 	/**
       
   252 	 * Returns whether a particular element is in table scope.
       
   253 	 *
       
   254 	 * @since 6.4.0
       
   255 	 *
       
   256 	 * @see https://html.spec.whatwg.org/#has-an-element-in-table-scope
       
   257 	 *
       
   258 	 * @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
       
   259 	 *
       
   260 	 * @param string $tag_name Name of tag to check.
       
   261 	 * @return bool Whether given element is in scope.
       
   262 	 */
       
   263 	public function has_element_in_table_scope( $tag_name ) {
       
   264 		throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on table scope.' );
       
   265 
       
   266 		return false; // The linter requires this unreachable code until the function is implemented and can return.
       
   267 	}
       
   268 
       
   269 	/**
       
   270 	 * Returns whether a particular element is in select scope.
       
   271 	 *
       
   272 	 * @since 6.4.0
       
   273 	 *
       
   274 	 * @see https://html.spec.whatwg.org/#has-an-element-in-select-scope
       
   275 	 *
       
   276 	 * @throws WP_HTML_Unsupported_Exception Always until this function is implemented.
       
   277 	 *
       
   278 	 * @param string $tag_name Name of tag to check.
       
   279 	 * @return bool Whether given element is in scope.
       
   280 	 */
       
   281 	public function has_element_in_select_scope( $tag_name ) {
       
   282 		throw new WP_HTML_Unsupported_Exception( 'Cannot process elements depending on select scope.' );
       
   283 
       
   284 		return false; // The linter requires this unreachable code until the function is implemented and can return.
       
   285 	}
       
   286 
       
   287 	/**
       
   288 	 * Returns whether a P is in BUTTON scope.
       
   289 	 *
       
   290 	 * @since 6.4.0
       
   291 	 *
       
   292 	 * @see https://html.spec.whatwg.org/#has-an-element-in-button-scope
       
   293 	 *
       
   294 	 * @return bool Whether a P is in BUTTON scope.
       
   295 	 */
       
   296 	public function has_p_in_button_scope() {
       
   297 		return $this->has_p_in_button_scope;
       
   298 	}
       
   299 
       
   300 	/**
       
   301 	 * Pops a node off of the stack of open elements.
       
   302 	 *
       
   303 	 * @since 6.4.0
       
   304 	 *
       
   305 	 * @see https://html.spec.whatwg.org/#stack-of-open-elements
       
   306 	 *
       
   307 	 * @return bool Whether a node was popped off of the stack.
       
   308 	 */
       
   309 	public function pop() {
       
   310 		$item = array_pop( $this->stack );
       
   311 		if ( null === $item ) {
       
   312 			return false;
       
   313 		}
       
   314 
       
   315 		if ( 'context-node' === $item->bookmark_name ) {
       
   316 			$this->stack[] = $item;
       
   317 			return false;
       
   318 		}
       
   319 
       
   320 		$this->after_element_pop( $item );
       
   321 		return true;
       
   322 	}
       
   323 
       
   324 	/**
       
   325 	 * Pops nodes off of the stack of open elements until one with the given tag name has been popped.
       
   326 	 *
       
   327 	 * @since 6.4.0
       
   328 	 *
       
   329 	 * @see WP_HTML_Open_Elements::pop
       
   330 	 *
       
   331 	 * @param string $tag_name Name of tag that needs to be popped off of the stack of open elements.
       
   332 	 * @return bool Whether a tag of the given name was found and popped off of the stack of open elements.
       
   333 	 */
       
   334 	public function pop_until( $tag_name ) {
       
   335 		foreach ( $this->walk_up() as $item ) {
       
   336 			if ( 'context-node' === $item->bookmark_name ) {
       
   337 				return true;
       
   338 			}
       
   339 
       
   340 			$this->pop();
       
   341 
       
   342 			if (
       
   343 				'(internal: H1 through H6 - do not use)' === $tag_name &&
       
   344 				in_array( $item->node_name, array( 'H1', 'H2', 'H3', 'H4', 'H5', 'H6' ), true )
       
   345 			) {
       
   346 				return true;
       
   347 			}
       
   348 
       
   349 			if ( $tag_name === $item->node_name ) {
       
   350 				return true;
       
   351 			}
       
   352 		}
       
   353 
       
   354 		return false;
       
   355 	}
       
   356 
       
   357 	/**
       
   358 	 * Pushes a node onto the stack of open elements.
       
   359 	 *
       
   360 	 * @since 6.4.0
       
   361 	 *
       
   362 	 * @see https://html.spec.whatwg.org/#stack-of-open-elements
       
   363 	 *
       
   364 	 * @param WP_HTML_Token $stack_item Item to add onto stack.
       
   365 	 */
       
   366 	public function push( $stack_item ) {
       
   367 		$this->stack[] = $stack_item;
       
   368 		$this->after_element_push( $stack_item );
       
   369 	}
       
   370 
       
   371 	/**
       
   372 	 * Removes a specific node from the stack of open elements.
       
   373 	 *
       
   374 	 * @since 6.4.0
       
   375 	 *
       
   376 	 * @param WP_HTML_Token $token The node to remove from the stack of open elements.
       
   377 	 * @return bool Whether the node was found and removed from the stack of open elements.
       
   378 	 */
       
   379 	public function remove_node( $token ) {
       
   380 		if ( 'context-node' === $token->bookmark_name ) {
       
   381 			return false;
       
   382 		}
       
   383 
       
   384 		foreach ( $this->walk_up() as $position_from_end => $item ) {
       
   385 			if ( $token->bookmark_name !== $item->bookmark_name ) {
       
   386 				continue;
       
   387 			}
       
   388 
       
   389 			$position_from_start = $this->count() - $position_from_end - 1;
       
   390 			array_splice( $this->stack, $position_from_start, 1 );
       
   391 			$this->after_element_pop( $item );
       
   392 			return true;
       
   393 		}
       
   394 
       
   395 		return false;
       
   396 	}
       
   397 
       
   398 
       
   399 	/**
       
   400 	 * Steps through the stack of open elements, starting with the top element
       
   401 	 * (added first) and walking downwards to the one added last.
       
   402 	 *
       
   403 	 * This generator function is designed to be used inside a "foreach" loop.
       
   404 	 *
       
   405 	 * Example:
       
   406 	 *
       
   407 	 *     $html = '<em><strong><a>We are here';
       
   408 	 *     foreach ( $stack->walk_down() as $node ) {
       
   409 	 *         echo "{$node->node_name} -> ";
       
   410 	 *     }
       
   411 	 *     > EM -> STRONG -> A ->
       
   412 	 *
       
   413 	 * To start with the most-recently added element and walk towards the top,
       
   414 	 * see WP_HTML_Open_Elements::walk_up().
       
   415 	 *
       
   416 	 * @since 6.4.0
       
   417 	 */
       
   418 	public function walk_down() {
       
   419 		$count = count( $this->stack );
       
   420 
       
   421 		for ( $i = 0; $i < $count; $i++ ) {
       
   422 			yield $this->stack[ $i ];
       
   423 		}
       
   424 	}
       
   425 
       
   426 	/**
       
   427 	 * Steps through the stack of open elements, starting with the bottom element
       
   428 	 * (added last) and walking upwards to the one added first.
       
   429 	 *
       
   430 	 * This generator function is designed to be used inside a "foreach" loop.
       
   431 	 *
       
   432 	 * Example:
       
   433 	 *
       
   434 	 *     $html = '<em><strong><a>We are here';
       
   435 	 *     foreach ( $stack->walk_up() as $node ) {
       
   436 	 *         echo "{$node->node_name} -> ";
       
   437 	 *     }
       
   438 	 *     > A -> STRONG -> EM ->
       
   439 	 *
       
   440 	 * To start with the first added element and walk towards the bottom,
       
   441 	 * see WP_HTML_Open_Elements::walk_down().
       
   442 	 *
       
   443 	 * @since 6.4.0
       
   444 	 * @since 6.5.0 Accepts $above_this_node to start traversal above a given node, if it exists.
       
   445 	 *
       
   446 	 * @param ?WP_HTML_Token $above_this_node Start traversing above this node, if provided and if the node exists.
       
   447 	 */
       
   448 	public function walk_up( $above_this_node = null ) {
       
   449 		$has_found_node = null === $above_this_node;
       
   450 
       
   451 		for ( $i = count( $this->stack ) - 1; $i >= 0; $i-- ) {
       
   452 			$node = $this->stack[ $i ];
       
   453 
       
   454 			if ( ! $has_found_node ) {
       
   455 				$has_found_node = $node === $above_this_node;
       
   456 				continue;
       
   457 			}
       
   458 
       
   459 			yield $node;
       
   460 		}
       
   461 	}
       
   462 
       
   463 	/*
       
   464 	 * Internal helpers.
       
   465 	 */
       
   466 
       
   467 	/**
       
   468 	 * Updates internal flags after adding an element.
       
   469 	 *
       
   470 	 * Certain conditions (such as "has_p_in_button_scope") are maintained here as
       
   471 	 * flags that are only modified when adding and removing elements. This allows
       
   472 	 * the HTML Processor to quickly check for these conditions instead of iterating
       
   473 	 * over the open stack elements upon each new tag it encounters. These flags,
       
   474 	 * however, need to be maintained as items are added and removed from the stack.
       
   475 	 *
       
   476 	 * @since 6.4.0
       
   477 	 *
       
   478 	 * @param WP_HTML_Token $item Element that was added to the stack of open elements.
       
   479 	 */
       
   480 	public function after_element_push( $item ) {
       
   481 		/*
       
   482 		 * When adding support for new elements, expand this switch to trap
       
   483 		 * cases where the precalculated value needs to change.
       
   484 		 */
       
   485 		switch ( $item->node_name ) {
       
   486 			case 'BUTTON':
       
   487 				$this->has_p_in_button_scope = false;
       
   488 				break;
       
   489 
       
   490 			case 'P':
       
   491 				$this->has_p_in_button_scope = true;
       
   492 				break;
       
   493 		}
       
   494 
       
   495 		if ( null !== $this->push_handler ) {
       
   496 			( $this->push_handler )( $item );
       
   497 		}
       
   498 	}
       
   499 
       
   500 	/**
       
   501 	 * Updates internal flags after removing an element.
       
   502 	 *
       
   503 	 * Certain conditions (such as "has_p_in_button_scope") are maintained here as
       
   504 	 * flags that are only modified when adding and removing elements. This allows
       
   505 	 * the HTML Processor to quickly check for these conditions instead of iterating
       
   506 	 * over the open stack elements upon each new tag it encounters. These flags,
       
   507 	 * however, need to be maintained as items are added and removed from the stack.
       
   508 	 *
       
   509 	 * @since 6.4.0
       
   510 	 *
       
   511 	 * @param WP_HTML_Token $item Element that was removed from the stack of open elements.
       
   512 	 */
       
   513 	public function after_element_pop( $item ) {
       
   514 		/*
       
   515 		 * When adding support for new elements, expand this switch to trap
       
   516 		 * cases where the precalculated value needs to change.
       
   517 		 */
       
   518 		switch ( $item->node_name ) {
       
   519 			case 'BUTTON':
       
   520 				$this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
       
   521 				break;
       
   522 
       
   523 			case 'P':
       
   524 				$this->has_p_in_button_scope = $this->has_element_in_button_scope( 'P' );
       
   525 				break;
       
   526 		}
       
   527 
       
   528 		if ( null !== $this->pop_handler ) {
       
   529 			( $this->pop_handler )( $item );
       
   530 		}
       
   531 	}
       
   532 
       
   533 	/**
       
   534 	 * Wakeup magic method.
       
   535 	 *
       
   536 	 * @since 6.6.0
       
   537 	 */
       
   538 	public function __wakeup() {
       
   539 		throw new \LogicException( __CLASS__ . ' should never be unserialized' );
       
   540 	}
       
   541 }