Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageLazyLoader
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 6
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 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 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 apply_lazy_loading
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
56
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        if ( empty( $content ) ) {
155            return $content;
156        }
157
158        // Remove all comments using clean_content (placeholder Divi + numeric ones)
159        $content = $this->clean_content(
160            '/.*/s', // match everything
161            '/<!--\s*\[et_pb_line_break_holder\]\s*-->|<!--\d+_\d+-\d+-->/',
162            '',
163            $content
164        );
165
166        // Apply loading="lazy" to images using regex, avoiding DOMDocument
167        $content = preg_replace_callback(
168            '/<img\b[^>]*>/i',
169            function ( $matches ) {
170                $img = $matches[0];
171
172                // Check for excluded classes
173                foreach ( ImageLazyLoader::$exclusions['classes'] as $class ) {
174                    if ( stripos( $img, $class ) !== false ) {
175                        return $img;
176                    }
177                }
178
179                // CHeck for excluded attributes
180                foreach ( ImageLazyLoader::$exclusions['attributes'] as $attr ) {
181                    if ( stripos( $img, $attr ) !== false ) {
182                        return $img;
183                    }
184                }
185
186                // Add loading="lazy" if not set
187                if ( stripos( $img, 'loading=' ) === false ) {
188                    $img = str_replace( '<img', '<img loading="lazy"', $img );
189                }
190
191                return $img;
192            },
193            $content
194        );
195
196        return $content;
197    }
198}