@@ -251,6 +251,15 @@ class WP_HTML_Processor extends WP_HTML_Tag_Processor {
251251 */
252252 private $ current_element = null ;
253253
254+ /**
255+ * Elements removed from the stack of open elements without a normal pop event.
256+ *
257+ * @since 7.1.0
258+ *
259+ * @var array[]
260+ */
261+ private $ non_lifo_breadcrumb_removals = array ();
262+
254263 /**
255264 * Context node if created as a fragment parser.
256265 *
@@ -814,6 +823,10 @@ private function next_visitable_token(): bool {
814823 * tokens works in the meantime and isn't obviously wrong.
815824 */
816825 if ( empty ( $ this ->element_queue ) ) {
826+ if ( $ this ->queue_virtual_closer_after_non_lifo_removal () ) {
827+ return $ this ->next_visitable_token ();
828+ }
829+
817830 if ( $ this ->step () ) {
818831 return $ this ->next_visitable_token ();
819832 }
@@ -823,6 +836,10 @@ private function next_visitable_token(): bool {
823836 }
824837 }
825838
839+ if ( $ this ->queue_virtual_closer_after_non_lifo_removal () ) {
840+ return $ this ->next_visitable_token ();
841+ }
842+
826843 // Process the next event on the queue.
827844 $ this ->current_element = array_shift ( $ this ->element_queue );
828845 if ( ! isset ( $ this ->current_element ) ) {
@@ -860,6 +877,61 @@ private function next_visitable_token(): bool {
860877 return true ;
861878 }
862879
880+ /**
881+ * Queues a virtual closer for a removed node once its subtree closes.
882+ *
883+ * Non-LIFO removals from the stack of open elements do not emit a normal
884+ * pop event because those events blindly pop the current breadcrumb. The
885+ * removed node remains an ancestor of the currently open subtree, but must
886+ * be reported as a virtual closer before visiting the next token after
887+ * that subtree closes.
888+ *
889+ * @since 7.1.0
890+ *
891+ * @return bool Whether a virtual closer was queued.
892+ */
893+ private function queue_virtual_closer_after_non_lifo_removal (): bool {
894+ if ( empty ( $ this ->non_lifo_breadcrumb_removals ) ) {
895+ return false ;
896+ }
897+
898+ $ removed_node = end ( $ this ->non_lifo_breadcrumb_removals );
899+ $ removed_token = $ removed_node ['token ' ];
900+ $ breadcrumb_depth = $ removed_node ['breadcrumb_depth ' ];
901+
902+ if (
903+ count ( $ this ->breadcrumbs ) !== $ breadcrumb_depth ||
904+ empty ( $ this ->breadcrumbs ) ||
905+ end ( $ this ->breadcrumbs ) !== $ removed_token ->node_name
906+ ) {
907+ return false ;
908+ }
909+
910+ // At EOF, normal stack pops may be queued and processed after the stack is empty.
911+ $ adjusted_current_node = $ this ->get_adjusted_current_node ();
912+
913+ if ( isset ( $ adjusted_current_node ) && end ( $ this ->breadcrumbs ) === $ adjusted_current_node ->node_name ) {
914+ return false ;
915+ }
916+
917+ $ next_event = reset ( $ this ->element_queue );
918+ if (
919+ false !== $ next_event &&
920+ WP_HTML_Stack_Event::POP === $ next_event ->operation &&
921+ $ next_event ->token !== $ removed_token &&
922+ $ next_event ->token ->node_name === $ removed_token ->node_name
923+ ) {
924+ return false ;
925+ }
926+
927+ array_pop ( $ this ->non_lifo_breadcrumb_removals );
928+ array_unshift (
929+ $ this ->element_queue ,
930+ new WP_HTML_Stack_Event ( $ removed_token , WP_HTML_Stack_Event::POP , 'virtual ' )
931+ );
932+ return true ;
933+ }
934+
863935 /**
864936 * Indicates if the current tag token is a tag closer.
865937 *
@@ -2848,7 +2920,18 @@ private function step_in_body(): bool {
28482920 case 'A ' :
28492921 $ this ->run_adoption_agency_algorithm ();
28502922 $ this ->state ->active_formatting_elements ->remove_node ( $ item );
2851- $ this ->state ->stack_of_open_elements ->remove_node ( $ item );
2923+ $ is_current_node = $ item === $ this ->state ->stack_of_open_elements ->current_node ();
2924+ if ( $ this ->state ->stack_of_open_elements ->remove_node ( $ item ) && ! $ is_current_node ) {
2925+ $ breadcrumb_depth = count ( $ this ->breadcrumbs );
2926+ while ( 0 < $ breadcrumb_depth && $ this ->breadcrumbs [ $ breadcrumb_depth - 1 ] !== $ item ->node_name ) {
2927+ --$ breadcrumb_depth ;
2928+ }
2929+
2930+ $ this ->non_lifo_breadcrumb_removals [] = array (
2931+ 'token ' => $ item ,
2932+ 'breadcrumb_depth ' => $ breadcrumb_depth ,
2933+ );
2934+ }
28522935 break 2 ;
28532936 }
28542937 }
@@ -5675,6 +5758,7 @@ public function seek( $bookmark_name ): bool {
56755758 $ this ->state ->current_token = null ;
56765759 $ this ->current_element = null ;
56775760 $ this ->element_queue = array ();
5761+ $ this ->non_lifo_breadcrumb_removals = array ();
56785762
56795763 /*
56805764 * The absence of a context node indicates a full parse.
0 commit comments