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( '#]*>\s*.+?\s*#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 ``. $tag = preg_replace( '/(]*?)\/>/i', '$1>', (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