cart && WC()->cart->is_empty(); } public static function enhance( $html ): string { $html = (string) $html; if ( 200 !== http_response_code() || ! str_contains( $html, '' ) ) { return $html; } return self::optimise( $html ); } public static function optimise( string $html ): string { $tags = new \WP_HTML_Tag_Processor( $html ); $tokens = [ 'classes' => [], 'ids' => [], 'tags' => [ 'html' => true, 'body' => true ] ]; $links = []; $styles = []; $hidden = 0; $seen = 0; $nonce = substr( md5( (string) wp_rand() ), 0, 8 ); $mounts = self::mount_attributes(); while ( $tags->next_tag( [ 'tag_closers' => 'visit' ] ) ) { $name = (string) $tags->get_tag(); if ( 'NOSCRIPT' === $name || 'TEMPLATE' === $name ) { $hidden = max( 0, $hidden + ( $tags->is_tag_closer() ? -1 : 1 ) ); } if ( $tags->is_tag_closer() ) { continue; } foreach ( $mounts as $attribute ) { if ( null !== $tags->get_attribute( $attribute ) ) { return $html; } } $tokens['tags'][ strtolower( $name ) ] = true; foreach ( $tags->class_list() as $class ) { $tokens['classes'][ $class ] = true; } $id = $tags->get_attribute( 'id' ); if ( is_string( $id ) && '' !== $id ) { $tokens['ids'][ $id ] = true; } if ( $hidden ) { continue; } if ( 'STYLE' === $name ) { $text = (string) $tags->get_modifiable_text(); $media = strtolower( trim( (string) $tags->get_attribute( 'media' ) ) ); if ( strlen( $text ) >= self::INLINE_MIN && in_array( $media, [ '', 'all', 'screen' ], true ) ) { $styles[ $seen ] = $text; } ++$seen; continue; } if ( 'LINK' !== $name ) { continue; } $link = self::link( $tags ); if ( null === $link ) { continue; } $mark = $nonce . '-' . count( $links ); $tags->remove_attribute( 'href' ); $tags->set_attribute( 'data-rooster-href', $link['href'] ); $tags->set_attribute( 'data-rooster-mark', $mark ); $links[ $mark ] = $link; } if ( ! $links && ! $styles ) { return $html; } foreach ( $tokens as &$set ) { ksort( $set, SORT_STRING ); } unset( $set ); $key = md5( self::VERSION . wp_json_encode( array_map( static fn ( $l ) => [ $l['href'], $l['mtime'] ], array_values( $links ) ) ) . wp_json_encode( array_map( 'md5', $styles ) ) . wp_json_encode( $tokens ) ); $used = self::load( $key ); if ( null === $used ) { $used = [ 'links' => [], 'styles' => [], 'whole' => [] ]; foreach ( $links as $link ) { $index = self::sheet_index( $link['file'], $link['href'] ); $css = null === $index ? null : self::emit( $index, $tokens ); if ( null !== $css ) { $whole = self::emit( $index, null ); if ( strlen( $whole ) - strlen( $css ) < self::WHOLE_UNDER ) { $css = $whole; $used['whole'][ $link['href'] ] = true; } } $used['links'][ $link['href'] ] = $css; } foreach ( $styles as $n => $text ) { $index = self::text_index( $text ); $used['styles'][ $n ] = null === $index ? null : self::emit( $index, $tokens ); } self::store( $key, $used, $styles ); } $html = $tags->get_updated_html(); $cut = array_filter( (array) $used['styles'], static fn ( $css ) => null !== $css ); if ( $cut ) { $tags = new \WP_HTML_Tag_Processor( $html ); $seen = 0; $hidden = 0; while ( $tags->next_tag( [ 'tag_closers' => 'visit' ] ) ) { $name = (string) $tags->get_tag(); if ( 'NOSCRIPT' === $name || 'TEMPLATE' === $name ) { $hidden = max( 0, $hidden + ( $tags->is_tag_closer() ? -1 : 1 ) ); } if ( $tags->is_tag_closer() || $hidden || 'STYLE' !== $name ) { continue; } if ( isset( $cut[ $seen ] ) && $tags->set_modifiable_text( (string) $cut[ $seen ] ) ) { $tags->set_attribute( 'data-rooster-full', $key . ':' . $seen ); } ++$seen; } $html = $tags->get_updated_html(); } $out = ''; $offset = 0; $group = null; $flush = static function () use ( &$out, &$group ): void { if ( null === $group ) { return; } if ( '' !== trim( $group['css'] ) || $group['ids'] ) { $ids = $group['ids'] ? ' data-rooster-ids="' . esc_attr( implode( ' ', $group['ids'] ) ) . '"' : ''; $out .= ''; } $out .= $group['tags']; $group = null; }; foreach ( $links as $mark => $link ) { $attr = ' data-rooster-mark="' . $mark . '"'; $at = strpos( $html, $attr, $offset ); $open = false === $at ? false : strrpos( substr( $html, $offset, $at - $offset ), '<' ); if ( false === $open ) { continue; } $open += $offset; $close = strpos( $html, '>', $at ); $tag = str_replace( $attr, '', substr( $html, $open, $close + 1 - $open ) ); $between = substr( $html, $offset, $open - $offset ); $css = $used['links'][ $link['href'] ] ?? null; $offset = $close + 1; if ( null !== $group && '' !== trim( $between ) ) { $flush(); } if ( null === $css ) { $back = new \WP_HTML_Tag_Processor( $tag ); $back->next_tag(); $back->remove_attribute( 'data-rooster-href' ); $back->set_attribute( 'href', $link['href'] ); $flush(); $out .= $between . $back->get_updated_html(); continue; } if ( null === $group ) { $out .= $between; $group = [ 'css' => '', 'tags' => '', 'ids' => [] ]; } else { $group['tags'] .= $between; } $group['css'] .= $css; if ( empty( $used['whole'][ $link['href'] ] ) ) { $group['tags'] .= $tag; } elseif ( '' !== $link['id'] ) { $group['ids'][] = $link['id']; } } $flush(); $out .= substr( $html, $offset ); $end = strripos( $out, '' ); $js = ''; return false === $end ? $out . $js : substr( $out, 0, $end ) . $js . substr( $out, $end ); } public static function mount_attributes(): array { static $list = null; return $list ??= array_values( array_filter( (array) apply_filters( 'rooster/used_css/mount_attributes', [ 'data-rl-widget' ] ), 'is_string' ) ); } private static function link( \WP_HTML_Tag_Processor $tags ): ?array { $rel = strtolower( (string) $tags->get_attribute( 'rel' ) ); $media = strtolower( trim( (string) $tags->get_attribute( 'media' ) ) ); $href = $tags->get_attribute( 'href' ); $onload = (string) $tags->get_attribute( 'onload' ); $swap = 'print' === $media && str_contains( str_replace( '"', "'", $onload ), "this.media='all'" ); if ( ! is_string( $href ) || '' === $href || ! in_array( 'stylesheet', preg_split( '/\s+/', $rel ) ?: [], true ) || ( '' !== $onload && ! $swap ) || ( ! $swap && ! in_array( $media, [ '', 'all', 'screen' ], true ) ) ) { return null; } $file = self::file( $href ); if ( '' === $file ) { return null; } return [ 'href' => $href, 'file' => $file, 'mtime' => (int) filemtime( $file ), 'id' => (string) $tags->get_attribute( 'id' ) ]; } public static function file( string $url ): string { $url = (string) strtok( html_entity_decode( $url ), '?#' ); $url = set_url_scheme( str_starts_with( $url, '//' ) ? ( is_ssl() ? 'https:' : 'http:' ) . $url : $url ); $map = [ content_url() => WP_CONTENT_DIR, includes_url() => ABSPATH . WPINC, site_url( '/' ) => ABSPATH ]; foreach ( $map as $base => $dir ) { $base = set_url_scheme( $base ); foreach ( [ $base, (string) wp_make_link_relative( $base ) ] as $prefix ) { if ( '' === $prefix || ! str_starts_with( $url, $prefix ) ) { continue; } $relative = rawurldecode( ltrim( substr( $url, strlen( $prefix ) ), '/' ) ); $path = rtrim( $dir, '/\\' ) . '/' . $relative; if ( str_contains( $relative, '..' ) || ! str_ends_with( strtolower( $relative ), '.css' ) || ! is_file( $path ) ) { return ''; } return $path; } } return ''; } public static function filter( string $css, array $tokens, string $url = '' ): ?string { $index = self::index( '' === $url ? $css : self::absolute_urls( $css, $url ) ); return null === $index ? null : self::emit( $index, $tokens ); } private static function sheet_index( string $file, string $url ): ?array { $key = md5( self::VERSION . '|' . $file . '|' . (int) filemtime( $file ) . '|' . strtok( $url, '?#' ) ); return self::cached_index( $key, static fn () => self::index( self::absolute_urls( (string) file_get_contents( $file ), $url ) ) ); } private static function text_index( string $css ): ?array { return self::cached_index( md5( self::VERSION . '|text|' . $css ), static fn () => self::index( $css ) ); } private static function cached_index( string $key, callable $build ): ?array { static $memo = []; if ( array_key_exists( $key, $memo ) ) { return $memo[ $key ]; } $dir = self::dir(); $path = '' === $dir ? '' : $dir . '/sheets/' . $key . '.json'; if ( '' !== $path && is_readable( $path ) ) { $data = json_decode( (string) file_get_contents( $path ), true ); if ( is_array( $data ) ) { return $memo[ $key ] = empty( $data['import'] ) ? (array) $data['nodes'] : null; } } $index = $build(); if ( '' !== $path && wp_mkdir_p( dirname( $path ) ) ) { file_put_contents( $path, (string) wp_json_encode( [ 'import' => null === $index, 'nodes' => $index ?? [] ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), LOCK_EX ); } return $memo[ $key ] = $index; } public static function index( string $css ): ?array { $css = (string) preg_replace( '#/\*.*?\*/#s', '', $css ); if ( preg_match( '/@import\b/i', $css ) ) { return null; } $i = 0; return self::nodes( $css, $i, strlen( $css ) ); } private static function nodes( string $css, int &$i, int $len ): array { $out = []; while ( $i < $len ) { $start = $i; $stop = self::scan( $css, $i, $len ); $head = trim( substr( $css, $start, $i - $start ) ); ++$i; if ( '}' === $stop || '' === $stop ) { return $out; } if ( ';' === $stop ) { if ( '' !== $head && ! preg_match( '/^@charset\b/i', $head ) ) { $out[] = [ 'x', $head . ';' ]; } continue; } if ( str_starts_with( $head, '@' ) ) { $name = strtolower( (string) preg_replace( '/^@(-[a-z]+-)?([a-z-]+).*$/is', '$2', $head ) ); if ( in_array( $name, self::GROUPS, true ) ) { $out[] = [ 'g', $head, self::nodes( $css, $i, $len ), 'layer' === $name ? 1 : 0 ]; } else { $out[] = [ 'x', $head . '{' . self::block( $css, $i, $len ) . '}' ]; } continue; } $out[] = [ 'r', $head . '{' . self::block( $css, $i, $len ) . '}', self::needs( $head ) ]; } return $out; } private static function scan( string $css, int &$i, int $len ): string { $depth = 0; while ( $i < $len ) { $i += strcspn( $css, "\"'\\(){};", $i ); if ( $i >= $len ) { break; } $c = $css[ $i ]; if ( '"' === $c || "'" === $c ) { $i = self::skip_string( $css, $i, $len ); continue; } if ( '\\' === $c ) { $i += 2; continue; } if ( '(' === $c ) { ++$depth; } elseif ( ')' === $c ) { $depth = max( 0, $depth - 1 ); } elseif ( 0 === $depth ) { return $c; } ++$i; } return ''; } private static function block( string $css, int &$i, int $len ): string { $start = $i; $depth = 1; while ( $i < $len ) { $i += strcspn( $css, "\"'\\{}", $i ); if ( $i >= $len ) { break; } $c = $css[ $i ]; if ( '"' === $c || "'" === $c ) { $i = self::skip_string( $css, $i, $len ); continue; } if ( '\\' === $c ) { $i += 2; continue; } if ( '{' === $c ) { ++$depth; } elseif ( 0 === --$depth ) { ++$i; return substr( $css, $start, $i - 1 - $start ); } ++$i; } return substr( $css, $start ); } private static function skip_string( string $css, int $i, int $len ): int { $quote = $css[ $i++ ]; while ( $i < $len ) { $i += strcspn( $css, $quote . '\\', $i ); if ( $i >= $len || $css[ $i ] === $quote ) { break; } $i += 2; } return $i + 1; } private static function needs( string $list ): array { $out = []; $ident = '((?:\\\\[0-9a-fA-F]{1,6}\s?|\\\\.|[\w-]|[^\x00-\x7F])+)'; $all = []; foreach ( self::split( $list ) as $selector ) { $alternatives = self::expand( $selector ); if ( null === $alternatives ) { return []; } array_push( $all, ...$alternatives ); } foreach ( $all as $selector ) { $s = self::strip_functions( $selector ); $s = (string) preg_replace( '/\[(?:[^\]"\']|"[^"]*"|\'[^\']*\')*\]/', '', $s ); $s = (string) preg_replace( '/(?+~|]+/', trim( $s ) ) ?: [] as $compound ) { if ( preg_match( '/^([a-zA-Z][a-zA-Z0-9-]*)/', $compound, $tag ) ) { $tags[] = strtolower( $tag[1] ); } } if ( ! $classes && ! $ids && ! $tags ) { return []; } $out[] = [ array_values( array_unique( $classes ) ), array_values( array_unique( $ids ) ), array_values( array_unique( $tags ) ) ]; } return $out; } private static function expand( string $selector ): ?array { $done = []; $todo = [ $selector ]; while ( $todo ) { $s = array_pop( $todo ); $at = self::forgiving_at( $s ); if ( null === $at ) { $done[] = $s; } else { [ $open, $close ] = $at; $head = substr( $s, 0, $open[0] ); $tail = substr( $s, $close + 1 ); foreach ( self::split( substr( $s, $open[1], $close - $open[1] ) ) as $argument ) { $todo[] = $head . ' ' . trim( $argument ) . ' ' . $tail; } } if ( count( $done ) + count( $todo ) > self::MAX_ALTERNATIVES ) { return null; } } return $done; } private static function forgiving_at( string $selector ): ?array { if ( false === stripos( $selector, 'is(' ) && false === stripos( $selector, 'where(' ) && false === stripos( $selector, 'any(' ) && false === stripos( $selector, 'matches(' ) ) { return null; } $len = strlen( $selector ); $depth = 0; for ( $i = 0; $i < $len; $i++ ) { $c = $selector[ $i ]; if ( '\\' === $c ) { ++$i; } elseif ( '"' === $c || "'" === $c ) { $i = self::skip_string( $selector, $i, $len ) - 1; } elseif ( '(' === $c || '[' === $c ) { ++$depth; } elseif ( ( ')' === $c || ']' === $c ) && $depth > 0 ) { --$depth; } elseif ( ':' === $c && 0 === $depth && preg_match( '/\G:(?:is|where|matches|-webkit-any|-moz-any)\(/i', $selector, $m, 0, $i ) ) { $start = $i + strlen( $m[0] ); $inner = 1; for ( $j = $start; $j < $len; $j++ ) { $d = $selector[ $j ]; if ( '\\' === $d ) { ++$j; } elseif ( '"' === $d || "'" === $d ) { $j = self::skip_string( $selector, $j, $len ) - 1; } elseif ( '(' === $d ) { ++$inner; } elseif ( ')' === $d && 0 === --$inner ) { return [ [ $i, $start ], $j ]; } } return null; } } return null; } private static function split( string $list ): array { $parts = []; $depth = 0; $start = 0; $len = strlen( $list ); for ( $i = 0; $i < $len; $i++ ) { $c = $list[ $i ]; if ( '"' === $c || "'" === $c ) { $i = self::skip_string( $list, $i, $len ) - 1; } elseif ( '\\' === $c ) { ++$i; } elseif ( '(' === $c || '[' === $c ) { ++$depth; } elseif ( ( ')' === $c || ']' === $c ) && $depth > 0 ) { --$depth; } elseif ( ',' === $c && 0 === $depth ) { $parts[] = substr( $list, $start, $i - $start ); $start = $i + 1; } } $parts[] = substr( $list, $start ); return $parts; } private static function strip_functions( string $selector ): string { if ( ! str_contains( $selector, '(' ) ) { return $selector; } $out = ''; $len = strlen( $selector ); for ( $i = 0; $i < $len; $i++ ) { if ( '\\' === $selector[ $i ] ) { $out .= substr( $selector, $i, 2 ); ++$i; continue; } if ( ':' === $selector[ $i ] && preg_match( '/\G::?[a-zA-Z-]+\(/', $selector, $m, 0, $i ) ) { $depth = 0; for ( $j = $i + strlen( $m[0] ) - 1; $j < $len; $j++ ) { if ( '(' === $selector[ $j ] ) { ++$depth; } elseif ( ')' === $selector[ $j ] && 0 === --$depth ) { break; } } $i = $j; continue; } $out .= $selector[ $i ]; } return $out; } private static function unescape( string $ident ): string { $ident = (string) preg_replace_callback( '/\\\\([0-9a-fA-F]{1,6})\s?/', static fn ( $m ) => mb_chr( (int) hexdec( $m[1] ), 'UTF-8' ), $ident ); return (string) preg_replace( '/\\\\(.)/u', '$1', $ident ); } private static function emit( array $nodes, ?array $tokens ): string { $out = ''; foreach ( $nodes as $node ) { if ( 'x' === $node[0] ) { $out .= $node[1]; } elseif ( 'g' === $node[0] ) { $inner = self::emit( (array) $node[2], $tokens ); if ( '' !== $inner || ! empty( $node[3] ) ) { $out .= $node[1] . '{' . $inner . '}'; } } elseif ( null === $tokens || self::needed( (array) $node[2], $tokens ) ) { $out .= $node[1]; } } return $out; } private static function needed( array $needs, array $tokens ): bool { if ( ! $needs ) { return true; } foreach ( $needs as [ $classes, $ids, $tags ] ) { foreach ( $classes as $class ) { if ( ! isset( $tokens['classes'][ $class ] ) && ! self::safe( $class ) ) { continue 2; } } foreach ( $ids as $id ) { if ( ! isset( $tokens['ids'][ $id ] ) ) { continue 2; } } foreach ( $tags as $tag ) { if ( ! isset( $tokens['tags'][ $tag ] ) ) { continue 2; } } return true; } return false; } private static function safe( string $class ): bool { static $seen = []; if ( ! isset( $seen[ $class ] ) ) { $seen[ $class ] = false; foreach ( self::safelist() as $pattern ) { if ( preg_match( $pattern, $class ) ) { $seen[ $class ] = true; break; } } } return $seen[ $class ]; } public static function safelist(): array { static $list = null; return $list ??= array_values( (array) apply_filters( 'rooster/used_css/safelist', [ '/^is-(open|opened|active|selected|expanded|current|visible|hidden|loading|loaded|ready|copied|menu-open|dialog-open|modal-open|closing|scrolled|sticky|dragging)$/', '/^has-(modal-open|child-open|focus)$/', '/^(open|active|show|visible|rooster-human|rooster-defer-plain)$/', ] ) ); } public static function absolute_urls( string $css, string $sheet_url ): string { $sheet = (string) strtok( html_entity_decode( $sheet_url ), '?#' ); $dir = substr( $sheet, 0, (int) strrpos( $sheet, '/' ) + 1 ); return (string) preg_replace_callback( '/url\(\s*([\'"]?)(?![a-z]+:|\/|#|%23)([^\'")]+)\1\s*\)/i', static function ( $m ) use ( $dir ) { $path = $dir . $m[2]; $parts = []; $url = (string) preg_replace( '#^(https?:)?//[^/]+#', '', $path ); $root = substr( $path, 0, strlen( $path ) - strlen( $url ) ); foreach ( explode( '/', $url ) as $segment ) { if ( '..' === $segment ) { array_pop( $parts ); } elseif ( '.' !== $segment ) { $parts[] = $segment; } } return 'url(' . $m[1] . $root . implode( '/', $parts ) . $m[1] . ')'; }, $css ); } private static function dir(): string { $uploads = wp_upload_dir( null, false ); return empty( $uploads['basedir'] ) ? '' : $uploads['basedir'] . '/rooster/used-css'; } private static function url(): string { $uploads = wp_upload_dir( null, false ); return empty( $uploads['baseurl'] ) ? '' : trailingslashit( set_url_scheme( $uploads['baseurl'] ) ) . 'rooster/used-css/'; } private static function load( string $key ): ?array { $dir = self::dir(); if ( '' === $dir || ! is_readable( $dir . '/' . $key . '.json' ) ) { return null; } $file = $dir . '/' . $key . '.json'; $data = json_decode( (string) file_get_contents( $file ), true ); if ( ! is_array( $data ) || ! isset( $data['links'], $data['styles'] ) ) { return null; } if ( (int) filemtime( $file ) < time() - DAY_IN_SECONDS ) { touch( $file ); if ( is_file( $dir . '/' . $key . '-full.json' ) ) { touch( $dir . '/' . $key . '-full.json' ); } } return $data; } private static function store( string $key, array &$used, array $styles ): void { $dir = self::dir(); $cut = array_intersect_key( $styles, array_filter( $used['styles'], static fn ( $css ) => null !== $css ) ); $flags = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; if ( '' === $dir || ! wp_mkdir_p( $dir ) || ! self::room( $dir ) || ( $cut && false === file_put_contents( $dir . '/' . $key . '-full.json', (string) wp_json_encode( (object) $cut, $flags ), LOCK_EX ) ) ) { $used['styles'] = []; return; } file_put_contents( $dir . '/' . $key . '.json', (string) wp_json_encode( $used, $flags ), LOCK_EX ); } private static function room( string $dir ): bool { $files = (array) glob( $dir . '/*.json' ); if ( count( $files ) < self::MAX_FILES ) { return true; } $week = time() - WEEK_IN_SECONDS; foreach ( $files as $file ) { if ( (int) filemtime( (string) $file ) < $week ) { wp_delete_file( (string) $file ); } } return count( (array) glob( $dir . '/*.json' ) ) < self::MAX_FILES; } public static function clear(): void { $dir = self::dir(); foreach ( '' === $dir ? [] : array_merge( (array) glob( $dir . '/*.json' ), (array) glob( $dir . '/sheets/*.json' ) ) as $file ) { wp_delete_file( (string) $file ); } } private static function script(): string { static $js = null; $file = AIEO_ENGINE_DIR . ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '/assets/js/inline/used-css.js' : '/assets/js/inline/used-css.min.js' ); return $js ??= trim( (string) file_get_contents( $file ) ); } }