]/i', $page ) || 1 !== preg_match( '/\sclass\s*=\s*["\']?(?:[^"\'>]*\s)?wrap[\s"\'>]/i', $page ) ) ) { return null; // no mark, and no heading or no .wrap: nothing to scan for } $scan = self::scanner( $page ); $divs = 0; $wrap = 0; // the depth of the .wrap the scan is in, 0 outside $heading = null; while ( $scan->next_tag( array( 'tag_closers' => 'visit' ) ) ) { $tag = (string) $scan->get_tag(); if ( 'DIV' === $tag ) { if ( $scan->is_tag_closer() ) { $wrap = $wrap === $divs ? 0 : $wrap; --$divs; continue; } ++$divs; $wrap = 0 === $wrap && true === $scan->has_class( 'wrap' ) ? $divs : $wrap; } if ( $scan->is_tag_closer() ) { continue; } if ( true === $scan->has_class( 'wp-header-end' ) ) { $at = self::after( $scan, $tag ); return null !== $at ? array( 'at' => $at, 'kit' => 0 !== $wrap ) : null; } if ( null === $heading && 0 !== $wrap && ( 'H1' === $tag || 'H2' === $tag ) ) { $heading = self::after( $scan, $tag ); // kept: a .wp-header-end further down wins if ( ! $ends ) { break; } } } return null !== $heading ? array( 'at' => $heading, 'kit' => false ) : null; } /** * The offset after the element the scan stands on: its tag when void, else its closer (the scan moves to it). * * @param object $scan The scan (scanner()). * @param string $tag The element's tag name. */ private static function after( $scan, string $tag ): ?int { $span = $scan->span(); if ( \WP_HTML_Processor::is_void( $tag ) ) { return $span[1]; } $depth = 1; while ( $scan->next_tag( array( 'tag_closers' => 'visit' ) ) ) { if ( $tag !== $scan->get_tag() ) { continue; } $depth += $scan->is_tag_closer() ? -1 : 1; if ( 0 === $depth ) { return $scan->span()[1]; } } return null; } /** * The notices cut into what the scripts move and what stays: every `div.notice`, `.updated` or `.error` that is not * `inline` (nor `below-h2`), wherever it sits, is core's; a `div.rooster-admin-notice` outside any div is the * kit's when the chrome's script moves it ($kit). An inline script right after a moved notice goes with it. * * @param string $notices What the notices' hooks printed. * @param bool $kit Whether the kit's notices move. * @return array{stay: string, core: string, kit: string}|null Null when a notice is not closed. */ private static function cut( string $notices, bool $kit ): ?array { $scan = self::scanner( $notices ); $cuts = array(); // each: its start, its end, whose (core or kit) $divs = 0; $open = null; $after = null; // the cut a script right after it would join while ( $scan->next_tag( array( 'tag_closers' => 'visit' ) ) ) { $tag = (string) $scan->get_tag(); $span = $scan->span(); if ( null !== $after ) { $joins = 'SCRIPT' === $tag && ! $scan->is_tag_closer() && '' === trim( substr( $notices, $cuts[ $after ][1], $span[0] - $cuts[ $after ][1] ) ) && null === $scan->get_attribute( 'src' ); $after = null; if ( $joins ) { $cuts[ count( $cuts ) - 1 ][1] = self::close_of( $scan, $notices ); continue; } } if ( 'DIV' !== $tag ) { continue; } if ( $scan->is_tag_closer() ) { if ( null !== $open && $divs === $open[1] ) { $cuts[] = array( $open[0], $span[1], $open[2] ); $after = count( $cuts ) - 1; $open = null; } --$divs; continue; } ++$divs; if ( null !== $open ) { continue; } if ( ( $scan->has_class( 'notice' ) || $scan->has_class( 'updated' ) || $scan->has_class( 'error' ) ) && ! $scan->has_class( 'inline' ) && ! $scan->has_class( 'below-h2' ) ) { $open = array( $span[0], $divs, 'core' ); } elseif ( $kit && 1 === $divs && $scan->has_class( 'rooster-admin-notice' ) ) { $open = array( $span[0], $divs, 'kit' ); } } if ( null !== $open ) { return null; } $out = array( 'stay' => '', 'core' => '', 'kit' => '', ); $from = 0; foreach ( $cuts as $cut ) { $out['stay'] .= substr( $notices, $from, $cut[0] - $from ); $out[ $cut[2] ] .= substr( $notices, $cut[0], $cut[1] - $cut[0] ); $from = $cut[1]; } $out['stay'] .= substr( $notices, $from ); return $out; } /** * The offset after the closer of the script the scan stands on: the first `span(); $close = stripos( $notices, '', $close ) : false; return false !== $end ? $end + 1 : strlen( $notices ); } /** * Core's tag processor, telling where the tag it stands on starts and ends. * * @param string $html The HTML to scan. */ private static function scanner( string $html ): object { return new class( $html ) extends \WP_HTML_Tag_Processor { /** * The tag's byte offsets: [start, end]. * * @return int[] */ public function span(): array { $this->set_bookmark( 'here' ); $span = $this->bookmarks['here']; return array( $span->start, $span->start + $span->length ); } }; } }