Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
22.81% covered (danger)
22.81%
13 / 57
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageLazyLoader
22.81% covered (danger)
22.81%
13 / 57
16.67% covered (danger)
16.67%
1 / 6
203.99
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 apply_lazy_loading_to_blocks
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 enqueue_lazy_loader
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 get_inline_script
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 clean_content
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 apply_lazy_loading
39.13% covered (danger)
39.13%
9 / 23
0.00% covered (danger)
0.00%
0 / 1
22.43
1<?php
2
3namespace NewfoldLabs\WP\Module\Performance\Images;
4
5use DOMDocument;
6use Exception;
7
8/**
9 * Manages the initialization and application of lazy loading for images.
10 */
11class ImageLazyLoader {
12
13    /**
14     * Exclusion rules for lazy loading.
15     * Defines classes and attributes that should prevent lazy loading from being applied to specific images.
16     *
17     * @var array
18     */
19    private static $exclusions = array(
20        'classes'    => array(
21            'nfd-performance-not-lazy',
22            'a3-notlazy',
23            'disable-lazyload',
24            'no-lazy',
25            'no-lazyload',
26            'skip-lazy',
27        ),
28        'attributes' => array(
29            'data-lazy-src',
30            'data-crazy-lazy="exclude"',
31            'data-no-lazy',
32            'data-no-lazy="1"',
33        ),
34    );
35
36    /**
37     * List of content filters where lazy loading will be applied.
38     * These filters modify various types of WordPress content.
39     *
40     * @var array
41     */
42    private static $content_filters = array(
43        'the_content',
44        'post_thumbnail_html',
45        'widget_text',
46        'get_avatar',
47    );
48
49    /**
50     * Constructor to initialize the lazy loading feature.
51     */
52    public function __construct() {
53        // Enqueue the lazy loader script with inline settings.
54        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_lazy_loader' ) );
55
56        // Add filters to apply lazy loading to various content types.
57        foreach ( self::$content_filters as $filter ) {
58            add_filter( $filter, array( $this, 'apply_lazy_loading' ) );
59        }
60
61        // Hook into Gutenberg block rendering to apply lazy loading.
62        add_filter( 'render_block', array( $this, 'apply_lazy_loading_to_blocks' ), 10, 2 );
63    }
64
65    /**
66     * Applies lazy loading to images within Gutenberg blocks.
67     *
68     * @param string $block_content The HTML content of the block.
69     * @param array  $block The block data array.
70     * @return string Modified block content with lazy loading applied.
71     */
72    public function apply_lazy_loading_to_blocks( $block_content, $block ) {
73        // Only target core/image blocks or other blocks with images.
74        if ( 'core/image' === $block['blockName'] || strpos( $block_content, '<img' ) !== false ) {
75            return $this->apply_lazy_loading( $block_content );
76        }
77
78        return $block_content;
79    }
80
81    /**
82     * Enqueues the lazy loader script file and adds inline exclusion settings.
83     */
84    public function enqueue_lazy_loader() {
85        $script_path = NFD_PERFORMANCE_BUILD_DIR . '/assets/image-lazy-loader.min.js';
86        $script_url  = NFD_PERFORMANCE_BUILD_URL . '/assets/image-lazy-loader.min.js';
87
88        // Register the script with version based on file modification time.
89        wp_register_script(
90            'nfd-performance-lazy-loader',
91            $script_url,
92            array(),
93            file_exists( $script_path ) ? filemtime( $script_path ) : false,
94            true
95        );
96
97        // Inject the exclusion settings into the script.
98        wp_add_inline_script(
99            'nfd-performance-lazy-loader',
100            $this->get_inline_script(),
101            'before'
102        );
103
104        wp_enqueue_script( 'nfd-performance-lazy-loader' );
105    }
106
107    /**
108     * Generates the inline script to define lazy loading exclusions.
109     * This script populates the `window.nfdPerformance` object with exclusion rules.
110     *
111     * @return string JavaScript code to inline.
112     */
113    private function get_inline_script() {
114        return 'window.nfdPerformance = window.nfdPerformance || {};
115        window.nfdPerformance.imageOptimization = window.nfdPerformance.imageOptimization || {};
116        window.nfdPerformance.imageOptimization.lazyLoading = ' . wp_json_encode( self::$exclusions ) . ';';
117    }
118
119    /**
120     * Cleans up content by replacing specific patterns with replacements.
121     * This method is used to sanitize or modify content before lazy loading is applied.
122     *
123     * @param string $pattern Regular expression pattern to match.
124     * @param string $search String to search for in the content.
125     * @param string $replace String to replace the search string with.
126     * @param string $content The content to be cleaned.
127     * @return string The cleaned content.
128     */
129    public function clean_content( $pattern, $search, $replace, $content ) {
130
131        if ( empty( $content ) || empty( $pattern ) || empty( $search ) ) {
132            return $content;
133        }
134
135        $content = preg_replace_callback(
136            $pattern,
137            function ( $matches ) use ( $search, $replace ) {
138                $cleanedIframe = str_replace( $search, $replace, $matches[0] );
139                return $cleanedIframe;
140            },
141            $content
142        );
143        return $content;
144    }
145
146    /**
147     * Applies lazy loading to images in HTML content.
148     * Skips images with specified exclusion classes or attributes.
149     *
150     * @param string $content The HTML content to process.
151     * @return string Modified HTML content with lazy loading applied, or original content on error.
152     */
153    public function apply_lazy_loading( $content ) {
154        // Return unmodified content if it is empty.
155        if ( empty( $content ) ) {
156            return $content;
157        }
158
159        $exclusion_classes    = self::$exclusions['classes'];
160        $exclusion_attributes = self::$exclusions['attributes'];
161
162        $content = preg_replace_callback(
163            '/<img\b([^>]*)>/i',
164            function ( $matches ) use ( $exclusion_classes, $exclusion_attributes ) {
165                $img_tag = $matches[0];
166
167                // check for exclusion classes
168                if ( preg_match( '/class=["\']([^"\']+)["\']/', $img_tag, $class_match ) ) {
169                    $classes = explode( ' ', $class_match[1] );
170                    foreach ( $exclusion_classes as $excluded ) {
171                        if ( in_array( $excluded, $classes, true ) ) {
172                            return $img_tag;
173                        }
174                    }
175                }
176
177                // Check for exclusion attributes
178                foreach ( $exclusion_attributes as $excluded_attr ) {
179                    if ( preg_match( '/' . preg_quote( $excluded_attr, '/' ) . '(\s*=\s*["\'][^"\']*["\'])?/', $img_tag ) ) {
180                        return $img_tag;
181                    }
182                }
183
184                // Not add lazy if already present
185                if ( preg_match( '/\bloading\s*=\s*["\']?lazy["\']?/i', $img_tag ) ) {
186                    return $img_tag;
187                }
188
189                // add loading="lazy" attribute
190                return preg_replace( '/<img\b/', '<img loading="lazy"', $img_tag, 1 );
191            },
192            $content
193        );
194
195        return $content;
196    }
197}