PluginProbe
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript / 4.0.19
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript v4.0.19
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.0.19, at inc/photon/class-berqUsedCSS.php

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