First Commit

This commit is contained in:
2026-03-18 14:21:32 -07:00
parent 8712bbcc1a
commit 1cf5bbeeb0
12 changed files with 4185 additions and 0 deletions
+480
View File
@@ -0,0 +1,480 @@
<?php
/**
* Assets Class
*
* Handles bundling of CSS and JavaScript files into single files
* for improved performance of static HTML pages.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Assets {
/**
* Single instance
*/
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Nothing to initialize
}
/**
* Bundle CSS and JS assets in the HTML
*
* @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 assets
*/
public function bundle_assets($html, $post_id, $page_url) {
$settings = WP_To_HTML::get_settings();
// Bundle CSS if enabled
if (!empty($settings['bundle_css'])) {
$html = $this->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 </head>
$bundled_link = '<link rel="stylesheet" href="' . esc_url($bundle_url) . '" type="text/css" media="all" />';
$html = str_replace('</head>', $bundled_link . "\n</head>", $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 </body>
$bundled_script = '<script src="' . esc_url($bundle_url) . '"' . $attributes . '></script>';
$html = str_replace('</body>', $bundled_script . "\n</body>", $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 = '/<link[^>]*rel=["\']stylesheet["\'][^>]*>/i';
preg_match_all($pattern, $html, $matches);
// Also match link tags where rel comes after href
$pattern2 = '/<link[^>]*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 = '/<script[^>]*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;
}
}