Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 69 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
ImageLazyLoader | |
0.00% |
0 / 69 |
|
0.00% |
0 / 6 |
702 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
apply_lazy_loading_to_blocks | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
enqueue_lazy_loader | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
get_inline_script | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
clean_content | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
apply_lazy_loading | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
210 |
1 | <?php |
2 | |
3 | namespace NewfoldLabs\WP\Module\Performance\Images; |
4 | |
5 | use DOMDocument; |
6 | use Exception; |
7 | |
8 | /** |
9 | * Manages the initialization and application of lazy loading for images. |
10 | */ |
11 | class 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 | $doc = new DOMDocument(); |
160 | // Suppress warnings from invalid or malformed HTML. |
161 | libxml_use_internal_errors( true ); |
162 | |
163 | try { |
164 | if ( function_exists( 'et_setup_theme' ) ) { |
165 | $content = $this->clean_content( |
166 | '/<iframe(.*?)<\/iframe>/s', |
167 | '<!-- [et_pb_line_break_holder] -->', |
168 | '', |
169 | $content |
170 | ); |
171 | } |
172 | |
173 | // Attempt to parse the HTML content using htmlentities for encoding. |
174 | $content = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>' . $content . '</body></html>'; |
175 | if ( ! $doc->loadHTML( $content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ) ) { |
176 | return $content; |
177 | } |
178 | } catch ( Exception $e ) { |
179 | return $content; |
180 | } finally { |
181 | // Clear any errors collected during parsing to free memory. |
182 | libxml_clear_errors(); |
183 | } |
184 | |
185 | $images = $doc->getElementsByTagName( 'img' ); |
186 | |
187 | foreach ( $images as $image ) { |
188 | $skip = false; |
189 | |
190 | // Check if the image has an excluded class. |
191 | foreach ( self::$exclusions['classes'] as $class ) { |
192 | if ( $image->hasAttribute( 'class' ) && strpos( $image->getAttribute( 'class' ), $class ) !== false ) { |
193 | $skip = true; |
194 | break; |
195 | } |
196 | } |
197 | |
198 | // Check if the image has an excluded attribute. |
199 | foreach ( self::$exclusions['attributes'] as $attr ) { |
200 | if ( $image->hasAttribute( $attr ) ) { |
201 | $skip = true; |
202 | break; |
203 | } |
204 | } |
205 | |
206 | if ( $skip ) { |
207 | continue; |
208 | } |
209 | |
210 | // Add the loading="lazy" attribute if not already present. |
211 | if ( ! $image->hasAttribute( 'loading' ) ) { |
212 | $image->setAttribute( 'loading', 'lazy' ); |
213 | } |
214 | } |
215 | |
216 | // Extract the body content and return it. |
217 | $body = $doc->getElementsByTagName( 'body' )->item( 0 ); |
218 | return $body ? $doc->saveHTML( $body ) : $content; |
219 | } |
220 | } |