PluginProbe
Block Animations, Motion & Scroll Effects – Ghost Kit / trunk
Block Animations, Motion & Scroll Effects – Ghost Kit vtrunk
3.7.1 3.7.0 3.6.1 trunk 1.6.3 2.25.0 3.3.0 3.3.1 3.3.2 3.3.3 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6 3.5.0 3.5.1 3.6.0
ghostkit / classes / class-assets-detector.php

class-assets-detector.php in Block Animations, Motion & Scroll Effects – Ghost Kit trunk, at classes/class-assets-detector.php

474 lines 12.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Detect frontend asset handles from parsed or rendered blocks.
4 *
5 * @package ghostkit
6 */
7
8 if ( ! defined( 'ABSPATH' ) ) {
9 exit;
10 }
11
12 /**
13 * Class GhostKit_Assets_Detector
14 */
15 class GhostKit_Assets_Detector {
16 /**
17 * Content / className markers → asset handles.
18 *
19 * @return array<string, array{styles: string[], scripts: string[], match?: string}>
20 */
21 private static function get_content_markers() {
22 return array(
23 'ghostkit-badge' => array(
24 'styles' => array( 'ghostkit' ),
25 'scripts' => array(),
26 ),
27 'ghostkit-text-uppercase' => array(
28 'styles' => array( 'ghostkit' ),
29 'scripts' => array(),
30 ),
31 'ghostkit-has-transform' => array(
32 'styles' => array( 'ghostkit' ),
33 'scripts' => array(),
34 ),
35 'ghostkit-d-' => array(
36 'match' => 'prefix',
37 'styles' => array( 'ghostkit' ),
38 'scripts' => array(),
39 ),
40 'ghostkit-paragraph-columns' => array(
41 'match' => 'prefix',
42 'styles' => array( 'ghostkit' ),
43 'scripts' => array(),
44 ),
45 'ghostkit-list-columns' => array(
46 'match' => 'prefix',
47 'styles' => array( 'ghostkit' ),
48 'scripts' => array(),
49 ),
50 'is-style-styled' => array(
51 'styles' => array( 'ghostkit' ),
52 'scripts' => array( 'ghostkit-style-variant-core-list' ),
53 ),
54 'is-style-icon' => array(
55 'styles' => array( 'ghostkit' ),
56 'scripts' => array(),
57 ),
58 'is-style-none' => array(
59 'styles' => array( 'ghostkit' ),
60 'scripts' => array(),
61 ),
62 'is-style-numbered' => array(
63 'styles' => array( 'ghostkit' ),
64 'scripts' => array(),
65 ),
66 'data-gkt-effects' => array(
67 'styles' => array(),
68 'scripts' => array( 'ghostkit-extension-effects' ),
69 ),
70 'ghostkit-count-up' => array(
71 'styles' => array(),
72 'scripts' => array( 'ghostkit-extension-effects' ),
73 ),
74 'data-ghostkit-sr' => array(
75 'styles' => array( 'ghostkit' ),
76 'scripts' => array(),
77 ),
78 );
79 }
80
81 /**
82 * Detect assets for a list of blocks (includes innerBlocks).
83 *
84 * @param array $blocks Parsed blocks.
85 * @param string $context Context: parse|render.
86 *
87 * @return array{styles: string[], scripts: string[]}
88 */
89 public static function detect_from_blocks( array $blocks, string $context = 'parse' ): array {
90 $assets = array(
91 'styles' => array(),
92 'scripts' => array(),
93 );
94
95 foreach ( $blocks as $block ) {
96 if ( ! is_array( $block ) ) {
97 continue;
98 }
99
100 $assets = self::merge_assets( $assets, self::detect_from_block( $block, $context ) );
101
102 if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
103 $assets = self::merge_assets( $assets, self::detect_from_blocks( $block['innerBlocks'], $context ) );
104 }
105 }
106
107 return $assets;
108 }
109
110 /**
111 * Detect assets for a single block.
112 *
113 * @param array $block Block array.
114 * @param string $context Context: parse|render.
115 * @param string $block_content Rendered HTML (render context only).
116 *
117 * @return array{styles: string[], scripts: string[]}
118 */
119 public static function detect_from_block( array $block, string $context = 'parse', string $block_content = '' ): array {
120 $assets = array(
121 'styles' => array(),
122 'scripts' => array(),
123 );
124
125 $block_name = isset( $block['blockName'] ) ? $block['blockName'] : '';
126
127 // Block.json handles are collected at parse-time; render-time covers attrs/HTML markers only.
128 if ( 'parse' === $context && $block_name ) {
129 $registry = WP_Block_Type_Registry::get_instance();
130 $assets = self::merge_assets( $assets, self::get_handles_from_block_type( $registry->get_registered( $block_name ) ) );
131 }
132
133 $assets = self::merge_assets( $assets, self::detect_from_attrs( $block ) );
134
135 $class_name = _wp_array_get( $block, array( 'attrs', 'className' ), '' );
136 if ( is_string( $class_name ) && '' !== $class_name ) {
137 $assets = self::merge_assets( $assets, self::detect_from_content_string( $class_name ) );
138 }
139
140 $content_string = self::get_block_content_string( $block, 'render' === $context ? $block_content : '' );
141 if ( '' !== $content_string ) {
142 $assets = self::merge_assets( $assets, self::detect_from_content_string( $content_string ) );
143 }
144
145 if ( $block_name && in_array( $block_name, array( 'ghostkit/counter-box', 'ghostkit/progress' ), true ) ) {
146 $assets = self::merge_assets(
147 $assets,
148 array(
149 'styles' => array(),
150 'scripts' => array( 'ghostkit-extension-effects' ),
151 )
152 );
153 }
154
155 $assets = apply_filters( 'gkt_detect_block_assets', $assets, $block, $context, $block_content );
156
157 return array(
158 'styles' => array_values( array_unique( $assets['styles'] ) ),
159 'scripts' => array_values( array_unique( $assets['scripts'] ) ),
160 );
161 }
162
163 /**
164 * Collect innerHTML / innerContent / optional rendered content.
165 *
166 * @param array $block Block array.
167 * @param string $block_content Rendered HTML.
168 *
169 * @return string
170 */
171 public static function get_block_content_string( array $block, string $block_content = '' ): string {
172 $parts = array();
173
174 if ( ! empty( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ) {
175 $parts[] = $block['innerHTML'];
176 }
177
178 if ( ! empty( $block['innerContent'] ) && is_array( $block['innerContent'] ) ) {
179 foreach ( $block['innerContent'] as $chunk ) {
180 if ( is_string( $chunk ) && '' !== $chunk ) {
181 $parts[] = $chunk;
182 }
183 }
184 }
185
186 if ( '' !== $block_content ) {
187 $parts[] = $block_content;
188 }
189
190 return implode( '', $parts );
191 }
192
193 /**
194 * Read style/script handles from a registered block type.
195 *
196 * @param WP_Block_Type|null $block_type Block type.
197 *
198 * @return array{styles: string[], scripts: string[]}
199 */
200 public static function get_handles_from_block_type( $block_type ): array {
201 $assets = array(
202 'styles' => array(),
203 'scripts' => array(),
204 );
205
206 if ( ! $block_type instanceof WP_Block_Type ) {
207 return $assets;
208 }
209
210 $assets['styles'] = array_merge(
211 (array) ( $block_type->style_handles ?? array() ),
212 (array) ( $block_type->view_style_handles ?? array() )
213 );
214
215 $assets['scripts'] = array_merge(
216 (array) ( $block_type->script_handles ?? array() ),
217 (array) ( $block_type->view_script_handles ?? array() )
218 );
219
220 return array(
221 'styles' => array_values( array_unique( $assets['styles'] ) ),
222 'scripts' => array_values( array_unique( $assets['scripts'] ) ),
223 );
224 }
225
226 /**
227 * Store detected handles for early enqueue (parse-time).
228 *
229 * @param array $assets Detected assets.
230 */
231 public static function store_detected_assets( array $assets ): void {
232 self::store_detected_handles( $assets['styles'] ?? array(), 'style', 9 );
233 self::store_detected_handles( $assets['scripts'] ?? array(), 'script', 11 );
234 }
235
236 /**
237 * Queue registered handles for deferred enqueue.
238 *
239 * @param string[] $handles Asset handles.
240 * @param string $type style|script.
241 * @param int $priority Store priority.
242 */
243 private static function store_detected_handles( array $handles, string $type, int $priority ): void {
244 foreach ( $handles as $handle ) {
245 if ( ! is_string( $handle ) || '' === $handle ) {
246 continue;
247 }
248
249 $is_registered = 'style' === $type
250 ? wp_style_is( $handle, 'registered' )
251 : wp_script_is( $handle, 'registered' );
252
253 if ( $is_registered ) {
254 GhostKit_Assets::store_used_assets( $handle, true, $type, $priority );
255 continue;
256 }
257
258 /**
259 * Fires when a detected asset handle is not registered.
260 *
261 * @param string $handle Asset handle.
262 * @param string $type Asset type: style|script.
263 */
264 do_action( 'gkt_assets_detector_missing_handle', $handle, $type );
265 }
266 }
267
268 /**
269 * Enqueue detected handles on render (with guards).
270 *
271 * @param array $assets Detected assets.
272 */
273 public static function enqueue_detected_assets( array $assets ): void {
274 self::enqueue_detected_handles( $assets['styles'] ?? array(), 'style' );
275 self::enqueue_detected_handles( $assets['scripts'] ?? array(), 'script' );
276 }
277
278 /**
279 * Enqueue registered handles that are not already enqueued.
280 *
281 * @param string[] $handles Asset handles.
282 * @param string $type style|script.
283 */
284 private static function enqueue_detected_handles( array $handles, string $type ): void {
285 foreach ( $handles as $handle ) {
286 if ( ! is_string( $handle ) || '' === $handle ) {
287 continue;
288 }
289
290 if ( 'style' === $type ) {
291 if ( wp_style_is( $handle, 'registered' ) && ! wp_style_is( $handle, 'enqueued' ) ) {
292 wp_enqueue_style( $handle );
293 }
294 continue;
295 }
296
297 if ( wp_script_is( $handle, 'registered' ) && ! wp_script_is( $handle, 'enqueued' ) ) {
298 wp_enqueue_script( $handle );
299 }
300 }
301 }
302
303 /**
304 * Detect from block attrs (ghostkit object and deprecated keys).
305 *
306 * @param array $block Block array.
307 *
308 * @return array{styles: string[], scripts: string[]}
309 */
310 private static function detect_from_attrs( array $block ): array {
311 $assets = array(
312 'styles' => array(),
313 'scripts' => array(),
314 );
315
316 $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array();
317
318 $ghostkit_data = _wp_array_get( $attrs, array( 'ghostkit' ), null );
319 if ( is_array( $ghostkit_data ) ) {
320 if ( self::is_non_empty_value( _wp_array_get( $ghostkit_data, array( 'styles' ), null ) ) ) {
321 $assets['styles'][] = 'ghostkit';
322 }
323
324 if ( self::is_non_empty_value( _wp_array_get( $ghostkit_data, array( 'effects' ), null ) ) ) {
325 $assets['scripts'][] = 'ghostkit-extension-effects';
326 }
327 }
328
329 if ( self::is_non_empty_value( _wp_array_get( $attrs, array( 'ghostkitCustomCSS' ), null ) ) ) {
330 $assets['styles'][] = 'ghostkit';
331 }
332
333 if (
334 self::is_non_empty_value( _wp_array_get( $attrs, array( 'ghostkitListIcon' ), null ) ) ||
335 self::is_non_empty_value( _wp_array_get( $attrs, array( 'ghostkitListIconColor' ), null ) )
336 ) {
337 $assets['styles'][] = 'ghostkit';
338 }
339
340 if ( self::is_non_empty_value( _wp_array_get( $attrs, array( 'ghostkitSR' ), null ) ) ) {
341 $assets['styles'][] = 'ghostkit';
342 }
343
344 return array(
345 'styles' => array_values( array_unique( $assets['styles'] ) ),
346 'scripts' => array_values( array_unique( $assets['scripts'] ) ),
347 );
348 }
349
350 /**
351 * Match content markers in a string.
352 *
353 * @param string $content Content to scan.
354 *
355 * @return array{styles: string[], scripts: string[]}
356 */
357 private static function detect_from_content_string( string $content ): array {
358 $assets = array(
359 'styles' => array(),
360 'scripts' => array(),
361 );
362
363 foreach ( self::get_content_markers() as $marker => $config ) {
364 $match_type = isset( $config['match'] ) ? $config['match'] : 'substring';
365
366 if ( ! self::content_matches_marker( $content, $marker, $match_type ) ) {
367 continue;
368 }
369
370 $assets = self::merge_assets(
371 $assets,
372 array(
373 'styles' => $config['styles'] ?? array(),
374 'scripts' => $config['scripts'] ?? array(),
375 )
376 );
377 }
378
379 return $assets;
380 }
381
382 /**
383 * Whether a marker is present in content.
384 *
385 * @param string $content String to scan (className, HTML fragment, etc.).
386 * @param string $marker Marker string.
387 * @param string $match_type substring|prefix.
388 *
389 * @return bool
390 */
391 private static function content_matches_marker( string $content, string $marker, string $match_type ): bool {
392 if ( 'prefix' === $match_type ) {
393 return self::content_has_class_prefix( $content, $marker );
394 }
395
396 return false !== strpos( $content, $marker );
397 }
398
399 /**
400 * Whether any whitespace-delimited class token starts with the given prefix.
401 *
402 * Example: prefix `ghostkit-paragraph-columns` matches `lala test ghostkit-paragraph-columns-2 wow`.
403 *
404 * @param string $content Class list or HTML containing class names.
405 * @param string $prefix Class name prefix.
406 *
407 * @return bool
408 */
409 private static function content_has_class_prefix( string $content, string $prefix ): bool {
410 if ( '' === $prefix ) {
411 return false;
412 }
413
414 $quoted_prefix = preg_quote( $prefix, '/' );
415
416 // Class token at start of string or after whitespace / quote; optional suffix after prefix.
417 $pattern = '/(?:^|[\s"\'])' . $quoted_prefix . '[a-zA-Z0-9_-]*/';
418
419 return 1 === preg_match( $pattern, $content );
420 }
421
422 /**
423 * Merge two asset arrays.
424 *
425 * @param array $a First assets.
426 * @param array $b Second assets.
427 *
428 * @return array{styles: string[], scripts: string[]}
429 */
430 private static function merge_assets( array $a, array $b ): array {
431 return array(
432 'styles' => array_values(
433 array_unique(
434 array_merge(
435 $a['styles'] ?? array(),
436 $b['styles'] ?? array()
437 )
438 )
439 ),
440 'scripts' => array_values(
441 array_unique(
442 array_merge(
443 $a['scripts'] ?? array(),
444 $b['scripts'] ?? array()
445 )
446 )
447 ),
448 );
449 }
450
451 /**
452 * Whether an attribute value is non-empty.
453 *
454 * @param mixed $value Value.
455 *
456 * @return bool
457 */
458 private static function is_non_empty_value( $value ): bool {
459 if ( null === $value ) {
460 return false;
461 }
462
463 if ( is_string( $value ) ) {
464 return trim( $value ) !== '';
465 }
466
467 if ( is_array( $value ) ) {
468 return ! empty( $value );
469 }
470
471 return (bool) $value;
472 }
473 }
474