site_url = $site_url;
}
function forceInclude($selectors) {
self::$forceInclude = array_merge(self::$forceInclude, $selectors);
}
function process_css($html)
{
// self::$html = $this->cleanHtml($html);
self::$html = $html;
$selectors = [];
// $this->js_classes = $this->get_js_selector_classes($html);
$dom = HtmlDomParser::str_get_html($html);
$used_fonts = [];
$used_font_weights = [
'normal' => 1,
'400' => 1,
];
$used_css = '';
foreach ($dom->find('style') as $style) {
$style->setAttribute('data-berqwp-critical-css', '1');
}
// mark stylesheet links
foreach ($dom->find('link') as $link) {
$rel = strtolower($link->rel ?? '');
$as = strtolower($link->as ?? '');
if ($rel === 'stylesheet' || $as === 'style') {
$link->setAttribute('data-berqwp-critical-css', '1');
}
}
$nodes = $dom->find('[data-berqwp-critical-css="1"]');
foreach ($nodes as $css_node) {
$local_css = '';
if ($css_node->tag == 'style') {
$local_css = "@media all { ".$css_node->innertext." } ";
}
if ($css_node->tag == 'link') {
$href = $css_node->href ?? '';
$media = $css_node->media ?? '';
$onload = $css_node->onload ?? '';
if ($onload === "this.media='all'") {
$media = 'all';
}
if ($onload === "this.media='all';this.onload=null;") {
$media = 'all';
}
if ($media === 'print') {
continue;
}
if (!self::is_valid_asset($href)) {
continue;
}
// convert to abs
$baseUrl = self::getBaseUrl($this->site_url);
$href = self::rel2abs($href, $baseUrl);
$response = self::fetch($href);
if ($response === false) {
continue;
}
$local_css .= $response;
}
if (!empty($local_css)) {
$matches = self::extractUrlsFromCss($local_css);
foreach ($matches as $url) {
$url = trim($url);
$baseUrl = self::getBaseUrl($this->site_url);
if ($css_node->tag === 'link') {
$baseUrl = $href;
}
if (!empty($url) && self::is_valid_asset($url)) {
$local_css = $this->replace_url($url, self::rel2abs($url, $baseUrl), $local_css);
}
}
if (!empty($media) && $media !== 'screen') {
$local_css = "@media $media {{$local_css}}";
}
$this->combine_css .= $local_css;
$css = $this->extract_used_css($local_css, $selectors);
$css = $this->minify_css($css);
$css = preg_replace('/@media[^{]+\{\s*\}/i', '', $css);
$css = preg_replace('/@@+/', '', $css);
$used_css .= $css;
preg_match_all("/font-family\s*:\s*([^!;}]+)/", $css, $mf);
preg_match_all("/font-weight\s*:\s*([^!;}]+)/", $css, $mw);
if (!empty($mf)) {
foreach ($mf[1] as $ff) {
if (strpos($ff, ',') !== false) {
foreach (explode(',', $ff) as $fm) {
$fm = trim($fm, " \t\n\r\0\x0B'\"");
$used_fonts[$fm] = 1;
}
} else {
$fm = trim($ff, " \t\n\r\0\x0B'\"");
$used_fonts[$fm] = 1;
}
}
foreach ($mw[1] as $fw) {
$weight = trim($fw, " \t\n\r\0\x0B'\"");
$used_font_weights[$weight] = 1;
}
}
// Insert style tag before the link tag
$style_tag = '';
$css_node->outertext = $style_tag . $css_node->outertext;
}
}
$font_faces = $this->used_font_faces($used_fonts, $used_font_weights);
$font_faces = str_replace('font-display:block;', 'font-display:swap;', $font_faces);
$font_faces = str_replace('font-style:', 'font-display:swap;font-style:', $font_faces);
// $used_keyframes = $this->used_keyframes($used_css);
// $used_varirables = $this->extract_used_variables($used_css);
// if (!empty($used_varirables)) {
// $used_varirables = ':root{' . implode(';', $used_varirables) . '}';
// } else {
// $used_varirables = '';
// }
return $used_css . $font_faces;
}
function has_cache($url)
{
$path = WP_CONTENT_DIR . '/cache/berqwp/external/';
if (!is_dir($path)) {
if (!wp_mkdir_p($path)) {
return false;
}
}
$file_name = md5($url) . '.gz';
$file_path = $path . $file_name;
// return is_file($file_path) && !empty(@filemtime($file_path)) && @filemtime($file_path) > (time() - DAY_IN_SECONDS);
return is_file($file_path) && !empty(@filemtime($file_path)) && @filemtime($file_path) > (time() - DAY_IN_SECONDS);
// return is_file($file_path);
}
function cache_asset($url, $content)
{
$path = WP_CONTENT_DIR . '/cache/berqwp/external/';
if (!is_dir($path)) {
if (!wp_mkdir_p($path)) {
return false;
}
}
$file_name = md5($url) . '.gz';
return file_put_contents($path . $file_name, gzencode($content, 9), LOCK_EX);
}
function cache_miss($url)
{
$path = WP_CONTENT_DIR . '/cache/berqwp/external/';
if (!is_dir($path)) {
if (!wp_mkdir_p($path)) return false;
}
return file_put_contents($path . md5($url) . '.gz', gzencode('berqwp-404', 9), LOCK_EX);
}
function is_cached_miss($url)
{
if (!$this->has_cache($url)) return false;
return $this->get_cache($url) === 'berqwp-404';
}
function get_cache($url)
{
$path = WP_CONTENT_DIR . '/cache/berqwp/external/';
if (!is_dir($path)) {
if (!wp_mkdir_p($path)) {
return false;
}
}
$file_name = md5($url) . '.gz';
return gzdecode(file_get_contents($path . $file_name));
}
function fetch($url)
{
$local_css = '';
if ($this->is_self_hosted_url($url)) {
if ($this->is_cached_miss($url)) {
return false;
}
$local_path = $this->url_to_path($url);
if ($local_path && is_file($local_path)) {
return file_get_contents($local_path);
}
$this->cache_miss($url);
return false;
}
if ($this->is_cached_miss($url)) {
return false;
}
if ($this->has_cache($url)) {
$local_css = $this->get_cache($url);
} else {
$response = wp_remote_get($url, ['timeout' => 30]);
$is_valid_response = wp_remote_retrieve_response_code($response) >= 200 && wp_remote_retrieve_response_code($response) < 400;
if (is_wp_error($response) || !$is_valid_response) {
$this->cache_miss($url);
return false;
}
$local_css = wp_remote_retrieve_body($response);
// preg_match_all("/@import\s+url\(([^)]+)[^;]+;/", $local_css, $matches);
preg_match_all('/@import\s+(?:url\s*\(\s*[\'"]?([^\'")]+)[\'"]?\s*\)|[\'"]([^\'";]+)[\'"])/', $local_css, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $import_css_url) {
$import_css_url = trim($import_css_url, " \t\n\r\0\x0B'\"");
$baseUrl = self::getBaseUrl($url);
$import_css_url = self::rel2abs($import_css_url, $baseUrl);
if (!self::is_valid_asset($import_css_url)) {
continue;
}
$sub_response = self::fetch($import_css_url);
if ($sub_response === false) {
continue;
}
$url_matches = self::extractUrlsFromCss($sub_response);
foreach ($url_matches as $url_func) {
$url_func = trim($url_func);
if (!empty($url_func)) {
// $sub_response = str_replace($url_func, self::rel2abs($url_func, $import_css_url), $sub_response);
$sub_response = $this->replace_url($url_func, self::rel2abs($url_func, $import_css_url), $sub_response);
}
}
$local_css .= $sub_response;
}
}
$this->cache_asset($url, $local_css);
}
return $local_css;
}
function replace_url($search, $replace, $content)
{
// match with single quotes
if (strpos($content, "'$search'") !== false) {
$content = str_replace("'$search'", "'$replace'", $content);
}
// match double quotes
if (strpos($content, "\"$search\"") !== false) {
$content = str_replace("\"$search\"", "\"$replace\"", $content);
}
// match brakets
if (strpos($content, "($search)") !== false) {
$content = str_replace("($search)", "($replace)", $content);
}
// match spaces
if (strpos($content, " $search ") !== false) {
$content = str_replace(" $search ", " $replace ", $content);
}
return $content;
}
static function is_valid_asset($url)
{
return strpos($url, 'data:') === false;
}
public static function rel2abs($rel, $base)
{
/* return if already absolute URL */
if (parse_url($rel, PHP_URL_SCHEME) != '')
return $rel;
// Return with base scheme if protocol-relative (starts with //)
if (strpos($rel, '//') === 0) {
$scheme = parse_url($base, PHP_URL_SCHEME) ?: 'https';
return $scheme . ':' . $rel;
}
/* queries and anchors */
if ($rel[0] == '#' || $rel[0] == '?')
return $base . $rel;
/* parse base URL and convert to local variables:
$scheme, $host, $path */
extract(parse_url($base));
/* remove non-directory element from path */
$path = preg_replace('#/[^/]*$#', '', $path);
/* destroy path if relative url points to root */
if ($rel[0] == '/')
$path = '';
/* dirty absolute URL */
$abs = "$host$path/$rel";
/* replace '//' or '/./' or '/foo/../' with '/' */
$re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#');
for ($n = 1; $n > 0; $abs = preg_replace($re, '/', $abs, -1, $n)) {
}
/* absolute URL is ready! */
return $scheme . '://' . $abs;
}
public static function getBaseUrl($fileUrl)
{
// Parse the URL and get its components
$parsedUrl = parse_url($fileUrl);
$scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
$host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
$path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
// Remove the file name from the path to get the base URL
$basePath = preg_replace('/\/[^\/]+$/', '/', $path);
// Construct the base URL
return $scheme . $host . $basePath;
}
public static function extractUrlsFromCss($cssContent)
{
$urls = [];
$pattern = '/url\((.*?)\)/i';
preg_match_all($pattern, $cssContent, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $match) {
$urls[] = trim($match, '\'" ');
}
}
return $urls;
}
function is_self_hosted_url($url)
{
if (empty($url)) {
return false;
}
// Relative URLs are self-hosted
if (strpos($url, '//') === false) {
return true;
}
$site_host = wp_parse_url(site_url(), PHP_URL_HOST);
$url_host = wp_parse_url($url, PHP_URL_HOST);
return $site_host && $url_host && strtolower($site_host) === strtolower($url_host);
}
function url_to_path($url)
{
if (strpos($url, '?') !== false) {
$url = explode('?', $url)[0];
}
// Handle relative URLs
if (strpos($url, '//') === 0) {
return realpath(ABSPATH . ltrim($url, '/'));
}
$content_url = content_url();
$content_dir = WP_CONTENT_DIR;
if (strpos($url, $content_url) === 0) {
// var_dump(realpath(
// str_replace($content_url, $content_dir, $url)
// ));
return str_replace($content_url, $content_dir, $url);
}
// Fallback for site root files
$site_url = get_site_url('/');
if (strpos($url, $site_url) === 0) {
return realpath(
str_replace($site_url, ABSPATH, $url)
);
}
return false;
}
function used_font_faces($used_fonts, $used_font_weights)
{
preg_match_all("/@font-face\s*{[^}]*}/", $this->combine_css, $matches);
$fonts = $matches[0];
$all_fonts = [];
$used_font_faces = '';
foreach ($fonts as $font) {
preg_match("/font-family\s*:\s*([^;}]+)/", $font, $mf);
preg_match("/font-weight\s*:\s*([^;}]+)/", $font, $mw);
$family = trim($mf[1], " \t\n\r\0\x0B'\"");
$weight = trim($mw[1], " \t\n\r\0\x0B'\"");
if (!empty($used_fonts[$family]) && !empty($used_font_weights[$weight])) {
$used_font_faces .= $font;
}
unset($font);
}
return $used_font_faces;
}
function used_keyframes($used_css)
{
preg_match_all('/animation\s*:\s*([^\s!;}]+)/', $used_css, $matches);
if (empty($matches[1])) {
return '';
}
$key_frame_css = '';
$used_keyframe_names = [];
foreach ($matches[1] as $animation_name) {
$animation_name = trim($animation_name);
$used_keyframe_names[$animation_name] = 1;
}
$captured = 0;
preg_match_all('/@keyframes\s+[\w-]+\s*\{(?:[^{}]*|\{[^{}]*\})*\}/', $this->combine_css, $keyframe_matches);
if (!empty($keyframe_matches)) {
foreach ($keyframe_matches[0] as $keyframe) {
// if ($captured === count($used_keyframe_names)) {
// break;
// }
preg_match('/@keyframes\s+([^\s{]+)/', $keyframe, $kf_name_match);
if (isset($used_keyframe_names[$kf_name_match[1]])) {
$key_frame_css .= $keyframe;
$captured++;
}
}
}
return $key_frame_css;
}
function extract_used_variables($used_css)
{
// preg_match_all("/var\s*\(\s*([^\s)]+)/", $used_css, $matches);
preg_match_all("/var\s*\(\s*([^)]+)/", $used_css, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $index => $value) {
if (strpos($value, ',') !== false) {
foreach (explode(',', $value) as $new_dec) {
$matches[1][] = trim($new_dec);
}
unset($matches[1][$index]);
}
}
return $this->extract_variable_declarations($matches[1]);
}
}
function extract_variable_declarations($variables)
{
$variables = array_map(function ($item) {
return trim($item);
}, $variables);
$variables = array_unique($variables);
$variable_proerties = [];
// preg_match_all("/:root\s*{([^;}]+)/", $this->combine_css, $matches);
preg_match_all("/:root\s*{\s*([^}]+)/", $this->combine_css, $matches);
// collected nested var() in :root
foreach ($matches[1] as $properties) {
$prop_arr = explode(';', $properties);
$prop_arr = array_filter($prop_arr, function ($property) use ($properties, &$variables) {
$property_name = explode(':', $property)[0];
$property_value = trim(explode(':', $property)[1]);
if (!empty($property_value) && strpos($property_value, 'var(') !== false) {
preg_match_all("/var\s*\(\s*([^)]+)/", $property_value, $var_matches);
foreach ($var_matches[1] as $index => $value) {
if (strpos($value, ',') !== false) {
foreach (explode(',', $value) as $new_dec) {
$var_matches[1][] = trim($new_dec);
}
unset($var_matches[1][$index]);
}
}
$variables = array_merge($variables, $var_matches[1]);
}
return !empty(trim($property_name)) && in_array(trim($property_name), $variables);
});
}
$variables = array_unique($variables);
foreach ($matches[1] as $properties) {
$prop_arr = explode(';', $properties);
$prop_arr = array_filter($prop_arr, function ($property) use ($variables) {
$property_name = explode(':', $property)[0];
$property_value = trim(explode(':', $property)[1]);
if (!empty(trim($property_name)) && strpos(trim($property_name), '--') !== 0) {
return true;
}
return !empty(trim($property_name)) && in_array(trim($property_name), $variables);
});
if (!empty($prop_arr)) {
foreach ($prop_arr as $prop) {
$variable_proerties[] = $prop;
}
}
}
return $variable_proerties;
// return $this->extract_used_variables();
}
function cleanHtml($buffer) {
$dom = HtmlDomParser::str_get_html($buffer);
// Remove tags
foreach ($dom->find('meta, style, script, noscript, link') as $node) {
$node->outertext = '';
}
// Strip the head tag but keep body intact
// $head = $dom->find('head', 0);
// if ($head) {
// $head->innertext = ''; // Only safe after children are already emptied
// }
return (string) $dom;
}
static function next_token($sel, $current = 0)
{
// $tokens = [' ', '.', '#', '>', '+', '~'];
$tokens = [' ', '.', '#'];
// $tokens = ['.', '#',];
$characters = str_split($sel);
foreach ($characters as $index => $char) {
if ($index >= $current && in_array($char, $tokens)) {
if (!isset($characters[$index - 1])) {
continue;
}
if ($char === '.' && $characters[$index - 1] === "\\") {
continue;
}
if ($char === '#' && $characters[$index - 1] === "\\") {
continue;
}
return ['char' => $char, 'pos' => $index];
}
}
return false;
}
private static function matches_js_class($class, $js_classes)
{
foreach ($js_classes as $pattern) {
// Check if pattern ends with wildcard
if (substr($pattern, -1) === '*') {
// Wildcard pattern - check if class starts with prefix
$prefix = substr($pattern, 0, -1); // Remove the '*'
if (strpos($class, $prefix) === 0) {
return true;
}
} else {
// Exact match
if ($class === $pattern) {
return true;
}
}
}
return false;
}
private static function is_selector_used($selector, $html_selectors, $js_classes = [], $js_classes_hash = '')
{
if (empty($selector)) {
return false;
}
// forece exclude
foreach (self::$forceExclude as $keyword) {
if (preg_match($keyword, $selector)) {
return false;
}
}
// var_dump($selector);
if ($selector == ':root') {
return true;
}
// force include
foreach (self::$forceInclude as $keyword) {
if (preg_match($keyword, $selector)) {
return true;
}
}
// eliminate false negatives (:not(), pseudo, etc...)
// $selector = preg_replace('/(? ', ' ', $selector);
$selector = str_replace('>', ' ', $selector);
$selector = str_replace(' + ', ' ', $selector);
$selector = str_replace('+', ' ', $selector);
$selector = str_replace(' ~ ', ' ', $selector);
$selector = str_replace('~', ' ', $selector);
$selector = str_replace(' *', '', $selector);
// $selector = str_replace("\\", '', $selector);
// $selector = preg_replace('/\\\\(.)/u', '$1', $selector);
// Conservative handling: keep CSS for selectors with universal selector (*)
// Universal selectors require checking if ANY element exists
if (strpos($selector, '*') !== false) {
return true; // Keep CSS for universal selector
}
// var_dump($selector);
// Remove JS-added classes from selector before checking
// This allows checking the remaining non-JS parts of the selector
if (strpos($selector, '.') !== false && !empty($js_classes)) {
// var_dump($selector, $js_classes);
// Extract all classes from the selector
preg_match_all('/\.([a-zA-Z0-9_\\\:\\/\\-]+)/', $selector, $matches);
if (!empty($matches[1])) {
$has_js_class = false;
foreach ($matches[1] as $class) {
// Check if this class matches any JS class pattern (exact or wildcard)
if (self::matches_js_class($class, $js_classes)) {
// Remove this class from the selector
$selector = preg_replace('/\.' . preg_quote($class, '/') . '(?=[\s.#>+~\[\]:,)]|$)/', '', $selector);
$has_js_class = true;
// $selector = trim($selector);
}
}
if ($has_js_class) {
// Clean up selector: remove extra spaces, leading/trailing spaces
$selector = preg_replace('/\s+/', ' ', $selector);
$selector = trim($selector);
// var_dump($selector);
// echo "
";
// If selector is now empty or just spaces, return true (was all JS classes)
if (empty($selector)) {
return true;
}
}
}
}
// Handle selectors starting with HTML tags
$starts_with_tag = false;
$tag_name = '';
if (!in_array($selector[0], ['.', '#', ' '])) {
// Extract tag name
if (preg_match('/^([a-zA-Z][a-zA-Z0-9-]*)/i', $selector, $matches)) {
$tag_name = strtolower($matches[1]);
$tag_length = strlen($matches[1]);
// Check if the tag exists in HTML
// if (!isset($html_selectors['tags'][$tag_name])) {
// return false; // Tag not in HTML, selector can't match
// }
$tag_regex = '<' . preg_quote($tag_name, '/') . '\\b[^>]*>';
if (!preg_match("/$tag_regex/", self::$html)) {
// var_dump($tag_regex);
return false; // Tag not in HTML, selector can't match
}
$starts_with_tag = true;
// Remove tag from selector for token processing
$selector = substr($selector, $tag_length);
$selector = ltrim($selector); // Remove any leading space after tag
// If nothing left after tag, return true (pure tag selector)
if (empty($selector)) {
return true;
}
} else {
// Can't parse as tag, return conservative true
return true;
}
}
$token = self::next_token($selector);
$reg = '';
$prev = null;
$current = null;
$next = null;
$last_pos = -1; // Track last position
$iteration_count = 0; // Track iterations
$max_iterations = strlen($selector) * 1; // Reasonable max: 3 iterations per character
$prev_class_pos = [];
$herahicle_match = [];
while ($token !== false) {
$char = $token['char'];
$pos = $token['pos'];
// Safety check 1: Position hasn't advanced
if ($pos === $last_pos) {
// error_log("is_selector_used: Infinite loop detected - same position: $pos, selector: $selector");
break;
}
// Safety check 2: Too many iterations
$iteration_count++;
if ($iteration_count > $max_iterations) {
// error_log("is_selector_used: Infinite loop detected - max iterations exceeded ($iteration_count), selector: $selector");
break;
}
$last_pos = $pos;
$current = $char;
// error_log("is_selector_used: Loop iteration $iteration_count - char: '$char', pos: $pos"); // DEBUG: Commented out for performance
if ($char == '.') {
// var_dump( $pos);
// echo "
";
$next_token = self::next_token($selector, $pos + 1);
$next = $next_token['char'] ?? false;
$length = ($next_token['pos'] ?? strlen($selector)) - $pos;
$class = substr($selector, $pos + 1, $length - 1);
$token = $next_token;
$class = preg_replace('/\\\\(.)/u', '$1', $class);
// var_dump('class', $class);
// if (strpos($class, '\[') === false) {
// }
// $class = str_replace("\\", '', $class);
if (strpos($class, '\[') === false && strpos($class, '[') !== false) {
preg_match_all('/\[((?:[^]]+)?)/', $class, $attr_matches);
$class = preg_replace("/\[[^>\]]+]/", '', $class);
foreach ($attr_matches[1] as $match) {
$match = explode('=', $match)[0];
$match = preg_quote($match, '/');
$match_att = '<(?:[^>]+)?' . $match . '(?:[^>]+)?>';
if (!preg_match("/$match_att/", self::$html)) {
return false;
}
}
}
$esc = preg_quote($class, '/');
// var_dump($esc);
if ($prev == null && $next == ' ') {
$reg .= 'class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))[^\'"]*[\'"][^>]*>';
}
if ($prev == null && $next == '.') {
$reg .= 'class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))';
}
if ($prev == '.' && $next == '.') {
$reg .= '(?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s))';
}
if ($prev == '.' && $next == ' ') {
$reg .= '(?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))(?:[^\'"]+)?[\'"][^>]*>';
}
if ($prev == ' ' && $next === false) {
$reg .= 'class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))(?:[^\'"]+)?[\'"][^>]*>';
}
if ($prev == ' ' && $next == '.') {
$reg .= 'class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))';
}
if ($prev == '.' && $next === false) {
$reg .= '(?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))(?:[^\'"]+)?[\'"]';
}
if ($prev == null && $next === false) {
$reg .= 'class\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"][^>]*>';
}
if ($prev == '#' && $next === ' ') {
$reg .= '.*?class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))(?:[^\'"]+)?[\'"][^>]*>';
}
} elseif ($char == ' ') {
$next_token = self::next_token($selector, $pos + 1);
$length = ($next_token['pos'] ?? strlen($selector)) - $pos;
$class = substr($selector, $pos + 1, $length - 1);
$next = $next_token['char'] ?? false;
$token = $next_token;
// $reg .= ".*?<[^>]+";
// var_dump($reg);
if (!empty($reg)) {
if (preg_match("/$reg/", self::$html, $matches)) {
$prev = $char;
// $herahicle_match[] = $reg;
$reg = '';
} else {
return false;
}
}
} elseif ($char == '#') {
$next_token = self::next_token($selector, $pos + 1);
$next = $next_token['char'] ?? false;
$length = ($next_token['pos'] ?? strlen($selector)) - $pos;
$id = substr($selector, $pos + 1, $length - 1);
$token = $next_token;
$id = preg_replace('/\\\\(.)/u', '$1', $id);
if (strpos($id, '\[') === false && strpos($id, '[') !== false) {
preg_match_all('/\[((?:[^]]+)?)/', $id, $attr_matches);
$id = preg_replace("/\[[^>\]]+]/", '', $id);
foreach ($attr_matches[1] as $match) {
$match = explode('=', $match)[0];
$match = preg_quote($match, '/');
$match_att = '<(?:[^>]+)?' . $match . '(?:[^>]+)?>';
if (!preg_match("/$match_att/", self::$html)) {
return false;
}
}
}
$esc = preg_quote($id, '/');
// var_dump($esc, $token['pos']);
if ($prev == null && $next === false) {
$reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"][^>]*>';
}
if ($prev == null && $next === '.') {
$reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '[\'"]';
}
if ($prev == null && $next === ' ') {
$reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"][^>]*>';
}
if ($prev == ' ' && $next === false) {
$reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"][^>]*>';
}
if ($prev == ' ' && $next == '.') {
$reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"]';
}
} elseif ($char == '>' || $char == '+' || $char == '~') {
return true;
// Child combinator (>), adjacent sibling (+), and general sibling (~)
// For conservative matching, treat them like descendant combinators
$next_token = self::next_token($selector, $pos + 1);
$token = $next_token;
$next = $next_token['char'] ?? false;
$char = ' ';
// Match any content between elements (conservative approach)
if ($prev == '.' && ($next == '.' || $next == '#')) {
$reg .= '[\s\S]*?';
}
if ($prev == '#' && ($next == '.' || $next == '#')) {
$reg .= '[\s\S]*?';
}
} else {
break;
}
$prev = $char;
}
// If selector started with a tag, prepend tag regex to pattern
if ($starts_with_tag) {
$tag_regex = '<' . preg_quote($tag_name, '/') . '\\b[^>]*>';
if (!empty($reg)) {
// Tag followed by other selectors
// $reg = $tag_regex . '[\s\S]*?' . $reg;
$reg = $tag_regex;
} else {
// Pure tag selector (shouldn't reach here, but safety)
$reg = $tag_regex;
}
}
// if (empty($reg) || $reg == ']*>') {
// return false;
// }
$result = false;
if (!empty($herahicle_match)) {
if (!empty($reg)) {
$herahicle_match[] = $reg;
}
// var_dump($herahicle_match);
// echo "
";
$last_pos = 0;
foreach ($herahicle_match as $i => $reg) {
preg_match("/$reg/", self::$html, $matches);
if (!empty($matches[0])) {
$strpos = strpos(self::$html, $matches[0]);
if ($strpos === false) {
return false;
}
if ($last_pos > 0 && $strpos < $last_pos) {
return false;
}
$last_pos = $strpos;
if ($last_pos > 0 && $strpos > $last_pos && ($i + 1) === count($herahicle_match)) {
return true;
}
} else {
return false;
}
}
} else {
$result = preg_match("/$reg/", self::$html);
}
return $result;
}
function get_js_selector_classes($html)
{
$dom = HtmlDomParser::str_get_html($html);
$used_class = [
'woocommerce-js',
'woocommerce-product-gallery*',
'js',
];
foreach ($dom->find('script') as $script) {
$js_content = '';
// Check if script has src attribute (external JS)
$src = strtolower($script->src ?? '');
if (!empty($src)) {
// convert to abs
$baseUrl = self::getBaseUrl($this->site_url);
$src = self::rel2abs($src, $baseUrl);
if ($this->has_cache($src)) {
$js_content = $this->get_cache($src);
} else {
$response = wp_remote_get($src, ['timeout' => 30]);
$is_valid_response = wp_remote_retrieve_response_code($response) >= 200 && wp_remote_retrieve_response_code($response) < 400;
if (is_wp_error($response) || !$is_valid_response) {
continue;
}
$js_content = wp_remote_retrieve_body($response);
$this->cache_asset($src, $js_content);
}
} else {
// Inline script tag - get content directly
$js_content = $script->innertext;
}
// Skip if no content
if (empty($js_content)) {
continue;
}
// Search for classList.add() patterns
preg_match_all('/classList\.add\(\s*[\'"]([^\'"]+)/', $js_content, $matches);
foreach ($matches[1] as $class) {
$used_class[] = $class;
}
// Search for .addClass() patterns (jQuery)
preg_match_all('/\.addClass\(\s*[\'"]([^\'"]+)/', $js_content, $matches);
foreach ($matches[1] as $class) {
$used_class[] = $class;
}
unset($script);
}
unset($dom);
// return array_unique($used_class);
return $used_class;
}
function extract_used_css($css, $selectors)
{
// Remove comments
$css = preg_replace('/\/\*[\s\S]*?\*\//', '', $css);
// Remove @keyframes blocks
// $css = preg_replace('/@[-\w]*keyframes[^{]+\{(?:[^{}]*\{[^}]*\})*[^}]*\}/i', '', $css);
// $css = preg_replace('/@(?:-webkit-|-moz-|-o-|-ms-)?keyframes\s+[^{]+\{(?:[^{}]+|\{[^}]*\})*\}/is', '', $css);
// Remove @font-face blocks
$css = preg_replace('/@font-face\s*\{(?:[^{}]+|\{[^}]*\})*\}/is', '', $css);
// Remove @media blocks
// $css = preg_replace('/@media[^{]+\{(?:[^{}]+|\{[^}]*\})*\}/is', '', $css);
// Remove @charset
$css = preg_replace('/@charset\s+["\'][^"\']+["\']\s*;/i', '', $css);
// Remove @import
// $css = preg_replace('/@import\s+[^;]+;/i', '', $css);
$css = preg_replace(
'/@import\s+(?:url\s*\(\s*["\']?[^)]+["\']?\s*\)|["\'][^"\']+["\'])\s*[^;]*;?/i',
'',
$css
);
$css = preg_replace('/[\'"]\s*}\s*[\'"]/', 'bwpclosingcurlybracket', $css);
$css = preg_replace('/[\'"]\s*{\s*[\'"]/', 'bwpopeningcurlybracket', $css);
// Match CSS blocks - ensure we skip any remaining @ symbols
// Use PREG_SET_ORDER | PREG_OFFSET_CAPTURE to get byte positions for precise removal
preg_match_all('/([^{}]+)\{([^{}]+)\}/', $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
// preg_match_all('/([^{@}]+)\{((?:[^{}"\']*|"[^"]*"|\'[^\']*\')*)\}/', $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
$removals = [];
foreach ($matches as $match) {
$block = trim($match[0][0]);
// Skip at-rules
if (!empty($block) && !preg_match('/^@/', $block)) {
$block_selectors = trim(explode('{', $block)[0]);
$individual_selectors = $this->split_selector_by_comma($block_selectors);
$keep_block = false;
$used_selectors = [];
foreach ($individual_selectors as $sel) {
$sel = trim($sel);
if (self::is_selector_used($sel, $selectors, $this->js_classes)) {
$used_selectors[] = $sel;
$keep_block = true;
}
// if (self::is_selector_used($sel, $selectors, $this->js_classes)) {
// $keep_block = true;
// break;
// }
}
if ($keep_block) {
$new_selector_str = implode(', ', $used_selectors);
$block_pos = strpos($css, $block);
$sels_pos = $block_pos + strpos($block, $block_selectors);
$new_block = str_replace($block_selectors, $new_selector_str, $block);
$removals[] = [
'offset' => $match[0][1],
'length' => strlen($match[0][0]),
'replace' => $new_block,
];
}
if (!$keep_block) {
$removals[] = [
'offset' => $match[0][1],
'length' => strlen($match[0][0]),
];
}
}
}
// Remove blocks in reverse order to preserve earlier offsets
for ($i = count($removals) - 1; $i >= 0; $i--) {
$r = $removals[$i];
// var_dump(substr($css, $r['offset'], $r['length']));
$css = substr_replace($css, $r['replace'] ?? "", $r['offset'], $r['length']);
}
// Final cleanup: remove any stray @ symbols before class/ID/attribute selectors
// These are fragments left from block removals
$css = preg_replace('/@+(?=[.#\[])/i', '', $css);
$css = str_replace('bwpclosingcurlybracket', '"}"', $css);
$css = str_replace('bwpopeningcurlybracket', '"{"', $css);
return $css;
}
function minify_css($css)
{
// Remove comments
// $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
// // Remove whitespace around selectors, braces, colons, semicolons
// $css = preg_replace('/\s*([{}:;,>+~])\s*/', '$1', $css);
// Remove remaining newlines and tabs
$css = str_replace(["\r\n", "\r", "\n", "\t"], '', $css);
// Collapse multiple spaces into one
$css = preg_replace('/\s+/', ' ', $css);
$css = preg_replace('/\n+/', '\n', $css);
// Remove spaces around parentheses
// $css = preg_replace('/\s*\(\s*/', '(', $css);
// $css = preg_replace('/\s*\)\s*/', ')', $css);
// // Remove last semicolon before closing brace
// $css = str_replace(';}', '}', $css);
// Remove leading/trailing whitespace
return trim($css);
}
private function split_selector_by_comma($selector)
{
$parts = [];
$current = '';
$depth = 0;
$len = strlen($selector);
for ($i = 0; $i < $len; $i++) {
$char = $selector[$i];
if ($char === '(') {
$depth++;
$current .= $char;
} elseif ($char === ')') {
$depth--;
$current .= $char;
} elseif ($char === '[') {
$depth++;
$current .= $char;
} elseif ($char === ']') {
$depth--;
$current .= $char;
} elseif ($char === ',' && $depth === 0) {
// Only split on commas outside parentheses
if (trim($current) !== '') {
$parts[] = trim($current);
}
$current = '';
} else {
$current .= $char;
}
}
// Add last part
if (trim($current) !== '') {
$parts[] = trim($current);
}
return $parts;
}
}