Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageLazyLoader
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 5
462
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
 apply_lazy_loading
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
182
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     * Applies lazy loading to images in HTML content.
121     * Skips images with specified exclusion classes or attributes.
122     *
123     * @param string $content The HTML content to process.
124     * @return string Modified HTML content with lazy loading applied, or original content on error.
125     */
126    public function apply_lazy_loading( $content ) {
127        // Return unmodified content if it is empty.
128        if ( empty( $content ) ) {
129            return $content;
130        }
131
132        $doc = new DOMDocument();
133        // Suppress warnings from invalid or malformed HTML.
134        libxml_use_internal_errors( true );
135
136        try {
137            // Attempt to parse the HTML content using htmlentities for encoding.
138            $content = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>' . $content . '</body></html>';
139            if ( ! $doc->loadHTML( $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ) ) {
140                return $content;
141            }
142        } catch ( Exception $e ) {
143            return $content;
144        } finally {
145            // Clear any errors collected during parsing to free memory.
146            libxml_clear_errors();
147        }
148
149        $images = $doc->getElementsByTagName( 'img' );
150
151        foreach ( $images as $image ) {
152            $skip = false;
153
154            // Check if the image has an excluded class.
155            foreach ( self::$exclusions['classes'] as $class ) {
156                if ( $image->hasAttribute( 'class' ) && strpos( $image->getAttribute( 'class' ), $class ) !== false ) {
157                    $skip = true;
158                    break;
159                }
160            }
161
162            // Check if the image has an excluded attribute.
163            foreach ( self::$exclusions['attributes'] as $attr ) {
164                if ( $image->hasAttribute( $attr ) ) {
165                    $skip = true;
166                    break;
167                }
168            }
169
170            if ( $skip ) {
171                continue;
172            }
173
174            // Add the loading="lazy" attribute if not already present.
175            if ( ! $image->hasAttribute( 'loading' ) ) {
176                $image->setAttribute( 'loading', 'lazy' );
177            }
178        }
179
180        // Extract the body content and return it.
181        $body = $doc->getElementsByTagName( 'body' )->item( 0 );
182        return $body ? $doc->saveHTML( $body ) : $content;
183    }
184}