358 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			358 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
namespace Sphere\Debloat;
 | 
						|
use Sphere\Debloat\OptimizeJs\Script;
 | 
						|
 | 
						|
/**
 | 
						|
 * JS Optimizations such as defer and delay.
 | 
						|
 * 
 | 
						|
 * @author  asadkn
 | 
						|
 * @since   1.0.0
 | 
						|
 */
 | 
						|
class OptimizeJs
 | 
						|
{
 | 
						|
	protected $html;
 | 
						|
 | 
						|
	/**
 | 
						|
	 * @var array
 | 
						|
	 */
 | 
						|
	protected $scripts = [];
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Include and exclude scripts for "delay".
 | 
						|
	 *
 | 
						|
	 * @var array
 | 
						|
	 */
 | 
						|
	protected $include_scripts = [];
 | 
						|
	protected $exclude_scripts = [];
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Exclude scripts from defer.
 | 
						|
	 */
 | 
						|
	protected $exclude_defer = [];
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Scripts already delayed or deffered.
 | 
						|
	 */
 | 
						|
	protected $done_delay  = [];
 | 
						|
	protected $done_defer = [];
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Scripts registered data from core.
 | 
						|
	 */
 | 
						|
	protected $enqueues = [];
 | 
						|
 | 
						|
	public function __construct(string $html)
 | 
						|
	{
 | 
						|
		$this->html = $html;
 | 
						|
	}
 | 
						|
 | 
						|
	public function process()
 | 
						|
	{
 | 
						|
		$this->find_scripts();
 | 
						|
 | 
						|
		// Figure out valid scripts.
 | 
						|
		$this->setup_valid_scripts();
 | 
						|
 | 
						|
		do_action('debloat/optimize_js_begin', $this);
 | 
						|
 | 
						|
		// Early First pass to setup valid parents for child dependency checks.
 | 
						|
		array_map(function($script) {
 | 
						|
			$this->should_defer_script($script);
 | 
						|
		}, $this->scripts);
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Setup scripts defer and delays and render the replacements.
 | 
						|
		 */
 | 
						|
		$has_delayed = false;
 | 
						|
		foreach ($this->scripts as $script) {
 | 
						|
 | 
						|
			// Has to be done for all scripts.
 | 
						|
			if (Plugin::options()->minify_js) {
 | 
						|
				$this->minify_js($script);
 | 
						|
				$script->render = true;
 | 
						|
			}
 | 
						|
 | 
						|
			// Defer script if not already deferred.
 | 
						|
			if ($this->should_defer_script($script)) {
 | 
						|
				$script->defer  = true;
 | 
						|
				$script->render = true;
 | 
						|
 | 
						|
				$has_delayed = true;
 | 
						|
			}
 | 
						|
 | 
						|
			// Should this script be delayed.
 | 
						|
			if ($this->should_delay_script($script)) {
 | 
						|
				$script->delay = true;
 | 
						|
				$script->render = true;
 | 
						|
 | 
						|
				$has_delayed = true;
 | 
						|
			}
 | 
						|
			else {
 | 
						|
				Util\debug_log('Skipping: ' . print_r($script, true));
 | 
						|
			}
 | 
						|
 | 
						|
			if ($script->render) {
 | 
						|
				$this->html = str_replace($script->orig_html, $script->render(), $this->html);
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if ($has_delayed) {
 | 
						|
			Plugin::delay_load()->enable(true);
 | 
						|
		}
 | 
						|
		
 | 
						|
		return $this->html;
 | 
						|
	}
 | 
						|
 | 
						|
	public function find_scripts()
 | 
						|
	{
 | 
						|
		/**
 | 
						|
		 * Collect all valid enqueues.
 | 
						|
		 */
 | 
						|
		$all_scripts  = [];
 | 
						|
		foreach (wp_scripts()->registered as $handle => $script) {
 | 
						|
 | 
						|
			// Not an enqueued script, ignore.
 | 
						|
			if (!in_array($handle, wp_scripts()->done)) {
 | 
						|
				continue;
 | 
						|
			}
 | 
						|
 | 
						|
			// Don't mutate original.
 | 
						|
			$script = clone $script;
 | 
						|
 | 
						|
			$script->deps = array_map(function($id) {
 | 
						|
				// jQuery JS should be mapped to core. Add -js suffix for others.
 | 
						|
				return $id === 'jquery' ? 'jquery-core-js' : $id . '-js';
 | 
						|
			}, $script->deps);
 | 
						|
 | 
						|
			// Add -js prefix to match the IDs that will retrieved from attrs.
 | 
						|
			$handle .= '-js';
 | 
						|
			$all_scripts[$handle] = $script;
 | 
						|
 | 
						|
			// Pseudo entry for extras, adding a dependency.
 | 
						|
			if (isset($script->extra['after'])) {
 | 
						|
				$all_scripts[$handle . '-after'] = (object) [
 | 
						|
					'deps' => [$handle]
 | 
						|
				];
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		$this->enqueues = $all_scripts;
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Find all scripts.
 | 
						|
		 */
 | 
						|
		if (!preg_match_all('#<script.*?</script>#si', $this->html, $matches)) {
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		foreach ($matches[0] as $script) {
 | 
						|
			$script = Script::from_tag($script);
 | 
						|
			if ($script) {
 | 
						|
				$script->deps = $this->enqueues[$script->id]->deps ?? [];
 | 
						|
 | 
						|
				// Note: There can be multiple scripts with same URL or id. So we don't use $script->id.
 | 
						|
				$this->scripts[] = $script;
 | 
						|
 | 
						|
				// Inline script has jQuery content? Ensure dependency.
 | 
						|
				if (
 | 
						|
					!$script->url && 
 | 
						|
					!in_array('jquery-core-js', $script->deps) && 
 | 
						|
					strpos($script->content, 'jQuery') !== false
 | 
						|
				) {
 | 
						|
					$script->deps[] = 'jquery-core-js';
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Check if the script has a parent dependency that is delayed/deferred.
 | 
						|
	 *
 | 
						|
	 * @param Script $script
 | 
						|
	 * @param array $valid
 | 
						|
	 * @return boolean
 | 
						|
	 */
 | 
						|
	public function check_dependency(Script $script, array $valid)
 | 
						|
	{
 | 
						|
		// Check if one of parent dependencies is delayed/deferred.
 | 
						|
		foreach ((array) $script->deps as $dep) {
 | 
						|
			if (isset($valid[$dep])) {
 | 
						|
				return true;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// For translations, if wp-i18n-js is valid, so should be translations.
 | 
						|
		if (preg_match('/-js-translations/', $script->id, $matches)) {
 | 
						|
			if (isset($valid['wp-i18n-js'])) {
 | 
						|
				return true;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// Special case: Inline script with a parent dep. If parent is true, so is child.
 | 
						|
		// Note: 'before' and 'extra' isn't accounted for and is not usually needed. Since 'extra' is 
 | 
						|
		// usually localization and 'before' has to happen before anyways.
 | 
						|
		if (preg_match('/(.+?-js)-(before|after)/', $script->id, $matches)) {
 | 
						|
			$handle = $matches[1];
 | 
						|
 | 
						|
			// Parent was valid, so is the current child.
 | 
						|
			if (isset($valid[$handle])) {
 | 
						|
				return true;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Should the script be delayed using one of the JS delay methods.
 | 
						|
	 *
 | 
						|
	 * @param Script $script
 | 
						|
	 * @return boolean
 | 
						|
	 */
 | 
						|
	public function should_delay_script(Script $script)
 | 
						|
	{
 | 
						|
		if (!Plugin::options()->delay_js) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
 | 
						|
		// Excludes should be handled before includes and parents check.
 | 
						|
		foreach ($this->exclude_scripts as $exclude) {
 | 
						|
			if (Util\asset_match($exclude, $script, 'orig_html')) {
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// Delay all.
 | 
						|
		if (Plugin::options()->delay_js_all) {
 | 
						|
			return true;
 | 
						|
		}
 | 
						|
 | 
						|
		if ($this->check_dependency($script, $this->done_delay)) {
 | 
						|
			$this->done_delay[$script->id] = true;
 | 
						|
			return true;
 | 
						|
		}
 | 
						|
 | 
						|
		foreach ($this->include_scripts as $include) {
 | 
						|
			if (Util\asset_match($include, $script, 'orig_html')) {
 | 
						|
				$this->done_delay[$script->id] = true;
 | 
						|
				return true;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return false;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Should the script be deferred.
 | 
						|
	 *
 | 
						|
	 * @param Script $script
 | 
						|
	 * @return boolean
 | 
						|
	 */
 | 
						|
	public function should_defer_script(Script $script) 
 | 
						|
	{
 | 
						|
		// Defer not enabled.
 | 
						|
		if (!Plugin::options()->defer_js) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
 | 
						|
		foreach ($this->exclude_defer as $exclude) {
 | 
						|
			if (Util\asset_match($exclude, $script, 'orig_html')) {
 | 
						|
				return false;
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		// For inline scripts: By default not deferred, unless child of a deferred. 
 | 
						|
		if (!$script->url) {
 | 
						|
			if (Plugin::options()->defer_js_inline) {
 | 
						|
				return true;
 | 
						|
			}
 | 
						|
 | 
						|
			if ($this->check_dependency($script, $this->done_defer)) {
 | 
						|
				$this->done_defer[$script->id] = true;
 | 
						|
				return true;
 | 
						|
			}
 | 
						|
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
 | 
						|
		// If defer or async attr already exists on original script.
 | 
						|
		if ($script->defer || $script->async) {
 | 
						|
			return false;
 | 
						|
		}
 | 
						|
 | 
						|
		$this->done_defer[$script->id] = true;
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Setup includes and excludes based on options.
 | 
						|
	 *
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function setup_valid_scripts()
 | 
						|
	{	
 | 
						|
		// Used by both delay and defer.
 | 
						|
		$shared_excludes = [
 | 
						|
			// Lazyloads should generally be loaded as early as possible and not deferred.
 | 
						|
			'lazysizes.js',
 | 
						|
			'lazyload.js',
 | 
						|
			'lazysizes.min.js',
 | 
						|
			'lazyLoadOptions',
 | 
						|
			'lazyLoadThumb',
 | 
						|
		];
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Defer scripts.
 | 
						|
		 */
 | 
						|
		$defer_excludes = array_merge(
 | 
						|
			Util\option_to_array(Plugin::options()->defer_js_excludes),
 | 
						|
			$shared_excludes
 | 
						|
		);
 | 
						|
 | 
						|
		$defer_excludes[] = 'id:-js-extra';
 | 
						|
		$this->exclude_defer = apply_filters('debloat/defer_js_excludes', $defer_excludes);
 | 
						|
 | 
						|
		/**
 | 
						|
		 * Delayed load scripts.
 | 
						|
		 */
 | 
						|
		$excludes = Util\option_to_array(Plugin::options()->delay_js_excludes);
 | 
						|
		$excludes = array_merge($excludes, $shared_excludes, [
 | 
						|
			// Jetpack stats.
 | 
						|
			'url://stats.wp.com',
 | 
						|
			'_stq.push',
 | 
						|
 | 
						|
			// WPML browser redirect.
 | 
						|
			'browser-redirect/app.js',
 | 
						|
 | 
						|
			// Skip -js-extra as it's  global variables and localization that shouldn't be delayed.
 | 
						|
			'id:-js-extra'
 | 
						|
		]);
 | 
						|
 | 
						|
		$this->exclude_scripts = apply_filters('debloat/delay_js_excludes', $excludes);
 | 
						|
 | 
						|
		$this->include_scripts = array_merge(
 | 
						|
			$this->include_scripts,
 | 
						|
			Util\option_to_array(Plugin::options()->delay_js_includes)
 | 
						|
		);
 | 
						|
 | 
						|
		// Enable delay adsense.
 | 
						|
		if (Plugin::options()->delay_js_adsense) {
 | 
						|
			$this->include_scripts[] = 'adsbygoogle.js';
 | 
						|
		}
 | 
						|
 | 
						|
		$this->include_scripts = apply_filters('debloat/delay_js_includes', $this->include_scripts);
 | 
						|
	}
 | 
						|
 | 
						|
	/**
 | 
						|
	 * Minify the JS file, if possible.
 | 
						|
	 *
 | 
						|
	 * @param Script $script
 | 
						|
	 * @return void
 | 
						|
	 */
 | 
						|
	public function minify_js(Script $script)
 | 
						|
	{
 | 
						|
		$minifier = new Minifier($script);
 | 
						|
		$minifier->process();
 | 
						|
	}
 | 
						|
} |