0 ) { continue; } if ( '{' === $c || ';' === $c || '}' === $c ) { break; } } $prelude = trim( substr( $css, $start, $j - $start ) ); if ( $j >= $len ) { break; } $c = $css[ $j ]; $i = $j + 1; if ( '}' === $c ) { return $nodes; } if ( ';' === $c ) { if ( '' !== $prelude ) { $nodes[] = array( 'type' => 'stmt', 'text' => $prelude . ';', ); } continue; } if ( '' !== $prelude && '@' === $prelude[0] ) { $name = strtolower( preg_replace( '/^@([a-zA-Z-]+).*/s', '$1', $prelude ) ); if ( in_array( $name, array( 'media', 'supports', 'layer', 'container', 'document', '-moz-document' ), true ) ) { $nodes[] = array( 'type' => 'at', 'name' => $name, 'prelude' => $prelude, 'children' => self::parse_block( $css, $i, $len ), ); } else { $body = self::raw_block( $css, $i, $len ); $nodes[] = array( 'type' => 'raw', 'name' => $name, 'prelude' => $prelude, 'text' => $prelude . '{' . $body . '}', ); } } else { $body = self::raw_block( $css, $i, $len ); $nodes[] = array( 'type' => 'rule', 'sel' => $prelude, 'body' => $body, ); } } return $nodes; } private static function raw_block( $css, &$i, $len ) { $depth = 1; $start = $i; $in = false; $q = ''; for ( ; $i < $len; $i++ ) { $c = $css[ $i ]; if ( $in ) { if ( '\\' === $c ) { ++$i; continue; } if ( $c === $q ) { $in = false; } continue; } if ( '"' === $c || "'" === $c ) { $in = true; $q = $c; continue; } if ( '{' === $c ) { ++$depth; } elseif ( '}' === $c ) { --$depth; if ( 0 === $depth ) { $inner = substr( $css, $start, $i - $start ); ++$i; return $inner; } } } return substr( $css, $start ); } public static function render( $nodes ) { $out = ''; foreach ( $nodes as $n ) { switch ( $n['type'] ) { case 'rule': $out .= $n['sel'] . '{' . trim( $n['body'] ) . '}'; break; case 'at': $inner = self::render( $n['children'] ); if ( '' !== $inner ) { $out .= $n['prelude'] . '{' . $inner . '}'; } break; case 'raw': $out .= $n['text']; break; case 'stmt': $out .= $n['text']; break; } } return $out; } /* ---------- HTML index ---------- */ public static function index( $html ) { $idx = array( 'tags' => array( 'html' => 1, 'body' => 1, ), 'classes' => array(), 'ids' => array(), 'attrs' => array(), ); if ( ! preg_match_all( '/<([a-zA-Z][\w-]*)([^>]*)>/', $html, $m, PREG_SET_ORDER ) ) { return $idx; } foreach ( $m as $t ) { $idx['tags'][ strtolower( $t[1] ) ] = 1; if ( '' === trim( $t[2] ) ) { continue; } if ( ! preg_match_all( '/\s([a-zA-Z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s"\'>]+)))?/', $t[2], $am, PREG_SET_ORDER ) ) { continue; } foreach ( $am as $a ) { $name = strtolower( $a[1] ); $idx['attrs'][ $name ] = 1; $val = isset( $a[2] ) && '' !== $a[2] ? $a[2] : ( isset( $a[3] ) && '' !== $a[3] ? $a[3] : ( isset( $a[4] ) ? $a[4] : '' ) ); if ( 'class' === $name ) { foreach ( preg_split( '/\s+/', trim( html_entity_decode( $val ) ) ) as $c ) { if ( '' !== $c ) { $idx['classes'][ $c ] = 1; } } } elseif ( 'id' === $name ) { $val = trim( $val ); if ( '' !== $val ) { $idx['ids'][ $val ] = 1; } } } } // Class names that scripts toggle at runtime ("classList.add('open')", "addClass('is-active')"). // Also catches aliases such as "var b = document.body.classList; b.add('x')" (extra classes only keep more CSS). if ( preg_match_all( '/(?:\.(?:add|toggle|replace)|addClass|toggleClass)\(\s*[\'"]([^\'"]+)[\'"]/', $html, $jm ) ) { foreach ( $jm[1] as $list ) { foreach ( preg_split( '/\s+/', trim( $list ) ) as $c ) { if ( '' !== $c ) { $idx['classes'][ $c ] = 1; } } } } return $idx; } /* ---------- selector matching ---------- */ private static function split_top( $s, $sep ) { if ( false === strpbrk( $s, '([\\' ) ) { return explode( $sep, $s ); } $out = array(); $depth = 0; $cur = ''; $len = strlen( $s ); for ( $i = 0; $i < $len; $i++ ) { $c = $s[ $i ]; if ( '\\' === $c ) { $cur .= $c . ( $i + 1 < $len ? $s[ ++$i ] : '' ); continue; } if ( '(' === $c || '[' === $c ) { ++$depth; } elseif ( ')' === $c || ']' === $c ) { --$depth; } if ( $c === $sep && 0 === $depth ) { $out[] = $cur; $cur = ''; continue; } $cur .= $c; } $out[] = $cur; return $out; } public static function selector_used( $sel, $idx, $safelist ) { if ( ! $safelist && false === strpos( $sel, ',' ) ) { return self::single_used( trim( $sel ), $idx ); } foreach ( $safelist as $s ) { if ( '' !== $s && false !== stripos( $sel, $s ) ) { return true; } } foreach ( self::split_top( $sel, ',' ) as $single ) { if ( self::single_used( trim( $single ), $idx ) ) { return true; } } return false; } private static function single_used( $s, $idx ) { if ( '' === $s ) { return true; } // Fast path for plain selectors (tags, classes, ids, combinators): no regex replacements needed. if ( strpbrk( $s, ':[\\(' ) === false ) { foreach ( preg_split( '/[\s>~+]+/', $s, -1, PREG_SPLIT_NO_EMPTY ) as $comp ) { $parts = preg_split( '/(?=[.#])/', $comp, -1, PREG_SPLIT_NO_EMPTY ); foreach ( $parts as $k => $x ) { if ( '*' === $x ) { continue; } if ( '.' === $x[0] ) { if ( ! isset( $idx['classes'][ substr( $x, 1 ) ] ) ) { return false; } } elseif ( '#' === $x[0] ) { if ( ! isset( $idx['ids'][ substr( $x, 1 ) ] ) ) { return false; } } elseif ( 0 === $k && ! isset( $idx['tags'][ strtolower( $x ) ] ) ) { if ( ! preg_match( '/^[a-zA-Z][\w-]*$/', $x ) ) { return true; // Unknown syntax: keep. } return false; } } } return true; } if ( preg_match( '/\\\\[0-9a-fA-F]/', $s ) ) { return true; // Hex escapes: cannot resolve safely, keep. } // Drop pseudo-classes / pseudo-elements, with or without parentheses (escaped colons are class chars). if ( false !== strpos( $s, '(' ) ) { $s = preg_replace( '/(?~+]\s*|\s+/', ' ', $s ) ); if ( '' === $s ) { return true; } foreach ( explode( ' ', $s ) as $comp ) { if ( '' === $comp || '*' === $comp ) { continue; } if ( ! preg_match( '/^([a-zA-Z][\w-]*|\*)?((?:[.#][^.#\s]+)*)$/', $comp, $m ) ) { return true; // Unparsable: keep. } $tag = strtolower( $m[1] ); if ( '' !== $tag && '*' !== $tag && ! isset( $idx['tags'][ $tag ] ) ) { return false; } if ( '' !== $m[2] && preg_match_all( '/([.#])([^.#]+)/', $m[2], $mm, PREG_SET_ORDER ) ) { foreach ( $mm as $x ) { if ( '.' === $x[1] ) { if ( ! isset( $idx['classes'][ $x[2] ] ) ) { return false; } } elseif ( ! isset( $idx['ids'][ $x[2] ] ) ) { return false; } } } } return true; } /* ---------- filtering ---------- */ /** Selector parts that only style an interaction state (not visible on first paint). */ private static function interaction_only( $single ) { if ( false === strpos( $single, ':' ) ) { return false; } return (bool) preg_match( '/(?]*\bclass\s*=\s*(["\'])(.*?)\1/is', $html, $m ) ) { $classes = array_filter( preg_split( '/\s+/', strtolower( trim( $m[2] ) ) ), function ( $c ) { return '' !== $c && ! preg_match( '/\d/', $c ); } ); sort( $classes ); return 't:' . implode( ' ', $classes ); } $uri = isset( $_SERVER['REQUEST_URI'] ) ? (string) wp_parse_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), PHP_URL_PATH ) : '/'; return 'u:' . $uri; } public static function build( $sheets, $html, $safelist, $mode ) { $sig = $mode . '|' . implode( '|', $safelist ); foreach ( $sheets as $s ) { $sig .= '|' . $s[0] . filemtime( $s[0] ); } $key = md5( $sig . '|' . self::page_key( $html, $mode ) . '|' . ( Velox_Cache::is_mobile_ua() ? 'm' : 'd' ) . VELOX_VERSION ); $file = self::dir() . 'r-' . $key . '.css'; if ( is_file( $file ) && time() - filemtime( $file ) < DAY_IN_SECONDS ) { $css = file_get_contents( $file ); if ( false !== $css ) { return $css; } } $idx = self::index( $html ); $nodes = array(); foreach ( $sheets as $s ) { $nodes = array_merge( $nodes, self::nodes_for_file( $s[0], $s[1] ) ); } $css = self::reduce( $nodes, $idx, $safelist, 'critical' === $mode ); wp_mkdir_p( self::dir() ); @file_put_contents( $file, $css, LOCK_EX ); return $css; } /* ---------- per page / template record: the CSS that wp_add_inline_style() prints ---------- */ /** Stored record, or null when missing, older than a day or built from changed stylesheets. */ public static function load_record( $key ) { $file = self::dir() . 's-' . $key . '.json'; if ( ! is_file( $file ) || time() - filemtime( $file ) > DAY_IN_SECONDS ) { return null; } $rec = json_decode( (string) file_get_contents( $file ), true ); if ( ! is_array( $rec ) || empty( $rec['css'] ) || empty( $rec['handles'] ) || ! in_array( $rec['mode'] ?? '', array( 'critical', 'used' ), true ) ) { return null; } foreach ( (array) ( $rec['files'] ?? array() ) as $f ) { if ( ! is_array( $f ) || ! is_file( (string) $f[0] ) || filemtime( (string) $f[0] ) !== (int) $f[1] ) { return null; } } return $rec; } public static function save_record( $key, $rec ) { wp_mkdir_p( self::dir() ); @file_put_contents( self::dir() . 's-' . $key . '.json', wp_json_encode( $rec ), LOCK_EX ); } public static function purge() { if ( is_dir( self::dir() ) ) { foreach ( array_merge( glob( self::dir() . 'r-*.css' ) ?: array(), glob( self::dir() . 's-*.json' ) ?: array(), glob( self::dir() . 'p-*.php' ) ?: array() ) as $f ) { wp_delete_file( $f ); } } } }