options = wp_optimize_minify_config()->get();
if ($this->should_process()) {
$this->process();
}
}
/**
* Returns singleton instance object
*
* @return WP_Optimize_Delay_JS
*/
public static function instance() {
static $_instance = null;
if (null === $_instance) {
$_instance = new self();
}
return $_instance;
}
/**
* If JavaScript optimizations are enabled and delay or preload JavaScript is enabled, process the output.
*
* @return bool
*/
private function should_process(): bool {
return $this->options['enabled'] && $this->options['enable_js'] && ($this->is_delay_js_enabled() || $this->is_preload_js_enabled()) && !$this->is_edit_mode();
}
/**
* Main processing function to manage JavaScript optimizations.
*
* @return void
*/
private function process() {
add_action('template_redirect', array($this, 'start_buffering'));
if ($this->is_delay_js_enabled()) {
$this->register_callback_to_output_delay_js_script();
}
}
/**
* Start output buffering with callback to optimize the content for delaying JavaScript execution.
*
* @return void
*/
public function start_buffering() {
static $registered = false;
if (!$registered) {
ob_start(array($this, 'optimize_buffered_content_for_delaying_js_execution'));
$registered = true;
}
}
/**
* A callback function that replaces script tags to make them delayable when the Delay JS option is enabled,
* and generates a list of tags to preload scripts when the Preload JavaScript Files option is enabled.
*
* @param string $content The content to be processed.
* @return string The modified content.
*/
private function optimize_buffered_content_for_delaying_js_execution($content) {
$content = $this->maybe_preload_js($content);
return $this->maybe_delay_js($content);
}
/**
* Adds actions to preload JavaScript if the feature is enabled.
*
* @param string $content
* @return string
*/
private function maybe_preload_js($content) {
if ($this->is_preload_js_enabled()) {
$content = $this->preload_js($content);
}
return $content;
}
/**
* Check if Preload JS is enabled.
*
* @return bool
*/
private function is_preload_js_enabled() {
return $this->options['enable_preload_js'];
}
/**
* Updates the content to add preload tags.
*
* @param string $content
* @return string
*/
private function preload_js($content) {
$pattern = $this->get_scripts_pattern();
$scripts = array();
preg_match_all($pattern, $content, $matches);
foreach ($matches[1] as $attributes_string) {
$attributes = WP_Optimize_Utils::parse_attributes($attributes_string);
if (!empty($attributes['src']) && !$this->is_excluded_script($attributes['src'])) {
$scripts[] = $attributes['src'];
}
}
$updated_content = preg_replace('/\<(head)\>/i', "<$1>\n".$this->generate_preload_scripts_tags($scripts), $content, 1);
if (!is_null($updated_content)) {
$content = $updated_content;
}
return $content;
}
/**
* Adds actions to delay JavaScript execution if the feature is enabled.
*
* @param string $content
* @return string
*/
private function maybe_delay_js($content) {
if ($this->is_delay_js_enabled()) {
$content = $this->delay_js($content);
}
return $content;
}
/**
* Check if Delay JS is enabled.
*
* @return bool
*/
private function is_delay_js_enabled() {
return $this->options['enable_delay_js'];
}
/**
* Updates the content to delay JavaScript execution.
*
* @param string $content
* @return string
*/
private function delay_js($content) {
$pattern = $this->get_scripts_pattern();
$updated_content = preg_replace_callback($pattern, array($this, 'update_script_tags_callback'), $content);
if (!is_null($updated_content)) {
$content = $updated_content;
}
return $content;
}
/**
* Output the Delay JS Script.
*
* @return void
*/
public function output_delay_js_script() {
$min_or_not_internal = WP_Optimize()->get_min_or_not_internal_string();
echo '';
}
/**
* Retrieve the list of excluded scripts from the options.
*
* @return array
*/
private function get_excluded_scripts() {
static $exceptions;
if (!is_null($exceptions)) return $exceptions;
$exceptions = $this->options['exclude_delay_js'];
$exceptions = is_array($exceptions) ? $exceptions : preg_split('#(\n|\r)#', $exceptions);
$exceptions = $exceptions ? array_filter(array_map('trim', $exceptions)) : array();
foreach ($exceptions as &$exception) {
$exception = str_replace('*', '.*', $exception);
}
return $exceptions;
}
/**
* Checks if minification is enabled for JavaScript files.
*
* @return boolean
*/
private function is_minification_enabled_for_js() {
return isset($this->options['enable_js_minification']) && $this->options['enable_js_minification'];
}
/**
* Verify whether the script is included in the excluded scripts list.
*
* @param string $script - script url
* @return bool
*/
private function is_excluded_script($script) {
$excluded_scripts = $this->get_excluded_scripts();
if (empty($excluded_scripts)) return false;
if ($this->is_minification_enabled_for_js()) {
$minified_scripts_to_exclude = $this->get_minified_scripts_to_exclude();
foreach ($minified_scripts_to_exclude as $minified_script) {
if (preg_match('#'.$minified_script.'#i', $script)) return true;
}
}
foreach ($excluded_scripts as $exception) {
if (preg_match('#'.$exception.'#i', $script)) return true;
}
return false;
}
/**
* Returns the list of minified JavaScript scripts to exclude from processing.
*
* @return array
*/
private function get_minified_scripts_to_exclude() {
static $scripts_to_exclude = null;
static $minified_files = null;
if (!is_null($scripts_to_exclude)) {
return $scripts_to_exclude;
}
$scripts_to_exclude = array();
// Get the list of excluded scripts from options.
$excluded_scripts = $this->get_excluded_scripts();
if ($this->is_minification_enabled_for_js()) {
if (is_null($minified_files)) {
$minified_files = WP_Optimize_Minify_Cache_Functions::get_cached_files(0, false);
}
} else {
$minified_files = array();
}
if (!empty($minified_files['js'])) {
foreach ($minified_files['js'] as $file_info) {
if (isset($file_info['log']->files)) {
$files = get_object_vars($file_info['log']->files);
// Loop through each file and match it with the exclusion patterns.
foreach ($files as $file) {
foreach ($excluded_scripts as $exception) {
if (preg_match('#' . $exception . '#i', $file->url)) {
// Store minified filename into result
$scripts_to_exclude[] = $file_info['filename'];
break(2);
}
}
}
}
}
}
return $scripts_to_exclude;
}
/**
* Generate HTML code with tags to preload JavaScript files.
*
* @param array $scripts array with urls to scripts
* @return string
*/
private function generate_preload_scripts_tags($scripts) {
$tags = array();
if (!empty($scripts)) {
foreach ($scripts as $script) {
if ($this->is_excluded_script($script)) continue;
$tags[] = '';
}
}
return implode("\n", $tags);
}
/**
* Callback function that updates the script tag if necessary, or keeps it as is when the script is in the excluded list.
*
* @param array $tag
* @return string
*/
private function update_script_tags_callback($tag) {
$attributes = WP_Optimize_Utils::parse_attributes($tag[1]);
// When the inline script content is not empty, we will change the script type to 'text/plain'.
$change_type = '' !== trim($tag[2]);
if (!empty($attributes['src']) && $this->is_excluded_script($attributes['src'])) {
return $tag[0];
} else {
return $this->build_delay_js_tag($attributes, $change_type, $tag[2]);
}
}
/**
* Updates tag attributes by changing src="value" to data-src="value". If $change_type is true,
* it also adds data-type="value" with the original type attribute value, and sets the original
* type value to 'text/plain'.
*
* @param array $attributes
* @param bool $change_type
* @param string $inline_script Inline script content
* @return string
*/
private function build_delay_js_tag($attributes, $change_type, $inline_script) {
if (!isset($attributes['data-no-delay-js'])) {
// Change 'src' attribute to 'data-src' attr for external scripts
if (!empty($attributes['src'])) {
$attributes['data-src'] = $attributes['src'];
unset($attributes['src']);
}
// Add data-type="value" with the original type attribute value and set type value to 'text/plain' for inline scripts
if ($change_type && (empty($attributes['type']) || $this->is_supported_type($attributes['type']))) {
$attributes['data-type'] = empty($attributes['type']) ? 'text/javascript' : $attributes['type'];
$attributes['type'] = 'text/plain';
}
}
return '';
}
/**
* Checks if the script type is in the supported list of types.
*
* @param string $type
* @return bool
*/
private function is_supported_type($type) {
$supported_types = array(
'text/javascript',
'module',
'application/javascript',
'application/ecmascript',
'application/x-ecmascript',
'application/x-javascript',
'text/x-ecmascript',
'text/x-javascript',
'text/javascript1.0',
'text/javascript1.1',
'text/javascript1.2',
'text/javascript1.3',
'text/javascript1.4',
'text/javascript1.5',
'text/jscript',
'text/livescript',
'text/ecmascript',
);
$type = strtolower($type);
return in_array($type, $supported_types);
}
/**
* Regular expression to get scripts.
*
* @return string
*/
private function get_scripts_pattern() {
return '/