bundle_css($html, $post_id, $page_url); } // Bundle JS if enabled if (!empty($settings['bundle_js'])) { $html = $this->bundle_js($html, $post_id, $page_url); } return $html; } /** * Bundle all CSS files into a single file * * @param string $html The HTML content * @param int $post_id The post ID * @param string $page_url The original page URL * @return string Modified HTML with bundled CSS */ private function bundle_css($html, $post_id, $page_url) { // Extract all stylesheet links $stylesheets = $this->extract_stylesheets($html); if (empty($stylesheets)) { return $html; } $bundled_css = ''; $base_url = $this->get_base_url($page_url); // Download and combine all CSS files foreach ($stylesheets as $stylesheet) { $url = $stylesheet['href']; $absolute_url = $this->make_absolute_url($url, $base_url); $css_content = $this->download_asset($absolute_url); if ($css_content !== false) { // Rewrite URLs in CSS to be absolute $css_content = $this->rewrite_css_urls($css_content, $absolute_url); // Add source comment and content $bundled_css .= "\n/* Source: {$absolute_url} */\n"; $bundled_css .= $css_content . "\n"; } } if (empty($bundled_css)) { return $html; } // Generate a hash for the bundle filename $bundle_hash = md5($bundled_css); $bundle_filename = "bundle-{$post_id}-{$bundle_hash}.css"; // Save the bundled CSS file $assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/'; if (!file_exists($assets_dir)) { wp_mkdir_p($assets_dir); } $bundle_path = $assets_dir . $bundle_filename; file_put_contents($bundle_path, $bundled_css); // Get the URL for the bundled file $bundle_url = WP_TO_HTML_CACHE_URL . 'assets/' . $bundle_filename; // Remove all original stylesheet links foreach ($stylesheets as $stylesheet) { $html = str_replace($stylesheet['full_tag'], '', $html); } // Add the bundled CSS link before $bundled_link = ''; $html = str_replace('', $bundled_link . "\n", $html); return $html; } /** * Bundle all JS files into a single file * * @param string $html The HTML content * @param int $post_id The post ID * @param string $page_url The original page URL * @return string Modified HTML with bundled JS */ private function bundle_js($html, $post_id, $page_url) { // Extract all script tags with src attributes $scripts = $this->extract_scripts($html); if (empty($scripts)) { return $html; } $bundled_js = ''; $base_url = $this->get_base_url($page_url); $has_defer = false; $has_async = false; // Download and combine all JS files foreach ($scripts as $script) { $url = $script['src']; $absolute_url = $this->make_absolute_url($url, $base_url); $js_content = $this->download_asset($absolute_url); if ($js_content !== false) { // Add source comment and content $bundled_js .= "\n/* Source: {$absolute_url} */\n"; $bundled_js .= $js_content . ";\n"; } // Track defer/async attributes if (!empty($script['defer'])) { $has_defer = true; } if (!empty($script['async'])) { $has_async = true; } } if (empty($bundled_js)) { return $html; } // Generate a hash for the bundle filename $bundle_hash = md5($bundled_js); $bundle_filename = "bundle-{$post_id}-{$bundle_hash}.js"; // Save the bundled JS file $assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/'; if (!file_exists($assets_dir)) { wp_mkdir_p($assets_dir); } $bundle_path = $assets_dir . $bundle_filename; file_put_contents($bundle_path, $bundled_js); // Get the URL for the bundled file $bundle_url = WP_TO_HTML_CACHE_URL . 'assets/' . $bundle_filename; // Remove all original script tags foreach ($scripts as $script) { $html = str_replace($script['full_tag'], '', $html); } // Build the bundled script tag with appropriate attributes $attributes = ''; if ($has_defer) { $attributes .= ' defer'; } if ($has_async) { $attributes .= ' async'; } // Add the bundled JS script before $bundled_script = ''; $html = str_replace('', $bundled_script . "\n", $html); return $html; } /** * Extract all stylesheet links from HTML * * @param string $html The HTML content * @return array Array of stylesheet data */ private function extract_stylesheets($html) { $stylesheets = array(); // Match link tags with rel="stylesheet" $pattern = '/]*rel=["\']stylesheet["\'][^>]*>/i'; preg_match_all($pattern, $html, $matches); // Also match link tags where rel comes after href $pattern2 = '/]*href=["\'][^"\']+["\'][^>]*rel=["\']stylesheet["\'][^>]*>/i'; preg_match_all($pattern2, $html, $matches2); $all_matches = array_unique(array_merge($matches[0], $matches2[0])); foreach ($all_matches as $tag) { // Extract href if (preg_match('/href=["\']([^"\']+)["\']/i', $tag, $href_match)) { $href = $href_match[1]; // Skip data URIs and inline styles if (strpos($href, 'data:') === 0) { continue; } $stylesheets[] = array( 'full_tag' => $tag, 'href' => $href, ); } } return $stylesheets; } /** * Extract all script tags with src from HTML * * @param string $html The HTML content * @return array Array of script data */ private function extract_scripts($html) { $scripts = array(); // Match script tags with src attribute $pattern = '/]*src=["\']([^"\']+)["\'][^>]*><\/script>/i'; preg_match_all($pattern, $html, $matches, PREG_SET_ORDER); foreach ($matches as $match) { $tag = $match[0]; $src = $match[1]; // Skip data URIs if (strpos($src, 'data:') === 0) { continue; } // Check for defer/async attributes $defer = (stripos($tag, 'defer') !== false); $async = (stripos($tag, 'async') !== false); $scripts[] = array( 'full_tag' => $tag, 'src' => $src, 'defer' => $defer, 'async' => $async, ); } return $scripts; } /** * Download an asset from a URL * * @param string $url The asset URL * @return string|false The asset content or false on failure */ private function download_asset($url) { $response = wp_remote_get($url, array( 'timeout' => 15, 'sslverify' => false, )); if (is_wp_error($response)) { return false; } $status_code = wp_remote_retrieve_response_code($response); if ($status_code !== 200) { return false; } return wp_remote_retrieve_body($response); } /** * Rewrite relative URLs in CSS to be absolute * * @param string $css The CSS content * @param string $css_url The original CSS file URL * @return string CSS with absolute URLs */ private function rewrite_css_urls($css, $css_url) { $base_url = dirname($css_url) . '/'; // Match url() patterns $pattern = '/url\s*\(\s*["\']?(?!data:)(?!https?:\/\/)(?!\/\/)([^"\'\)]+)["\']?\s*\)/i'; $css = preg_replace_callback($pattern, function($matches) use ($base_url) { $relative_url = $matches[1]; // Handle ../ paths if (strpos($relative_url, '../') === 0) { $absolute_url = $this->resolve_relative_path($base_url, $relative_url); } elseif (strpos($relative_url, './') === 0) { $absolute_url = $base_url . substr($relative_url, 2); } else { $absolute_url = $base_url . $relative_url; } return 'url("' . $absolute_url . '")'; }, $css); // Handle @import statements $import_pattern = '/@import\s+["\'](?!data:)(?!https?:\/\/)(?!\/\/)([^"\']+)["\']/i'; $css = preg_replace_callback($import_pattern, function($matches) use ($base_url) { $relative_url = $matches[1]; $absolute_url = $this->resolve_relative_path($base_url, $relative_url); return '@import "' . $absolute_url . '"'; }, $css); return $css; } /** * Resolve a relative path against a base URL * * @param string $base_url The base URL * @param string $relative_path The relative path * @return string The absolute URL */ private function resolve_relative_path($base_url, $relative_path) { // Parse the base URL $parsed = parse_url($base_url); $scheme = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : 'https://'; $host = isset($parsed['host']) ? $parsed['host'] : ''; $path = isset($parsed['path']) ? $parsed['path'] : '/'; // Remove filename from path if exists if (substr($path, -1) !== '/') { $path = dirname($path) . '/'; } // Process the relative path while (strpos($relative_path, '../') === 0) { $relative_path = substr($relative_path, 3); $path = dirname(rtrim($path, '/')) . '/'; } // Remove leading ./ if (strpos($relative_path, './') === 0) { $relative_path = substr($relative_path, 2); } return $scheme . $host . $path . $relative_path; } /** * Make a URL absolute * * @param string $url The URL to make absolute * @param string $base_url The base URL * @return string The absolute URL */ private function make_absolute_url($url, $base_url) { // Already absolute if (preg_match('/^https?:\/\//i', $url)) { return $url; } // Protocol-relative URL if (strpos($url, '//') === 0) { $parsed_base = parse_url($base_url); $scheme = isset($parsed_base['scheme']) ? $parsed_base['scheme'] : 'https'; return $scheme . ':' . $url; } // Absolute path (starts with /) if (strpos($url, '/') === 0) { $parsed_base = parse_url($base_url); $scheme = isset($parsed_base['scheme']) ? $parsed_base['scheme'] : 'https'; $host = isset($parsed_base['host']) ? $parsed_base['host'] : ''; return $scheme . '://' . $host . $url; } // Relative path return $this->resolve_relative_path($base_url, $url); } /** * Get the base URL from a page URL * * @param string $url The page URL * @return string The base URL (without path) */ private function get_base_url($url) { $parsed = parse_url($url); $scheme = isset($parsed['scheme']) ? $parsed['scheme'] : 'https'; $host = isset($parsed['host']) ? $parsed['host'] : ''; $path = isset($parsed['path']) ? $parsed['path'] : '/'; return $scheme . '://' . $host . $path; } /** * Clear bundled assets from cache * * @return bool */ public function clear_bundled_assets() { $assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/'; if (!file_exists($assets_dir)) { return true; } $files = glob($assets_dir . 'bundle-*'); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } return true; } /** * Delete bundled assets for a specific post * * @param int $post_id The post ID * @return bool */ public function delete_post_assets($post_id) { $assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/'; if (!file_exists($assets_dir)) { return true; } $files = glob($assets_dir . "bundle-{$post_id}-*"); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } return true; } }