PluginProbe
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript / 4.1.16
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript v4.1.16
4.1.16 4.1.15 4.1.14 4.1.13 4.1.12 4.1.11 4.1.10 4.0.30 4.0.29 4.0.28 4.0.27 4.0.26 4.0.24 4.0.25 4.0.23 4.0.22 4.0.21 4.0.19 4.0.18 4.0.17 4.0.16 1.9.3 1.9.4 1.9.5 1.9.6 All 170 releases
searchpro / inc / photon / class-berqUsedCSS.php

class-berqUsedCSS.php in BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript 4.1.16, at inc/photon/class-berqUsedCSS.php

1,420 lines 46.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) exit;
3
4 use BerqWP_Deps\voku\helper\HtmlDomParser;
5
6 class berqUsedCSS {
7
8 private static $html = null;
9
10 public $combine_css = '';
11 public $domain = '';
12 public $site_url = '';
13 public $js_classes = [];
14
15 static $forceExclude = [
16 '/:focus[^)]/',
17 '/:hover[^)]/',
18 ];
19 static $forceInclude = [
20 '/\[.*\$.*=.*\]/',
21 // '/:has\(.*?\)/',
22 // '/:not\(.*?\)/',
23 // '/:is\(.*?\)/',
24 // '/:where\(.*?\)/',
25 // '/^\:is/',
26 // '/^\:where/',
27 '/\.elementor-motion-effects-element-type-background/',
28 '/\.swiper-wrapper/',
29 '/\.woocommerce-js/',
30 '/\.et_pb_menu__logo img/',
31 '/\.tns-/',
32 '/\.woocommerce-js/',
33 '/\.dtb-hamburger/',
34 '/\.elementor-social-icon/',
35 '/\.visible/',
36 '/^\.td-js-loaded/',
37 '/^\.lte-navwrapper-mobile/',
38 '/^\.lte-navbar-items.navbar-mobile/',
39 '/^\.navbar-mobile/',
40 '/^\.theme-menu-mode-responsive/',
41 '/^\.theme-menu-responsive-button/',
42 '/^\.stk-video-background/',
43 '/^\.is-menu.is-dropdown/',
44 '/^\.animated/',
45 '/^\.et_pb_animation/',
46 '/^\.et_animated/',
47 '/^\.et_mobile_menu/',
48 '/^\.awb-menu.collapse-enabled/',
49 '/^\.collapse-enabled/',
50 '/^\.do-animate/',
51 '/^\.ast-header-break-point/',
52 '/^\.plus-canvas-content-wrap/',
53 '/^\.elementor-element:where/',
54 '/^\.bounce/',
55 '/^\.bounceIn/',
56 '/^\.bounceInDown/',
57 '/^\.bounceInLeft/',
58 '/^\.bounceInRight/',
59 '/^\.bounceInUp/',
60 '/^\.e-animation-bob/',
61 '/^\.e-animation-bounce-in/',
62 '/^\.e-animation-bounce-out/',
63 '/^\.e-animation-buzz-out/',
64 '/^\.e-animation-buzz/',
65 '/^\.e-animation-float/',
66 '/^\.e-animation-grow-rotate/',
67 '/^\.e-animation-grow/',
68 '/^\.e-animation-hang/',
69 '/^\.e-animation-pop/',
70 '/^\.e-animation-pulse-grow/',
71 '/^\.e-animation-pulse-shrink/',
72 '/^\.e-animation-pulse/',
73 '/^\.e-animation-push/',
74 '/^\.e-animation-rotate/',
75 '/^\.e-animation-shrink/',
76 '/^\.e-animation-sink/',
77 '/^\.e-animation-skew-backward/',
78 '/^\.e-animation-skew-forward/',
79 '/^\.e-animation-skew/',
80 '/^\.e-animation-wobble-bottom/',
81 '/^\.e-animation-wobble-horizontal/',
82 '/^\.e-animation-wobble-skew/',
83 '/^\.e-animation-wobble-to-bottom-right/',
84 '/^\.e-animation-wobble-to-top-right/',
85 '/^\.e-animation-wobble-top/',
86 '/^\.e-animation-wobble-vertical/',
87 '/^\.fadeIn/',
88 '/^\.fadeInDown/',
89 '/^\.fadeInLeft/',
90 '/^\.fadeInRight/',
91 '/^\.fadeInUp/',
92 '/^\.flash/',
93 '/^\.headShake/',
94 '/^\.jello/',
95 '/^\.lightSpeedIn/',
96 '/^\.pulse/',
97 '/^\.rollIn/',
98 '/^\.rotateIn/',
99 '/^\.rotateInDownLeft/',
100 '/^\.rotateInDownRight/',
101 '/^\.rotateInUpLeft/',
102 '/^\.rotateInUpRight/',
103 '/^\.rubberBand/',
104 '/^\.shake/',
105 '/^\.slideInDown/',
106 '/^\.slideInLeft/',
107 '/^\.slideInRight/',
108 '/^\.slideInUp/',
109 '/^\.swing/',
110 '/^\.tada/',
111 '/^\.wobble/',
112 '/^\.zoomIn/',
113 '/^\.zoomInDown/',
114 '/^\.zoomInLeft/',
115 '/^\.zoomInRight/',
116 '/^\.zoomInUp/',
117 ];
118
119 function __construct($site_url) {
120 $this->site_url = $site_url;
121 }
122
123 function forceInclude($selectors) {
124 self::$forceInclude = array_merge(self::$forceInclude, $selectors);
125 }
126
127 function process_css($html)
128 {
129
130 // self::$html = $this->cleanHtml($html);
131 self::$html = $html;
132 $selectors = [];
133 // $this->js_classes = $this->get_js_selector_classes($html);
134
135 $dom = HtmlDomParser::str_get_html($html);
136 $used_fonts = [];
137 $used_font_weights = [
138 'normal' => 1,
139 '400' => 1,
140 ];
141 $used_css = '';
142
143 foreach ($dom->find('style') as $style) {
144 $style->setAttribute('data-berqwp-critical-css', '1');
145 }
146
147 // mark stylesheet links
148 foreach ($dom->find('link') as $link) {
149 $rel = strtolower($link->rel ?? '');
150 $as = strtolower($link->as ?? '');
151
152 if ($rel === 'stylesheet' || $as === 'style') {
153 $link->setAttribute('data-berqwp-critical-css', '1');
154 }
155 }
156
157 $nodes = $dom->find('[data-berqwp-critical-css="1"]');
158
159
160 foreach ($nodes as $css_node) {
161 $local_css = '';
162
163 if ($css_node->tag == 'style') {
164 $local_css = "@media all { ".$css_node->innertext." } ";
165
166 }
167
168 if ($css_node->tag == 'link') {
169
170 $href = $css_node->href ?? '';
171 $media = $css_node->media ?? '';
172 $onload = $css_node->onload ?? '';
173
174 if ($onload === "this.media='all'") {
175 $media = 'all';
176 }
177
178 if ($onload === "this.media=&#039;all&#039;;this.onload=null;") {
179 $media = 'all';
180 }
181
182 if ($media === 'print') {
183 continue;
184 }
185
186 if (!self::is_valid_asset($href)) {
187 continue;
188 }
189
190 // convert to abs
191 $baseUrl = self::getBaseUrl($this->site_url);
192 $href = self::rel2abs($href, $baseUrl);
193
194 $response = self::fetch($href);
195
196 if ($response === false) {
197 continue;
198 }
199
200 $local_css .= $response;
201 }
202
203 if (!empty($local_css)) {
204 $matches = self::extractUrlsFromCss($local_css);
205
206 foreach ($matches as $url) {
207 $url = trim($url);
208 $baseUrl = self::getBaseUrl($this->site_url);
209
210 if ($css_node->tag === 'link') {
211 $baseUrl = $href;
212 }
213
214 if (!empty($url) && self::is_valid_asset($url)) {
215 $local_css = $this->replace_url($url, self::rel2abs($url, $baseUrl), $local_css);
216 }
217 }
218
219 if (!empty($media) && $media !== 'screen') {
220 $local_css = "@media $media {{$local_css}}";
221 }
222
223 $this->combine_css .= $local_css;
224
225 $css = $this->extract_used_css($local_css, $selectors);
226 $css = $this->minify_css($css);
227 $css = preg_replace('/@media[^{]+\{\s*\}/i', '', $css);
228 $css = preg_replace('/@@+/', '', $css);
229 $used_css .= $css;
230
231 preg_match_all("/font-family\s*:\s*([^!;}]+)/", $css, $mf);
232 preg_match_all("/font-weight\s*:\s*([^!;}]+)/", $css, $mw);
233
234 if (!empty($mf)) {
235
236 foreach ($mf[1] as $ff) {
237 if (strpos($ff, ',') !== false) {
238
239 foreach (explode(',', $ff) as $fm) {
240 $fm = trim($fm, " \t\n\r\0\x0B'\"");
241 $used_fonts[$fm] = 1;
242 }
243 } else {
244 $fm = trim($ff, " \t\n\r\0\x0B'\"");
245 $used_fonts[$fm] = 1;
246 }
247 }
248
249 foreach ($mw[1] as $fw) {
250 $weight = trim($fw, " \t\n\r\0\x0B'\"");
251 $used_font_weights[$weight] = 1;
252 }
253 }
254
255
256 // Insert style tag before the link tag
257 $style_tag = '<style data-berqwp="inline" data-berqwp-critical-inline="1">' . $css . '</style>';
258 $css_node->outertext = $style_tag . $css_node->outertext;
259 }
260 }
261
262 $font_faces = $this->used_font_faces($used_fonts, $used_font_weights);
263
264 $font_faces = str_replace('font-display:block;', 'font-display:swap;', $font_faces);
265 $font_faces = str_replace('font-style:', 'font-display:swap;font-style:', $font_faces);
266
267 // $used_keyframes = $this->used_keyframes($used_css);
268 // $used_varirables = $this->extract_used_variables($used_css);
269
270 // if (!empty($used_varirables)) {
271 // $used_varirables = ':root{' . implode(';', $used_varirables) . '}';
272 // } else {
273 // $used_varirables = '';
274 // }
275
276 return $used_css . $font_faces;
277
278 }
279
280 function has_cache($url)
281 {
282
283 $path = WP_CONTENT_DIR . '/cache/berqwp/external/';
284
285 if (!is_dir($path)) {
286 if (!wp_mkdir_p($path)) {
287 return false;
288 }
289 }
290
291 $file_name = md5($url) . '.gz';
292 $file_path = $path . $file_name;
293
294 // return is_file($file_path) && !empty(@filemtime($file_path)) && @filemtime($file_path) > (time() - DAY_IN_SECONDS);
295 return is_file($file_path) && !empty(@filemtime($file_path)) && @filemtime($file_path) > (time() - DAY_IN_SECONDS);
296 // return is_file($file_path);
297 }
298
299 function cache_asset($url, $content)
300 {
301 $path = WP_CONTENT_DIR . '/cache/berqwp/external/';
302
303 if (!is_dir($path)) {
304 if (!wp_mkdir_p($path)) {
305 return false;
306 }
307 }
308
309 $file_name = md5($url) . '.gz';
310
311 return file_put_contents($path . $file_name, gzencode($content, 9), LOCK_EX);
312 }
313
314 function cache_miss($url)
315 {
316 $path = WP_CONTENT_DIR . '/cache/berqwp/external/';
317 if (!is_dir($path)) {
318 if (!wp_mkdir_p($path)) return false;
319 }
320 return file_put_contents($path . md5($url) . '.gz', gzencode('berqwp-404', 9), LOCK_EX);
321 }
322
323 function is_cached_miss($url)
324 {
325 if (!$this->has_cache($url)) return false;
326 return $this->get_cache($url) === 'berqwp-404';
327 }
328
329 function get_cache($url)
330 {
331 $path = WP_CONTENT_DIR . '/cache/berqwp/external/';
332
333 if (!is_dir($path)) {
334 if (!wp_mkdir_p($path)) {
335 return false;
336 }
337 }
338
339 $file_name = md5($url) . '.gz';
340
341 return gzdecode(file_get_contents($path . $file_name));
342 }
343
344 function fetch($url)
345 {
346
347 $local_css = '';
348
349 if ($this->is_self_hosted_url($url)) {
350 if ($this->is_cached_miss($url)) {
351 return false;
352 }
353 $local_path = $this->url_to_path($url);
354 if ($local_path && is_file($local_path)) {
355 return file_get_contents($local_path);
356 }
357 $this->cache_miss($url);
358 return false;
359 }
360
361 if ($this->is_cached_miss($url)) {
362 return false;
363 }
364
365 if ($this->has_cache($url)) {
366 $local_css = $this->get_cache($url);
367 } else {
368
369 $response = wp_remote_get($url, ['timeout' => 30]);
370 $is_valid_response = wp_remote_retrieve_response_code($response) >= 200 && wp_remote_retrieve_response_code($response) < 400;
371
372 if (is_wp_error($response) || !$is_valid_response) {
373 $this->cache_miss($url);
374 return false;
375 }
376
377 $local_css = wp_remote_retrieve_body($response);
378
379 // preg_match_all("/@import\s+url\(([^)]+)[^;]+;/", $local_css, $matches);
380 preg_match_all('/@import\s+(?:url\s*\(\s*[\'"]?([^\'")]+)[\'"]?\s*\)|[\'"]([^\'";]+)[\'"])/', $local_css, $matches);
381
382 if (!empty($matches[1])) {
383 foreach ($matches[1] as $import_css_url) {
384 $import_css_url = trim($import_css_url, " \t\n\r\0\x0B'\"");
385
386 $baseUrl = self::getBaseUrl($url);
387 $import_css_url = self::rel2abs($import_css_url, $baseUrl);
388
389 if (!self::is_valid_asset($import_css_url)) {
390 continue;
391 }
392
393 $sub_response = self::fetch($import_css_url);
394
395 if ($sub_response === false) {
396 continue;
397 }
398
399 $url_matches = self::extractUrlsFromCss($sub_response);
400
401 foreach ($url_matches as $url_func) {
402 $url_func = trim($url_func);
403
404 if (!empty($url_func)) {
405 // $sub_response = str_replace($url_func, self::rel2abs($url_func, $import_css_url), $sub_response);
406 $sub_response = $this->replace_url($url_func, self::rel2abs($url_func, $import_css_url), $sub_response);
407 }
408 }
409
410 $local_css .= $sub_response;
411 }
412 }
413
414 $this->cache_asset($url, $local_css);
415 }
416
417 return $local_css;
418 }
419
420 function replace_url($search, $replace, $content)
421 {
422
423 // match with single quotes
424 if (strpos($content, "'$search'") !== false) {
425 $content = str_replace("'$search'", "'$replace'", $content);
426 }
427
428 // match double quotes
429 if (strpos($content, "\"$search\"") !== false) {
430 $content = str_replace("\"$search\"", "\"$replace\"", $content);
431 }
432
433 // match brakets
434 if (strpos($content, "($search)") !== false) {
435 $content = str_replace("($search)", "($replace)", $content);
436 }
437
438 // match spaces
439 if (strpos($content, " $search ") !== false) {
440 $content = str_replace(" $search ", " $replace ", $content);
441 }
442
443 return $content;
444 }
445
446 static function is_valid_asset($url)
447 {
448 return strpos($url, 'data:') === false;
449 }
450
451 public static function rel2abs($rel, $base)
452 {
453 /* return if already absolute URL */
454 if (parse_url($rel, PHP_URL_SCHEME) != '')
455 return $rel;
456
457 // Return with base scheme if protocol-relative (starts with //)
458 if (strpos($rel, '//') === 0) {
459 $scheme = parse_url($base, PHP_URL_SCHEME) ?: 'https';
460 return $scheme . ':' . $rel;
461 }
462
463 /* queries and anchors */
464 if ($rel[0] == '#' || $rel[0] == '?')
465 return $base . $rel;
466
467 /* parse base URL and convert to local variables:
468 $scheme, $host, $path */
469 extract(parse_url($base));
470
471 /* remove non-directory element from path */
472 $path = preg_replace('#/[^/]*$#', '', $path);
473
474 /* destroy path if relative url points to root */
475 if ($rel[0] == '/')
476 $path = '';
477
478 /* dirty absolute URL */
479 $abs = "$host$path/$rel";
480
481 /* replace '//' or '/./' or '/foo/../' with '/' */
482 $re = array('#(/\.?/)#', '#/(?!\.\.)[^/]+/\.\./#');
483 for ($n = 1; $n > 0; $abs = preg_replace($re, '/', $abs, -1, $n)) {
484 }
485
486 /* absolute URL is ready! */
487 return $scheme . '://' . $abs;
488 }
489
490 public static function getBaseUrl($fileUrl)
491 {
492 // Parse the URL and get its components
493 $parsedUrl = parse_url($fileUrl);
494 $scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
495 $host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
496 $path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
497
498 // Remove the file name from the path to get the base URL
499 $basePath = preg_replace('/\/[^\/]+$/', '/', $path);
500
501 // Construct the base URL
502 return $scheme . $host . $basePath;
503 }
504
505 public static function extractUrlsFromCss($cssContent)
506 {
507 $urls = [];
508 $pattern = '/url\((.*?)\)/i';
509
510 preg_match_all($pattern, $cssContent, $matches);
511
512 if (!empty($matches[1])) {
513 foreach ($matches[1] as $match) {
514 $urls[] = trim($match, '\'" ');
515 }
516 }
517
518 return $urls;
519 }
520
521 function is_self_hosted_url($url)
522 {
523 if (empty($url)) {
524 return false;
525 }
526
527 // Relative URLs are self-hosted
528 if (strpos($url, '//') === false) {
529 return true;
530 }
531
532 $site_host = wp_parse_url(site_url(), PHP_URL_HOST);
533 $url_host = wp_parse_url($url, PHP_URL_HOST);
534
535 return $site_host && $url_host && strtolower($site_host) === strtolower($url_host);
536 }
537
538 function url_to_path($url)
539 {
540
541 if (strpos($url, '?') !== false) {
542 $url = explode('?', $url)[0];
543 }
544
545 // Handle relative URLs
546 if (strpos($url, '//') === 0) {
547 return realpath(ABSPATH . ltrim($url, '/'));
548 }
549
550 $content_url = content_url();
551 $content_dir = WP_CONTENT_DIR;
552
553 if (strpos($url, $content_url) === 0) {
554 // var_dump(realpath(
555 // str_replace($content_url, $content_dir, $url)
556 // ));
557 return str_replace($content_url, $content_dir, $url);
558 }
559
560 // Fallback for site root files
561 $site_url = get_site_url('/');
562
563 if (strpos($url, $site_url) === 0) {
564 return realpath(
565 str_replace($site_url, ABSPATH, $url)
566 );
567 }
568
569 return false;
570 }
571
572 function used_font_faces($used_fonts, $used_font_weights)
573 {
574
575 preg_match_all("/@font-face\s*{[^}]*}/", $this->combine_css, $matches);
576 $fonts = $matches[0];
577 $all_fonts = [];
578 $used_font_faces = '';
579
580 foreach ($fonts as $font) {
581 preg_match("/font-family\s*:\s*([^;}]+)/", $font, $mf);
582 preg_match("/font-weight\s*:\s*([^;}]+)/", $font, $mw);
583
584 $family = trim($mf[1], " \t\n\r\0\x0B'\"");
585 $weight = trim($mw[1], " \t\n\r\0\x0B'\"");
586
587 if (!empty($used_fonts[$family]) && !empty($used_font_weights[$weight])) {
588 $used_font_faces .= $font;
589 }
590
591 unset($font);
592 }
593
594 return $used_font_faces;
595 }
596
597 function used_keyframes($used_css)
598 {
599 preg_match_all('/animation\s*:\s*([^\s!;}]+)/', $used_css, $matches);
600
601 if (empty($matches[1])) {
602 return '';
603 }
604
605 $key_frame_css = '';
606 $used_keyframe_names = [];
607
608 foreach ($matches[1] as $animation_name) {
609 $animation_name = trim($animation_name);
610 $used_keyframe_names[$animation_name] = 1;
611 }
612
613 $captured = 0;
614
615 preg_match_all('/@keyframes\s+[\w-]+\s*\{(?:[^{}]*|\{[^{}]*\})*\}/', $this->combine_css, $keyframe_matches);
616
617 if (!empty($keyframe_matches)) {
618 foreach ($keyframe_matches[0] as $keyframe) {
619
620
621 // if ($captured === count($used_keyframe_names)) {
622 // break;
623 // }
624
625 preg_match('/@keyframes\s+([^\s{]+)/', $keyframe, $kf_name_match);
626
627 if (isset($used_keyframe_names[$kf_name_match[1]])) {
628 $key_frame_css .= $keyframe;
629 $captured++;
630 }
631 }
632 }
633
634 return $key_frame_css;
635 }
636
637 function extract_used_variables($used_css)
638 {
639
640 // preg_match_all("/var\s*\(\s*([^\s)]+)/", $used_css, $matches);
641 preg_match_all("/var\s*\(\s*([^)]+)/", $used_css, $matches);
642
643 if (!empty($matches[1])) {
644 foreach ($matches[1] as $index => $value) {
645 if (strpos($value, ',') !== false) {
646 foreach (explode(',', $value) as $new_dec) {
647 $matches[1][] = trim($new_dec);
648 }
649 unset($matches[1][$index]);
650 }
651 }
652 return $this->extract_variable_declarations($matches[1]);
653 }
654 }
655
656 function extract_variable_declarations($variables)
657 {
658
659 $variables = array_map(function ($item) {
660 return trim($item);
661 }, $variables);
662
663 $variables = array_unique($variables);
664 $variable_proerties = [];
665 // preg_match_all("/:root\s*{([^;}]+)/", $this->combine_css, $matches);
666 preg_match_all("/:root\s*{\s*([^}]+)/", $this->combine_css, $matches);
667
668
669 // collected nested var() in :root
670 foreach ($matches[1] as $properties) {
671 $prop_arr = explode(';', $properties);
672 $prop_arr = array_filter($prop_arr, function ($property) use ($properties, &$variables) {
673 $property_name = explode(':', $property)[0];
674 $property_value = trim(explode(':', $property)[1]);
675
676 if (!empty($property_value) && strpos($property_value, 'var(') !== false) {
677 preg_match_all("/var\s*\(\s*([^)]+)/", $property_value, $var_matches);
678
679 foreach ($var_matches[1] as $index => $value) {
680 if (strpos($value, ',') !== false) {
681 foreach (explode(',', $value) as $new_dec) {
682 $var_matches[1][] = trim($new_dec);
683 }
684 unset($var_matches[1][$index]);
685 }
686 }
687
688 $variables = array_merge($variables, $var_matches[1]);
689 }
690
691 return !empty(trim($property_name)) && in_array(trim($property_name), $variables);
692 });
693 }
694
695 $variables = array_unique($variables);
696
697 foreach ($matches[1] as $properties) {
698 $prop_arr = explode(';', $properties);
699 $prop_arr = array_filter($prop_arr, function ($property) use ($variables) {
700 $property_name = explode(':', $property)[0];
701 $property_value = trim(explode(':', $property)[1]);
702
703 if (!empty(trim($property_name)) && strpos(trim($property_name), '--') !== 0) {
704 return true;
705 }
706
707 return !empty(trim($property_name)) && in_array(trim($property_name), $variables);
708 });
709
710 if (!empty($prop_arr)) {
711 foreach ($prop_arr as $prop) {
712 $variable_proerties[] = $prop;
713 }
714 }
715 }
716
717 return $variable_proerties;
718 // return $this->extract_used_variables();
719 }
720
721 function cleanHtml($buffer) {
722 $dom = HtmlDomParser::str_get_html($buffer);
723
724 // Remove tags
725 foreach ($dom->find('meta, style, script, noscript, link') as $node) {
726 $node->outertext = '';
727 }
728
729 // Strip the head tag but keep body intact
730 // $head = $dom->find('head', 0);
731 // if ($head) {
732 // $head->innertext = ''; // Only safe after children are already emptied
733 // }
734
735 return (string) $dom;
736 }
737
738 static function next_token($sel, $current = 0)
739 {
740 // $tokens = [' ', '.', '#', '>', '+', '~'];
741 $tokens = [' ', '.', '#'];
742 // $tokens = ['.', '#',];
743 $characters = str_split($sel);
744
745 foreach ($characters as $index => $char) {
746
747 if ($index >= $current && in_array($char, $tokens)) {
748
749 if (!isset($characters[$index - 1])) {
750 continue;
751 }
752
753 if ($char === '.' && $characters[$index - 1] === "\\") {
754 continue;
755 }
756
757 if ($char === '#' && $characters[$index - 1] === "\\") {
758 continue;
759 }
760
761 return ['char' => $char, 'pos' => $index];
762 }
763 }
764
765 return false;
766 }
767
768 private static function matches_js_class($class, $js_classes)
769 {
770 foreach ($js_classes as $pattern) {
771 // Check if pattern ends with wildcard
772 if (substr($pattern, -1) === '*') {
773 // Wildcard pattern - check if class starts with prefix
774 $prefix = substr($pattern, 0, -1); // Remove the '*'
775 if (strpos($class, $prefix) === 0) {
776 return true;
777 }
778 } else {
779 // Exact match
780 if ($class === $pattern) {
781 return true;
782 }
783 }
784 }
785
786 return false;
787 }
788
789
790 private static function is_selector_used($selector, $html_selectors, $js_classes = [], $js_classes_hash = '')
791 {
792
793 if (empty($selector)) {
794 return false;
795 }
796
797 // forece exclude
798 foreach (self::$forceExclude as $keyword) {
799 if (preg_match($keyword, $selector)) {
800 return false;
801 }
802 }
803
804 // var_dump($selector);
805
806 if ($selector == ':root') {
807 return true;
808 }
809
810 // force include
811 foreach (self::$forceInclude as $keyword) {
812 if (preg_match($keyword, $selector)) {
813 return true;
814 }
815 }
816
817 // eliminate false negatives (:not(), pseudo, etc...)
818 // $selector = preg_replace('/(?<!\\\\)::?[a-zA-Z0-9_-]+(\(.+?\))?/', '', $selector);
819 $selector = preg_replace('/:{1,2}[a-zA-Z-]+(\((?:[^()]*|\([^()]*\))*\))?/', '', $selector);
820 $selector = trim($selector); // Remove leading/trailing spaces after pseudo-element removal
821
822 // Return false if selector is empty after cleanup
823 if (empty($selector)) {
824 return true;
825 }
826
827 $selector = str_replace(' > ', ' ', $selector);
828 $selector = str_replace('>', ' ', $selector);
829 $selector = str_replace(' + ', ' ', $selector);
830 $selector = str_replace('+', ' ', $selector);
831 $selector = str_replace(' ~ ', ' ', $selector);
832 $selector = str_replace('~', ' ', $selector);
833 $selector = str_replace(' *', '', $selector);
834
835
836 // $selector = str_replace("\\", '', $selector);
837 // $selector = preg_replace('/\\\\(.)/u', '$1', $selector);
838
839 // Conservative handling: keep CSS for selectors with universal selector (*)
840 // Universal selectors require checking if ANY element exists
841 if (strpos($selector, '*') !== false) {
842 return true; // Keep CSS for universal selector
843 }
844
845
846 // var_dump($selector);
847 // Remove JS-added classes from selector before checking
848 // This allows checking the remaining non-JS parts of the selector
849
850 if (strpos($selector, '.') !== false && !empty($js_classes)) {
851 // var_dump($selector, $js_classes);
852 // Extract all classes from the selector
853 preg_match_all('/\.([a-zA-Z0-9_\\\:\\/\\-]+)/', $selector, $matches);
854 if (!empty($matches[1])) {
855 $has_js_class = false;
856
857 foreach ($matches[1] as $class) {
858 // Check if this class matches any JS class pattern (exact or wildcard)
859 if (self::matches_js_class($class, $js_classes)) {
860 // Remove this class from the selector
861 $selector = preg_replace('/\.' . preg_quote($class, '/') . '(?=[\s.#>+~\[\]:,)]|$)/', '', $selector);
862 $has_js_class = true;
863 // $selector = trim($selector);
864 }
865 }
866
867 if ($has_js_class) {
868
869 // Clean up selector: remove extra spaces, leading/trailing spaces
870 $selector = preg_replace('/\s+/', ' ', $selector);
871 $selector = trim($selector);
872
873 // var_dump($selector);
874 // echo "<br>";
875
876 // If selector is now empty or just spaces, return true (was all JS classes)
877 if (empty($selector)) {
878 return true;
879 }
880 }
881 }
882 }
883
884 // Handle selectors starting with HTML tags
885 $starts_with_tag = false;
886 $tag_name = '';
887
888 if (!in_array($selector[0], ['.', '#', ' '])) {
889 // Extract tag name
890 if (preg_match('/^([a-zA-Z][a-zA-Z0-9-]*)/i', $selector, $matches)) {
891 $tag_name = strtolower($matches[1]);
892 $tag_length = strlen($matches[1]);
893
894 // Check if the tag exists in HTML
895 // if (!isset($html_selectors['tags'][$tag_name])) {
896 // return false; // Tag not in HTML, selector can't match
897 // }
898
899 $tag_regex = '<' . preg_quote($tag_name, '/') . '\\b[^>]*>';
900
901 if (!preg_match("/$tag_regex/", self::$html)) {
902 // var_dump($tag_regex);
903 return false; // Tag not in HTML, selector can't match
904 }
905
906 $starts_with_tag = true;
907
908 // Remove tag from selector for token processing
909 $selector = substr($selector, $tag_length);
910 $selector = ltrim($selector); // Remove any leading space after tag
911
912 // If nothing left after tag, return true (pure tag selector)
913 if (empty($selector)) {
914 return true;
915 }
916 } else {
917 // Can't parse as tag, return conservative true
918 return true;
919 }
920 }
921
922 $token = self::next_token($selector);
923 $reg = '';
924
925 $prev = null;
926 $current = null;
927 $next = null;
928 $last_pos = -1; // Track last position
929 $iteration_count = 0; // Track iterations
930 $max_iterations = strlen($selector) * 1; // Reasonable max: 3 iterations per character
931
932 $prev_class_pos = [];
933 $herahicle_match = [];
934
935 while ($token !== false) {
936 $char = $token['char'];
937 $pos = $token['pos'];
938
939 // Safety check 1: Position hasn't advanced
940 if ($pos === $last_pos) {
941 // error_log("is_selector_used: Infinite loop detected - same position: $pos, selector: $selector");
942 break;
943 }
944
945 // Safety check 2: Too many iterations
946 $iteration_count++;
947 if ($iteration_count > $max_iterations) {
948 // error_log("is_selector_used: Infinite loop detected - max iterations exceeded ($iteration_count), selector: $selector");
949 break;
950 }
951
952 $last_pos = $pos;
953 $current = $char;
954
955 // error_log("is_selector_used: Loop iteration $iteration_count - char: '$char', pos: $pos"); // DEBUG: Commented out for performance
956
957 if ($char == '.') {
958 // var_dump( $pos);
959 // echo "<br>";
960 $next_token = self::next_token($selector, $pos + 1);
961 $next = $next_token['char'] ?? false;
962 $length = ($next_token['pos'] ?? strlen($selector)) - $pos;
963 $class = substr($selector, $pos + 1, $length - 1);
964 $token = $next_token;
965
966
967 $class = preg_replace('/\\\\(.)/u', '$1', $class);
968 // var_dump('class', $class);
969 // if (strpos($class, '\[') === false) {
970
971 // }
972
973 // $class = str_replace("\\", '', $class);
974
975 if (strpos($class, '\[') === false && strpos($class, '[') !== false) {
976
977 preg_match_all('/\[((?:[^]]+)?)/', $class, $attr_matches);
978 $class = preg_replace("/\[[^>\]]+]/", '', $class);
979
980 foreach ($attr_matches[1] as $match) {
981 $match = explode('=', $match)[0];
982 $match = preg_quote($match, '/');
983 $match_att = '<(?:[^>]+)?' . $match . '(?:[^>]+)?>';
984
985 if (!preg_match("/$match_att/", self::$html)) {
986 return false;
987 }
988 }
989 }
990
991 $esc = preg_quote($class, '/');
992
993 // var_dump($esc);
994
995 if ($prev == null && $next == ' ') {
996 $reg .= 'class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))[^\'"]*[\'"][^>]*>';
997 }
998
999 if ($prev == null && $next == '.') {
1000 $reg .= 'class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))';
1001 }
1002
1003 if ($prev == '.' && $next == '.') {
1004 $reg .= '(?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s))';
1005 }
1006
1007 if ($prev == '.' && $next == ' ') {
1008 $reg .= '(?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))(?:[^\'"]+)?[\'"][^>]*>';
1009 }
1010
1011 if ($prev == ' ' && $next === false) {
1012 $reg .= 'class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))(?:[^\'"]+)?[\'"][^>]*>';
1013 }
1014
1015 if ($prev == ' ' && $next == '.') {
1016 $reg .= 'class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))';
1017 }
1018
1019 if ($prev == '.' && $next === false) {
1020 $reg .= '(?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))(?:[^\'"]+)?[\'"]';
1021 }
1022
1023 if ($prev == null && $next === false) {
1024 $reg .= 'class\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"][^>]*>';
1025 }
1026
1027 if ($prev == '#' && $next === ' ') {
1028 $reg .= '.*?class\s*=\s*[\'"](?=[^"\']*(?:\s' . $esc . '|' . $esc . '\s|\b' . $esc . '\b))(?:[^\'"]+)?[\'"][^>]*>';
1029 }
1030 } elseif ($char == ' ') {
1031 $next_token = self::next_token($selector, $pos + 1);
1032 $length = ($next_token['pos'] ?? strlen($selector)) - $pos;
1033 $class = substr($selector, $pos + 1, $length - 1);
1034 $next = $next_token['char'] ?? false;
1035 $token = $next_token;
1036
1037 // $reg .= ".*?<[^>]+";
1038 // var_dump($reg);
1039 if (!empty($reg)) {
1040 if (preg_match("/$reg/", self::$html, $matches)) {
1041 $prev = $char;
1042 // $herahicle_match[] = $reg;
1043 $reg = '';
1044 } else {
1045 return false;
1046 }
1047 }
1048 } elseif ($char == '#') {
1049 $next_token = self::next_token($selector, $pos + 1);
1050 $next = $next_token['char'] ?? false;
1051 $length = ($next_token['pos'] ?? strlen($selector)) - $pos;
1052 $id = substr($selector, $pos + 1, $length - 1);
1053 $token = $next_token;
1054
1055 $id = preg_replace('/\\\\(.)/u', '$1', $id);
1056
1057 if (strpos($id, '\[') === false && strpos($id, '[') !== false) {
1058 preg_match_all('/\[((?:[^]]+)?)/', $id, $attr_matches);
1059 $id = preg_replace("/\[[^>\]]+]/", '', $id);
1060
1061 foreach ($attr_matches[1] as $match) {
1062 $match = explode('=', $match)[0];
1063 $match = preg_quote($match, '/');
1064 $match_att = '<(?:[^>]+)?' . $match . '(?:[^>]+)?>';
1065
1066 if (!preg_match("/$match_att/", self::$html)) {
1067 return false;
1068 }
1069 }
1070 }
1071
1072 $esc = preg_quote($id, '/');
1073 // var_dump($esc, $token['pos']);
1074
1075 if ($prev == null && $next === false) {
1076 $reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"][^>]*>';
1077 }
1078
1079 if ($prev == null && $next === '.') {
1080 $reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '[\'"]';
1081 }
1082
1083 if ($prev == null && $next === ' ') {
1084 $reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"][^>]*>';
1085 }
1086
1087 if ($prev == ' ' && $next === false) {
1088 $reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"][^>]*>';
1089 }
1090
1091 if ($prev == ' ' && $next == '.') {
1092 $reg .= 'id\s*=\s*[\'"][^\'"]*' . $esc . '(?:[^\'"]+)?[\'"]';
1093 }
1094 } elseif ($char == '>' || $char == '+' || $char == '~') {
1095 return true;
1096 // Child combinator (>), adjacent sibling (+), and general sibling (~)
1097 // For conservative matching, treat them like descendant combinators
1098 $next_token = self::next_token($selector, $pos + 1);
1099 $token = $next_token;
1100 $next = $next_token['char'] ?? false;
1101 $char = ' ';
1102
1103 // Match any content between elements (conservative approach)
1104 if ($prev == '.' && ($next == '.' || $next == '#')) {
1105 $reg .= '[\s\S]*?';
1106 }
1107
1108 if ($prev == '#' && ($next == '.' || $next == '#')) {
1109 $reg .= '[\s\S]*?';
1110 }
1111 } else {
1112 break;
1113 }
1114
1115 $prev = $char;
1116 }
1117
1118 // If selector started with a tag, prepend tag regex to pattern
1119 if ($starts_with_tag) {
1120 $tag_regex = '<' . preg_quote($tag_name, '/') . '\\b[^>]*>';
1121
1122 if (!empty($reg)) {
1123 // Tag followed by other selectors
1124 // $reg = $tag_regex . '[\s\S]*?' . $reg;
1125 $reg = $tag_regex;
1126 } else {
1127 // Pure tag selector (shouldn't reach here, but safety)
1128 $reg = $tag_regex;
1129 }
1130 }
1131
1132 // if (empty($reg) || $reg == ']*>') {
1133 // return false;
1134 // }
1135
1136 $result = false;
1137
1138 if (!empty($herahicle_match)) {
1139
1140 if (!empty($reg)) {
1141 $herahicle_match[] = $reg;
1142 }
1143
1144 // var_dump($herahicle_match);
1145 // echo "<br>";
1146 $last_pos = 0;
1147
1148 foreach ($herahicle_match as $i => $reg) {
1149
1150 preg_match("/$reg/", self::$html, $matches);
1151
1152 if (!empty($matches[0])) {
1153 $strpos = strpos(self::$html, $matches[0]);
1154
1155 if ($strpos === false) {
1156 return false;
1157 }
1158
1159 if ($last_pos > 0 && $strpos < $last_pos) {
1160 return false;
1161 }
1162
1163 $last_pos = $strpos;
1164
1165 if ($last_pos > 0 && $strpos > $last_pos && ($i + 1) === count($herahicle_match)) {
1166 return true;
1167 }
1168 } else {
1169 return false;
1170 }
1171 }
1172 } else {
1173
1174 $result = preg_match("/$reg/", self::$html);
1175 }
1176
1177
1178 return $result;
1179 }
1180
1181 function get_js_selector_classes($html)
1182 {
1183
1184 $dom = HtmlDomParser::str_get_html($html);
1185 $used_class = [
1186 'woocommerce-js',
1187 'woocommerce-product-gallery*',
1188 'js',
1189 ];
1190
1191 foreach ($dom->find('script') as $script) {
1192 $js_content = '';
1193
1194 // Check if script has src attribute (external JS)
1195 $src = strtolower($script->src ?? '');
1196
1197 if (!empty($src)) {
1198
1199 // convert to abs
1200 $baseUrl = self::getBaseUrl($this->site_url);
1201 $src = self::rel2abs($src, $baseUrl);
1202
1203 if ($this->has_cache($src)) {
1204 $js_content = $this->get_cache($src);
1205 } else {
1206
1207 $response = wp_remote_get($src, ['timeout' => 30]);
1208 $is_valid_response = wp_remote_retrieve_response_code($response) >= 200 && wp_remote_retrieve_response_code($response) < 400;
1209
1210 if (is_wp_error($response) || !$is_valid_response) {
1211 continue;
1212 }
1213
1214 $js_content = wp_remote_retrieve_body($response);
1215
1216 $this->cache_asset($src, $js_content);
1217 }
1218 } else {
1219 // Inline script tag - get content directly
1220 $js_content = $script->innertext;
1221 }
1222
1223 // Skip if no content
1224 if (empty($js_content)) {
1225 continue;
1226 }
1227
1228 // Search for classList.add() patterns
1229 preg_match_all('/classList\.add\(\s*[\'"]([^\'"]+)/', $js_content, $matches);
1230
1231 foreach ($matches[1] as $class) {
1232 $used_class[] = $class;
1233 }
1234
1235 // Search for .addClass() patterns (jQuery)
1236 preg_match_all('/\.addClass\(\s*[\'"]([^\'"]+)/', $js_content, $matches);
1237
1238 foreach ($matches[1] as $class) {
1239 $used_class[] = $class;
1240 }
1241
1242 unset($script);
1243 }
1244
1245 unset($dom);
1246
1247 // return array_unique($used_class);
1248 return $used_class;
1249 }
1250
1251 function extract_used_css($css, $selectors)
1252 {
1253 // Remove comments
1254 $css = preg_replace('/\/\*[\s\S]*?\*\//', '', $css);
1255
1256 // Remove @keyframes blocks
1257 // $css = preg_replace('/@[-\w]*keyframes[^{]+\{(?:[^{}]*\{[^}]*\})*[^}]*\}/i', '', $css);
1258 // $css = preg_replace('/@(?:-webkit-|-moz-|-o-|-ms-)?keyframes\s+[^{]+\{(?:[^{}]+|\{[^}]*\})*\}/is', '', $css);
1259
1260 // Remove @font-face blocks
1261 $css = preg_replace('/@font-face\s*\{(?:[^{}]+|\{[^}]*\})*\}/is', '', $css);
1262
1263 // Remove @media blocks
1264 // $css = preg_replace('/@media[^{]+\{(?:[^{}]+|\{[^}]*\})*\}/is', '', $css);
1265
1266 // Remove @charset
1267 $css = preg_replace('/@charset\s+["\'][^"\']+["\']\s*;/i', '', $css);
1268
1269 // Remove @import
1270 // $css = preg_replace('/@import\s+[^;]+;/i', '', $css);
1271 $css = preg_replace(
1272 '/@import\s+(?:url\s*\(\s*["\']?[^)]+["\']?\s*\)|["\'][^"\']+["\'])\s*[^;]*;?/i',
1273 '',
1274 $css
1275 );
1276
1277
1278 $css = preg_replace('/[\'"]\s*}\s*[\'"]/', 'bwpclosingcurlybracket', $css);
1279 $css = preg_replace('/[\'"]\s*{\s*[\'"]/', 'bwpopeningcurlybracket', $css);
1280
1281 // Match CSS blocks - ensure we skip any remaining @ symbols
1282 // Use PREG_SET_ORDER | PREG_OFFSET_CAPTURE to get byte positions for precise removal
1283 preg_match_all('/([^{}]+)\{([^{}]+)\}/', $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
1284 // preg_match_all('/([^{@}]+)\{((?:[^{}"\']*|"[^"]*"|\'[^\']*\')*)\}/', $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
1285
1286 $removals = [];
1287
1288 foreach ($matches as $match) {
1289 $block = trim($match[0][0]);
1290
1291 // Skip at-rules
1292 if (!empty($block) && !preg_match('/^@/', $block)) {
1293 $block_selectors = trim(explode('{', $block)[0]);
1294 $individual_selectors = $this->split_selector_by_comma($block_selectors);
1295 $keep_block = false;
1296 $used_selectors = [];
1297
1298 foreach ($individual_selectors as $sel) {
1299 $sel = trim($sel);
1300
1301 if (self::is_selector_used($sel, $selectors, $this->js_classes)) {
1302 $used_selectors[] = $sel;
1303 $keep_block = true;
1304 }
1305
1306 // if (self::is_selector_used($sel, $selectors, $this->js_classes)) {
1307
1308 // $keep_block = true;
1309 // break;
1310 // }
1311
1312 }
1313
1314 if ($keep_block) {
1315 $new_selector_str = implode(', ', $used_selectors);
1316 $block_pos = strpos($css, $block);
1317 $sels_pos = $block_pos + strpos($block, $block_selectors);
1318 $new_block = str_replace($block_selectors, $new_selector_str, $block);
1319
1320 $removals[] = [
1321 'offset' => $match[0][1],
1322 'length' => strlen($match[0][0]),
1323 'replace' => $new_block,
1324 ];
1325 }
1326
1327 if (!$keep_block) {
1328 $removals[] = [
1329 'offset' => $match[0][1],
1330 'length' => strlen($match[0][0]),
1331 ];
1332 }
1333 }
1334 }
1335
1336 // Remove blocks in reverse order to preserve earlier offsets
1337 for ($i = count($removals) - 1; $i >= 0; $i--) {
1338 $r = $removals[$i];
1339 // var_dump(substr($css, $r['offset'], $r['length']));
1340 $css = substr_replace($css, $r['replace'] ?? "", $r['offset'], $r['length']);
1341 }
1342
1343 // Final cleanup: remove any stray @ symbols before class/ID/attribute selectors
1344 // These are fragments left from block removals
1345 $css = preg_replace('/@+(?=[.#\[])/i', '', $css);
1346
1347 $css = str_replace('bwpclosingcurlybracket', '"}"', $css);
1348 $css = str_replace('bwpopeningcurlybracket', '"{"', $css);
1349
1350 return $css;
1351 }
1352
1353 function minify_css($css)
1354 {
1355 // Remove comments
1356 // $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css);
1357
1358 // // Remove whitespace around selectors, braces, colons, semicolons
1359 // $css = preg_replace('/\s*([{}:;,>+~])\s*/', '$1', $css);
1360
1361 // Remove remaining newlines and tabs
1362 $css = str_replace(["\r\n", "\r", "\n", "\t"], '', $css);
1363
1364 // Collapse multiple spaces into one
1365 $css = preg_replace('/\s+/', ' ', $css);
1366 $css = preg_replace('/\n+/', '\n', $css);
1367
1368 // Remove spaces around parentheses
1369 // $css = preg_replace('/\s*\(\s*/', '(', $css);
1370 // $css = preg_replace('/\s*\)\s*/', ')', $css);
1371
1372 // // Remove last semicolon before closing brace
1373 // $css = str_replace(';}', '}', $css);
1374
1375 // Remove leading/trailing whitespace
1376 return trim($css);
1377 }
1378
1379 private function split_selector_by_comma($selector)
1380 {
1381 $parts = [];
1382 $current = '';
1383 $depth = 0;
1384 $len = strlen($selector);
1385
1386 for ($i = 0; $i < $len; $i++) {
1387 $char = $selector[$i];
1388
1389 if ($char === '(') {
1390 $depth++;
1391 $current .= $char;
1392 } elseif ($char === ')') {
1393 $depth--;
1394 $current .= $char;
1395 } elseif ($char === '[') {
1396 $depth++;
1397 $current .= $char;
1398 } elseif ($char === ']') {
1399 $depth--;
1400 $current .= $char;
1401 } elseif ($char === ',' && $depth === 0) {
1402 // Only split on commas outside parentheses
1403 if (trim($current) !== '') {
1404 $parts[] = trim($current);
1405 }
1406 $current = '';
1407 } else {
1408 $current .= $char;
1409 }
1410 }
1411
1412 // Add last part
1413 if (trim($current) !== '') {
1414 $parts[] = trim($current);
1415 }
1416
1417 return $parts;
1418 }
1419
1420 }