array(),
'enqueued' => array(),
'dequeued' => array(),
'finalState' => array(),
);
private $google_fonts_icon_family_cache = array();
private $smart_delay_plan = null;
/**
* Nesting level of FastLayer's own HTML output buffer.
*
* The optimizer is the *inner* buffer (the page cache starts first), so its
* level is stored at open time and the shutdown flusher only ever closes a
* buffer at that exact level. A foreign buffer opened on top is left alone.
*
* @var int
*/
private $buffer_level = 0;
private $allowed_js_types = array(
'text/javascript',
'module',
'application/javascript',
'application/ecmascript',
'application/x-ecmascript',
'application/x-javascript',
'text/ecmascript',
'text/javascript1.0',
'text/javascript1.1',
'text/javascript1.2',
'text/javascript1.3',
'text/javascript1.4',
'text/javascript1.5',
'text/jscript',
'text/livescript',
'text/x-ecmascript',
'text/x-javascript',
);
public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
add_action( 'wp_default_styles', array( $this, 'debug_trace_block_css_registration' ), 1000 );
add_action( 'wp_enqueue_scripts', array( $this, 'minify_css' ), 999 );
add_action( 'wp_enqueue_scripts', array( $this, 'maybe_optimize_gutenberg_block_css' ), 999 );
add_action( 'wp_enqueue_scripts', array( $this, 'minify_js' ), 999 );
add_action( 'wp_print_styles', array( $this, 'maybe_optimize_gutenberg_block_css_on_print_styles' ), 1 );
add_action( 'wp_footer', array( $this, 'maybe_optimize_gutenberg_block_css_on_footer_fallback' ), 1 );
add_action( 'wp_footer', array( $this, 'print_block_css_console_report' ), 10000 );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_inline_critical_css' ), 0 );
add_filter( 'style_loader_tag', array( $this, 'maybe_combine_css_tag' ), 1, 4 );
add_filter( 'style_loader_tag', array( $this, 'maybe_add_css_preload' ), 10, 4 );
add_filter( 'style_loader_src', array( $this, 'enforce_google_fonts_display_swap' ), 20, 2 );
add_filter( 'wp_resource_hints', array( $this, 'add_performance_resource_hints' ), 10, 2 );
add_action( 'wp_head', array( $this, 'print_font_preloads' ), 1 );
add_action( 'wp_head', array( $this, 'print_font_display_overrides' ), 9 );
add_action( 'wp_enqueue_scripts', array( $this, 'maybe_dequeue_dashicons' ), 100 );
add_filter( 'script_loader_tag', array( $this, 'maybe_combine_js_tag' ), 1, 3 );
add_filter( 'script_loader_tag', array( $this, 'maybe_add_defer_attribute' ), 10, 3 );
add_filter( 'script_loader_tag', array( $this, 'maybe_add_async_attribute' ), 20, 3 );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_delay_js_loader' ), 9999 );
add_action( 'template_redirect', array( $this, 'start_html_buffer' ), 1 );
add_action( 'shutdown', array( $this, 'flush_html_buffer' ), 1 );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_lazyload_scripts' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_instant_click_scripts' ) );
add_filter( 'the_content', array( $this, 'lazyload_images' ) );
}
private function is_css_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_minify_css'] ) && '1' === (string) $options['enable_minify_css'];
}
private function is_js_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_minify_js'] ) && '1' === (string) $options['enable_minify_js'];
}
private function is_defer_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_js_defer'] ) && '1' === (string) $options['enable_js_defer'];
}
private function is_safe_mode_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_js_safe_mode'] ) && '1' === (string) $options['enable_js_safe_mode'];
}
private function is_async_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_js_async'] ) && '1' === (string) $options['enable_js_async'];
}
private function is_smart_delay_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_js_smart_delay'] ) && '1' === (string) $options['enable_js_smart_delay'];
}
private function is_smart_delay_debug_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_js_smart_delay_debug'] ) && '1' === (string) $options['enable_js_smart_delay_debug'];
}
private function get_smart_delay_timeout() {
$options = get_option( 'fastlayer_options', array() );
$timeout = isset( $options['smart_js_delay_timeout'] ) ? absint( $options['smart_js_delay_timeout'] ) : 6000;
return max( 1000, min( 15000, $timeout ) );
}
private function get_smart_delay_exclusion_patterns() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['smart_js_delay_exclusions'] ) ? preg_split( '/\r?\n/', trim( (string) $options['smart_js_delay_exclusions'] ) ) : array();
}
private function get_smart_delay_critical_handles() {
return array(
'jquery',
'jquery-core',
'jquery-migrate',
'elementor-frontend',
'swiper',
'wp-hooks',
'wp-i18n',
'wc-cart',
'wc-checkout',
'woocommerce',
'wc-add-to-cart',
'wc-cart-fragments',
'fastlayer-imagekit-runtime-debug',
'fastlayer-lazyload',
'fastlayer-lqip',
'fastlayer-delay-loader',
'fastlayer-smart-delay-loader',
'navigation',
'menu',
'webpack-runtime',
);
}
private function get_smart_delay_critical_keywords() {
return array(
'jquery',
'elementor-frontend',
'swiper',
'navigation',
'menu',
'webpack',
'wp-hooks',
'wp-i18n',
'wc-checkout',
'wc-cart',
'woocommerce',
'checkout',
'cart',
'login',
'session',
'lazyload',
'wpfastlayerlazyload',
'fastlayer-imagekit-runtime-debug',
);
}
private function is_smart_delay_excluded( $handle, $src, $tag ) {
$handle_lc = strtolower( (string) $handle );
$src_lc = strtolower( (string) $src );
if ( in_array( $handle_lc, $this->get_smart_delay_critical_handles(), true ) ) {
return true;
}
foreach ( $this->get_smart_delay_critical_keywords() as $keyword ) {
if ( '' !== $keyword && ( false !== strpos( $handle_lc, $keyword ) || false !== strpos( $src_lc, $keyword ) ) ) {
return true;
}
}
$custom_patterns = $this->get_smart_delay_exclusion_patterns();
foreach ( $custom_patterns as $raw_pattern ) {
$pattern = trim( (string) $raw_pattern );
if ( '' === $pattern ) {
continue;
}
if ( 0 === stripos( $pattern, 'handle:' ) ) {
$needle = trim( substr( $pattern, 7 ) );
if ( '' !== $needle && false !== strpos( $handle_lc, strtolower( $needle ) ) ) {
return true;
}
continue;
}
if ( 0 === stripos( $pattern, 'regex:' ) ) {
$regex = trim( substr( $pattern, 6 ) );
if ( $this->is_valid_regex_pattern( $regex ) && ( preg_match( $regex, (string) $src ) === 1 || preg_match( $regex, (string) $handle ) === 1 || preg_match( $regex, (string) $tag ) === 1 ) ) {
return true;
}
continue;
}
if ( $this->is_valid_regex_pattern( $pattern ) && ( preg_match( $pattern, (string) $src ) === 1 || preg_match( $pattern, (string) $handle ) === 1 || preg_match( $pattern, (string) $tag ) === 1 ) ) {
return true;
}
if ( '' !== $src && $this->pattern_matches_url( $pattern, (string) $src ) ) {
return true;
}
if ( false !== strpos( $handle_lc, strtolower( $pattern ) ) || false !== strpos( $src_lc, strtolower( $pattern ) ) ) {
return true;
}
}
return false;
}
private function is_fastlayer_runtime_handle( $handle ) {
return in_array(
strtolower( (string) $handle ),
array( 'fastlayer-lqip', 'fastlayer-delay-loader', 'fastlayer-smart-delay-loader' ),
true
);
}
private function is_valid_regex_pattern( $pattern ) {
if ( '' === (string) $pattern ) {
return false;
}
// The @ operator silences the warning PHP raises for a malformed
// pattern; only the boolean result matters here.
$result = @preg_match( $pattern, '' );
return false !== $result;
}
/**
* Handles that can realistically be printed on the current request.
*
* WordPress registers hundreds of handles per request (admin-only, block
* assets, REST assets...) that never reach the page. Running the closure
* over the whole registry would let unrelated, never-printed handles
* retract otherwise safe delay candidates, so the closure is scoped to the
* enqueue queue plus everything it transitively depends on.
*/
private function get_smart_delay_active_handles() {
global $wp_scripts;
$active = array();
if ( ! isset( $wp_scripts ) || ! is_object( $wp_scripts ) || ! isset( $wp_scripts->queue ) || ! is_array( $wp_scripts->queue ) ) {
return $active;
}
$queue = $wp_scripts->queue;
$stack = $queue;
while ( $stack ) {
$handle = (string) array_pop( $stack );
if ( '' === $handle || isset( $active[ $handle ] ) ) {
continue;
}
if ( ! isset( $wp_scripts->registered[ $handle ] ) ) {
continue;
}
$active[ $handle ] = true;
$deps = isset( $wp_scripts->registered[ $handle ]->deps ) && is_array( $wp_scripts->registered[ $handle ]->deps )
? $wp_scripts->registered[ $handle ]->deps
: array();
foreach ( $deps as $dep ) {
$dep = (string) $dep;
if ( '' !== $dep && ! isset( $active[ $dep ] ) ) {
$stack[] = $dep;
}
}
}
return $active;
}
/**
* Whether a handle is an acceptable delay candidate, ignoring the
* dependency graph entirely.
*/
private function is_smart_delay_candidate( $handle, $src, $tag ) {
$handle = (string) $handle;
if ( '' === $handle ) {
return false;
}
if ( $this->is_fastlayer_runtime_handle( $handle ) ) {
return false;
}
if ( $this->is_smart_delay_excluded( $handle, $src, $tag ) ) {
return false;
}
if ( $this->is_excluded_js( $src ) || $this->is_excluded_js_defer( $src ) ) {
return false;
}
if ( $this->is_safe_mode_enabled() && '' !== (string) $src && $this->is_internal_url( $src ) ) {
return false;
}
// Nothing to restore: either no external source and no inline body.
if ( '' === (string) $src && ! preg_match( '##is', (string) $tag ) ) {
return false;
}
return true;
}
/**
* Builds the Delay JS execution plan from the WordPress dependency graph.
*
* The invariant enforced here is:
*
* A handle may only be delayed when every script that depends on it,
* directly or transitively, is delayed as well.
*
* WordPress prints dependencies before dependents. If a dependency is
* deferred to interaction time while a dependent still runs during
* parsing, the dependent observes a half-initialised global
* (for example `backbone` executing `_.extend` before a delayed
* `underscore` has defined `_`), which is a hard console error.
*
* The set is therefore closed upwards over the "depends on" relation:
*
* - a delayed handle drags its eligible dependents into the set, so the
* whole chain is restored together in dependency order;
* - if a dependent cannot be delayed (a critical handle such as
* `elementor-frontend`, or a user exclusion), the dependency is
* retracted from the set instead, so the dependent still finds it
* already executed.
*
* Nothing here forces a critical handle to be delayed, so Delay JS stays
* selective: jQuery, Elementor frontend, Swiper, wp-hooks and friends are
* never delayed, and only genuine non-critical tails end up in the set.
*
* @return array{delayed:array,priority:array}
*/
private function get_smart_delay_plan() {
if ( null !== $this->smart_delay_plan ) {
return $this->smart_delay_plan;
}
$empty = array(
'delayed' => array(),
'priority' => array(),
);
global $wp_scripts;
if ( ! isset( $wp_scripts ) || ! is_object( $wp_scripts ) || ! isset( $wp_scripts->registered ) || ! is_array( $wp_scripts->registered ) ) {
$this->smart_delay_plan = $empty;
return $this->smart_delay_plan;
}
$registered = $wp_scripts->registered;
$active = $this->get_smart_delay_active_handles();
if ( empty( $active ) ) {
$this->smart_delay_plan = $empty;
return $this->smart_delay_plan;
}
$deps = array();
$dependents = array();
$candidate = array();
foreach ( $active as $handle => $unused ) {
if ( ! isset( $registered[ $handle ] ) ) {
continue;
}
$script = $registered[ $handle ];
$handle_deps = ( isset( $script->deps ) && is_array( $script->deps ) ) ? $script->deps : array();
$clean_deps = array();
foreach ( $handle_deps as $dep ) {
$dep = (string) $dep;
if ( '' === $dep || ! isset( $registered[ $dep ] ) ) {
continue;
}
$clean_deps[] = $dep;
$dependents[ $dep ][] = $handle;
}
$deps[ $handle ] = $clean_deps;
$candidate[ $handle ] = $this->is_smart_delay_candidate(
$handle,
isset( $script->src ) ? $script->src : '',
''
);
}
// Seed the set with every eligible handle.
$delayed = array();
foreach ( $candidate as $handle => $is_candidate ) {
if ( $is_candidate ) {
$delayed[ $handle ] = true;
}
}
// Close upwards over dependents. Bounded so a malformed graph
// (dependency cycle reported by third-party code) cannot spin.
$max_passes = count( $delayed ) + 5;
for ( $pass = 0; $pass < $max_passes; $pass++ ) {
$changed = false;
$retract = array();
foreach ( array_keys( $delayed ) as $handle ) {
if ( ! isset( $dependents[ $handle ] ) ) {
continue;
}
foreach ( $dependents[ $handle ] as $dependent ) {
if ( isset( $delayed[ $dependent ] ) ) {
continue;
}
if ( ! empty( $candidate[ $dependent ] ) ) {
$delayed[ $dependent ] = true;
$changed = true;
continue;
}
// The dependent must run eagerly and needs this handle,
// so this handle has to stay eager too.
$retract[ $handle ] = true;
}
}
if ( ! empty( $retract ) ) {
foreach ( array_keys( $retract ) as $handle ) {
if ( isset( $delayed[ $handle ] ) ) {
unset( $delayed[ $handle ] );
$changed = true;
}
}
}
if ( ! $changed ) {
break;
}
}
// Topological depth across the delayed sub-graph, so the runtime can
// restore dependencies before dependents instead of trusting DOM order.
$priority = array();
$resolving = array();
$depth = function ( $handle ) use ( &$depth, &$priority, &$resolving, $deps, $delayed ) {
if ( isset( $priority[ $handle ] ) ) {
return $priority[ $handle ];
}
if ( isset( $resolving[ $handle ] ) ) {
// Defensive: treat a cycle as depth 0 rather than recursing.
return 0;
}
$resolving[ $handle ] = true;
$max = 0;
foreach ( $deps[ $handle ] as $dep ) {
if ( ! isset( $delayed[ $dep ] ) ) {
continue;
}
$dep_depth = $depth( $dep ) + 1;
if ( $dep_depth > $max ) {
$max = $dep_depth;
}
}
unset( $resolving[ $handle ] );
$priority[ $handle ] = $max;
return $max;
};
foreach ( array_keys( $delayed ) as $handle ) {
$depth( $handle );
}
$this->smart_delay_plan = array(
'delayed' => $delayed,
'priority' => $priority,
);
return $this->smart_delay_plan;
}
/**
* Restore order for one block of a handle's delayed script group.
*
* The value is ` * + `, which keeps two
* guarantees at once:
*
* - every block of a dependency sits below every block of its dependent,
* because a dependency always has a strictly smaller depth;
* - inside one handle the order WordPress printed is kept:
* translations -> "before" -> file -> "after".
*/
private function get_smart_delay_priority( $handle, $slot = 'script' ) {
$plan = $this->get_smart_delay_plan();
$depth = isset( $plan['priority'][ $handle ] ) ? (int) $plan['priority'][ $handle ] : 0;
$base = $depth * self::SMART_DELAY_PRIORITY_STRIDE;
$slots = array(
'translations' => 0,
'before' => 0,
'script' => 1,
'after' => 2,
);
$offset = isset( $slots[ $slot ] ) ? (int) $slots[ $slot ] : 1;
return $base + $offset;
}
private function is_smart_delay_handle_delayed( $handle ) {
$plan = $this->get_smart_delay_plan();
return isset( $plan['delayed'][ (string) $handle ] );
}
/**
* Final authority for whether a handle's script group is delayed.
*
* The dependency-closure plan is the only decision point: a handle is
* delayed when the whole chain of scripts depending on it is delayed too,
* so nothing can execute before one of its dependencies.
*
* The guard below only detects markup this plugin already produced, so a
* delayed sibling block can never veto the decision for the group it
* belongs to.
*/
private function should_apply_smart_delay( $tag, $handle, $src ) {
if ( false !== strpos( $tag, 'data-wpfl-delayed=' ) || false !== stripos( $tag, 'wpfastlayerlazyloadscript' ) ) {
return false;
}
if ( ! $this->is_smart_delay_handle_delayed( $handle ) ) {
return false;
}
return true;
}
public function maybe_add_async_attribute( $tag, $handle, $src ) {
if ( ! $this->is_async_enabled() || $this->is_defer_enabled() || $this->is_smart_delay_enabled() || is_admin() || empty( $src ) || false !== strpos( $tag, ' async' ) || false !== strpos( $tag, ' defer' ) ) {
return $tag;
}
if ( $this->is_excluded_js( $src ) || $this->is_excluded_js_defer( $src ) ) {
return $tag;
}
if ( $this->is_safe_mode_enabled() && $this->is_internal_url( $src ) ) {
return $tag;
}
return str_replace( '#is';
// WordPress prints an explicit closing tag, but a third-party filter may
// hand over ``. Normalizing it keeps the block matching
// below from swallowing a sibling block while looking for ``.
$tag = preg_replace( '/(', (string) $tag );
if ( ! is_string( $tag ) || ! preg_match_all( $pattern, $tag, $matches ) ) {
return $tag;
}
$slots = array();
foreach ( $matches[0] as $block ) {
$slot = $this->get_smart_delay_block_slot( $block, $handle );
// Unknown or non-JavaScript block in the group: keep the group
// eager so WordPress keeps printing it in its own order.
if ( null === $slot ) {
return $tag;
}
$slots[] = $slot;
}
if ( empty( $slots ) ) {
return $tag;
}
$handle = (string) $handle;
$index = 0;
$wrapped = preg_replace_callback(
$pattern,
function ( $match ) use ( $handle, $slots, &$index ) {
$slot = $slots[ $index ];
$index++;
return $this->delay_smart_delay_block( $match[0], $handle, $slot );
},
$tag
);
return null === $wrapped ? $tag : $wrapped;
}
private function is_allowed_js_type( $type ) {
return in_array( trim( strtolower( $type ) ), $this->allowed_js_types, true );
}
public function enqueue_delay_js_loader() {
if ( is_admin() || ! $this->is_smart_delay_enabled() ) {
return;
}
$this->enqueue_smart_delay_js_loader();
}
private function enqueue_smart_delay_js_loader() {
$config = array(
'timeout' => $this->get_smart_delay_timeout(),
'debug' => $this->is_smart_delay_debug_enabled(),
'events' => array( 'scroll', 'mousemove', 'click', 'touchstart', 'keydown' ),
'prefix' => '[WP FastLayer Delay]',
);
$config_json = wp_json_encode( $config );
if ( ! is_string( $config_json ) || '' === $config_json ) {
return;
}
$loader = '(function(cfg){if(!cfg||window.__fastLayerSmartDelayInit){return;}window.__fastLayerSmartDelayInit=true;var startedAt=Date.now();var restored=false;var fallbackTimer=0;var flushTimer=0;var selector="script[type=\"text/wp-fastlayer-delay\"],script[type=\"wp-fastlayer-delay\"]";var FLUSH_AFTER=15000;function log(label,payload){if(!cfg.debug||!window.console){return;}if(window.console.groupCollapsed){window.console.groupCollapsed(cfg.prefix+" "+label);}if(typeof payload!=="undefined"){try{window.console.log(payload);}catch(e){}}if(window.console.groupEnd){window.console.groupEnd();}}function copyAttributes(from,to){var attrs=from&&from.attributes?from.attributes:[];for(var i=0;i=totalExternals&&0===waiters.length){finish();}}function runInline(item){var info=restoreBlock(item.node,null);if(!info){return;}restoredCount++;log("restored_inline",info);}function release(){while(waiters.length&&executed>=waiters[0].need){runInline(waiters.shift());}}function onExternalDone(){executed++;release();maybeFinish();}for(i=0;i true ) );
wp_enqueue_script( 'fastlayer-smart-delay-loader' );
wp_add_inline_script( 'fastlayer-smart-delay-loader', $loader );
}
private function get_exclude_css_patterns() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['exclude_css_files'] ) ? preg_split( '/\r?\n/', trim( $options['exclude_css_files'] ) ) : array();
}
private function get_exclude_js_patterns() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['exclude_js_files'] ) ? preg_split( '/\r?\n/', trim( $options['exclude_js_files'] ) ) : array();
}
private function get_exclude_js_defer_patterns() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['exclude_js_defer'] ) ? preg_split( '/\r?\n/', trim( $options['exclude_js_defer'] ) ) : array();
}
private function is_excluded_css( $url ) {
$patterns = array_merge( $this->get_exclude_css_patterns(), $this->get_icon_css_exclusion_patterns() );
return $this->is_excluded_by_patterns( $url, $patterns );
}
private function is_excluded_js( $url ) {
return $this->is_excluded_by_patterns( $url, $this->get_exclude_js_patterns() );
}
private function is_excluded_js_defer( $url ) {
return $this->is_excluded_by_patterns( $url, $this->get_exclude_js_defer_patterns() );
}
private function is_css_combine_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_css_combine'] ) && '1' === (string) $options['enable_css_combine'];
}
private function is_js_combine_enabled() {
$options = get_option( 'fastlayer_options', array() );
return isset( $options['enable_js_combine'] ) && '1' === (string) $options['enable_js_combine'];
}
public function maybe_combine_css_tag( $tag, $handle, $href, $media ) {
if ( ! $this->is_css_combine_enabled() || is_admin() || empty( $href ) ) {
return $tag;
}
if ( $this->is_icon_stylesheet( $href ) ) {
$this->debug_log( 'icon_stylesheet_detected', array( 'handle' => $handle, 'url' => $href, 'source' => 'combine_tag' ) );
$this->debug_log( 'stylesheet_excluded_from_unused_css', array( 'handle' => $handle, 'url' => $href, 'source' => 'combine_tag' ) );
return $tag;
}
if ( $this->is_excluded_css( $href ) || ! $this->is_internal_url( $href ) ) {
return $tag;
}
if ( empty( $this->combined_css_groups ) ) {
$this->combined_css_groups = $this->get_combined_css_groups();
}
$media_key = $this->get_css_group_key( $media );
if ( ! isset( $this->combined_css_groups[ $media_key ] ) ) {
return $tag;
}
$group_handles = $this->combined_css_groups[ $media_key ];
if ( count( $group_handles ) < 2 || ! in_array( $handle, $group_handles, true ) ) {
return $tag;
}
if ( ! isset( $this->combined_css_urls[ $media_key ] ) ) {
$this->combined_css_urls[ $media_key ] = $this->generate_combined_css_url( $group_handles );
}
if ( '' === $this->combined_css_urls[ $media_key ] ) {
return $tag;
}
if ( $handle !== $group_handles[0] ) {
return '';
}
return $this->render_registered_stylesheet_tag( $this->combined_css_urls[ $media_key ], $media_key, $media, $tag );
}
/**
* Renders the combined stylesheet through WordPress's own style printer.
*
* The combined URL is registered with wp_register_style() and the tag is
* produced by WP_Styles::do_item(), so markup, media handling and escaping
* stay exactly in line with every other stylesheet WordPress prints. The
* is still substituted in via the style_loader_tag filter: that
* filter is the point where the group order is already fixed and where a
* single combined request can replace its group of originals without
* duplicating or reordering anything.
*/
private function render_registered_stylesheet_tag( $combined_url, $media_key, $media, $fallback_tag ) {
$combined_handle = 'fastlayer-combined-css-' . $media_key;
if ( ! wp_style_is( $combined_handle, 'registered' ) ) {
wp_register_style( $combined_handle, $combined_url, array(), FASTLAYER_VERSION, $media );
}
// WP_Styles::do_item() is the only supported way to print a stylesheet
// tag. If the styles registry is unavailable, fall back to the original
// uncombined tag rather than hand-building markup.
global $wp_styles;
if ( ! isset( $wp_styles ) || ! ( $wp_styles instanceof \WP_Styles ) ) {
return $fallback_tag;
}
ob_start();
$wp_styles->do_item( $combined_handle );
$tag = (string) ob_get_clean();
if ( '' === trim( $tag ) ) {
return $fallback_tag;
}
return $tag;
}
public function maybe_combine_js_tag( $tag, $handle, $src ) {
if ( ! $this->is_js_combine_enabled() || is_admin() || empty( $src ) ) {
return $tag;
}
if ( $this->is_excluded_js( $src ) || ! $this->is_internal_url( $src ) ) {
return $tag;
}
if ( empty( $this->combined_js_handles ) ) {
$this->combined_js_handles = $this->get_combined_js_handles();
}
if ( count( $this->combined_js_handles ) < 2 || ! in_array( $handle, $this->combined_js_handles, true ) ) {
return $tag;
}
if ( '' === $this->combined_js_url ) {
$this->combined_js_url = $this->generate_combined_js_url();
}
if ( '' === $this->combined_js_url ) {
return $tag;
}
if ( $handle !== $this->combined_js_handles[0] ) {
return '';
}
return $this->render_registered_script_tag( $this->combined_js_url, $tag );
}
/**
* Renders the combined script through WordPress's own script printer.
*
* As with the combined stylesheet, the URL is handed to wp_register_script()
* and the tag is generated by WP_Scripts::do_item(), then substituted for
* the group's first