hash( 'md5', $this->extract_stylesheet_links( $html ) ), 'inline' => hash( 'md5', $this->extract_inline_styles( $html ) ), 'blocks' => hash( 'md5', $this->extract_block_structure( $html ) ), 'structure' => hash( 'md5', $this->extract_structural_elements( $html ) ), ]; return implode( '|', array_map( static fn( string $key, string $hash ): string => "$key:$hash", array_keys( $components ), $components ) ); } /** * Compare two composite hash strings and return which components changed. * * @param string $old_hash The previous composite hash. * @param string $new_hash The new composite hash. * * @return string[] List of component names that changed. */ public function get_changed_components( string $old_hash, string $new_hash ): array { if ( $old_hash === $new_hash ) { return []; } $old_parts = $this->parse_composite_hash( $old_hash ); $new_parts = $this->parse_composite_hash( $new_hash ); $changed = []; foreach ( $new_parts as $key => $new_value ) { if ( ! isset( $old_parts[ $key ] ) || $old_parts[ $key ] !== $new_value ) { $changed[] = $key; } } return $changed; } /** * Parse a composite hash string into components. * * @param string $composite_hash The composite hash string. * * @return array Associative array of component => hash. */ private function parse_composite_hash( string $composite_hash ): array { $parts = explode( '|', $composite_hash ); $result = []; foreach ( $parts as $part ) { $split = explode( ':', $part, 2 ); if ( count( $split ) === 2 ) { $result[ $split[0] ] = $split[1]; } } return $result; } /** * Extract stylesheet and preload link tags. * * @param string $html The HTML content. * * @return string Concatenated link tags. */ private function extract_stylesheet_links( string $html ): string { preg_match_all( '~]*\brel=["\'](?:stylesheet|preload)["\'][^>]*>~i', $html, $matches ); return implode( '', $matches[0] ); } /** * Extract inline style tags. * * @param string $html The HTML content. * * @return string Concatenated style tags. */ private function extract_inline_styles( string $html ): string { preg_match_all( '~]*>.*?~is', $html, $matches ); return implode( '', $matches[0] ); } /** * Extract block structures. * * @param string $html The HTML content. * * @return string Pipe-separated block comments. */ private function extract_block_structure( string $html ): string { $matches = []; preg_match_all( '~~i', $html, $matches ); return implode( '|', $matches[0] ?? [] ); } /** * Extract structural element counts. * * @param string $html The HTML content. * * @return string Pipe-separated element counts. */ private function extract_structural_elements( string $html ): string { $matches = []; preg_match_all( '~<(div|section|article|header|footer|main)[^>]*>~i', $html, $matches ); $counts = array_count_values( array_map( 'strtolower', $matches[1] ?? [] ) ); return implode( '|', array_map( static fn( string $element ): string => "$element:" . ( $counts[ $element ] ?? 0 ), [ 'div', 'section', 'article', 'header', 'footer', 'main' ] ) ); } }