PluginProbe
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables / 2.2.13
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables v2.2.13
2.2.13 2.2.12 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 trunk 1.0.0 1.0.1 2.0.0 2.0.1 2.1.0 2.1.1 2.1.2 2.2.0 2.2.1 2.2.2
table-builder-block / includes / Hooks / AssetGenerator.php

AssetGenerator.php in TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables 2.2.13, at includes/Hooks/AssetGenerator.php

537 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Generates and enqueues per-post/per-block CSS and font assets
4 *
5 * @package TableKit
6 */
7
8 namespace TableBuilder\Hooks;
9
10 defined( 'ABSPATH' ) || exit;
11
12 /**
13 * Collects block CSS/typography into inline styles and Google Fonts URLs,
14 * and persists shortId attributes into saved block content.
15 */
16 class AssetGenerator {
17
18
19 use \TableBuilder\Traits\Singleton;
20
21 /**
22 * Accumulated inline CSS for the current request.
23 *
24 * @var string
25 */
26 public $css = '';
27
28 /**
29 * Font-family => font-weight[] map collected from block typography attributes.
30 *
31 * @var array
32 */
33 protected $fonts = array();
34
35 /**
36 * AssetGenerator class constructor.
37 * private for singleton
38 *
39 * @return void
40 * @since 1.0.0
41 */
42 public function __construct() {
43 add_action( 'save_post', array( $this, 'save_post_hook' ), 10, 3 );
44 add_filter( 'wp_insert_post_data', array( $this, 'persist_short_ids_in_content' ), 10, 2 );
45 add_filter( 'render_block_data', array( $this, 'set_blocks_css' ), 10 );
46 add_filter( 'wp_resource_hints', array( $this, 'fonts_resource_hints' ), 10, 2 );
47 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), 10 );
48 add_action( 'enqueue_block_assets', array( $this, 'block_assets' ), 10 );
49
50 // Clear FSE cache when templates are saved.
51 add_action( 'save_post_wp_template', array( $this, 'clear_fse_cache' ) );
52 add_action( 'save_post_wp_template_part', array( $this, 'clear_fse_cache' ) );
53 }
54
55 /**
56 * Clears the cached FSE (Full Site Editing) template block scan, hooked to
57 * saving a wp_template/wp_template_part post.
58 *
59 * @return void
60 */
61 public function clear_fse_cache() {
62 delete_transient( 'table_builder_fse_blocks' );
63 }
64
65 /**
66 * Filters an array of blocks and returns only those where the block name contains 'tablebuilder'.
67 *
68 * @param array $blocks An array of blocks. Each block is an associative array that must contain a 'blockName' key. Default is an empty array.
69 * @return array Returns an array of blocks where the block name contains 'tablebuilder'.
70 */
71 public function filter_blocks( $blocks = array() ) {
72 $filtered_blocks = array();
73
74 foreach ( $blocks as $block ) {
75 if ( isset( $block['blockName'] ) && false !== strpos( $block['blockName'], 'tablebuilder' ) ) {
76 $filtered_blocks[] = $block;
77 }
78
79 if ( ! empty( $block['innerBlocks'] ) ) {
80 $filtered_blocks = array_merge( $filtered_blocks, $this->filter_blocks( $block['innerBlocks'] ) );
81 }
82 }
83
84 return $filtered_blocks;
85 }
86
87 /**
88 * Minifies CSS by condensing white spaces and removing comments.
89 *
90 * @param string $css The input CSS.
91 * @return string Minified CSS.
92 */
93 public function minimize_css( $css ) {
94 if ( '' === trim( $css ) ) {
95 return $css;
96 }
97
98 return preg_replace(
99 array(
100 '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|\/\*(?!\!)(?>.*?\*\/)|^\s*|\s*$#s',
101 '#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/))|\s*+;\s*+(})\s*+|\s*+([*$~^|]?+=|[{};,>~]|\s(?![0-9\.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|"(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si',
102 '#(?<=[\s:])(0)(cm|em|ex|in|mm|pc|pt|px|vh|vw|%)#si',
103 '#:(0\s+0|0\s+0\s+0\s+0)(?=[;\}]|\!important)#i',
104 '#(background-position):0(?=[;\}])#si',
105 '#(?<=[\s:,\-])0+\.(\d+)#s',
106 '#(\/\*(?>.*?\*\/))|(?<!content\:)([\'"])([a-z_][a-z0-9\-_]*?)\2(?=[\s\{\}\];,])#si',
107 '#(\/\*(?>.*?\*\/))|(\burl\()([\'"])([^\s]+?)\3(\))#si',
108 '#(?<=[\{;])(border|outline):none(?=[;\}\!])#',
109 '#(\/\*(?>.*?\*\/))|(^|[\{\}])(?:[^\s\{\}]+)\{\}#s',
110 ),
111 array(
112 '$1',
113 '$1$2$3$4$5$6$7',
114 '$1',
115 ':0',
116 '$1:0 0',
117 '.$1',
118 '$1$3',
119 '$1$2$4$5',
120 '$1$2$3',
121 '$1:0',
122 '$1$2',
123 ),
124 $css
125 );
126 }
127
128 /**
129 * Fires once a post has been saved.
130 *
131 * @param int $post_id The ID of the post.
132 * @param WP_Post $post The post object.
133 * @param bool $update Whether this is an existing post being updated.
134 * @return void
135 */
136 public function save_post_hook( $post_id, $post, $update ) {
137 // Bail out if is draft, revision, or autosave.
138 if ( 'auto-draft' === $post->post_status || wp_is_post_revision( $post_id ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) {
139 return;
140 }
141
142 // Skip if this is not an update (new post).
143 if ( ! $update ) {
144 return;
145 }
146
147 // Early bailout: Check if post content has tablebuilder blocks before parsing.
148 if ( false === strpos( $post->post_content, 'tablebuilder' ) ) {
149 return;
150 }
151
152 $post = get_post( $post_id );
153 $parse_blocks = $this->filter_blocks( parse_blocks( $post->post_content ) );
154
155 if ( $parse_blocks ) {
156 $fse = in_array( $post->post_type, array( 'wp_template_part', 'wp_template' ), true );
157 if ( $fse ) {
158 $this->set_fonts( null, $this->generate_fse_assets(), true );
159 } else {
160 $this->set_fonts( $post_id, $parse_blocks );
161 }
162 }
163 }
164
165 /**
166 * Persists shortId into saved block content before WordPress writes the post.
167 *
168 * @param array $data Sanitized post data about to be inserted/updated.
169 * @param array $postarr Raw post data array as passed to wp_insert_post()/wp_update_post().
170 * @return array Modified post data.
171 */
172 public function persist_short_ids_in_content( array $data, array $postarr ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- required by the "wp_insert_post_data" filter signature; not needed in the body.
173 if ( empty( $data['post_content'] ) || false === strpos( (string) $data['post_content'], 'tablebuilder/' ) ) {
174 return $data;
175 }
176
177 $content = wp_unslash( (string) $data['post_content'] );
178 $blocks = parse_blocks( $content );
179
180 if ( empty( $blocks ) ) {
181 return $data;
182 }
183
184 $updated_blocks = $this->ensure_short_ids_for_blocks( $blocks );
185 $updated_content = serialize_blocks( $updated_blocks );
186
187 if ( '' === $updated_content || $updated_content === $content ) {
188 return $data;
189 }
190
191 $data['post_content'] = wp_slash( $updated_content );
192
193 return $data;
194 }
195
196 /**
197 * Recursively walks blocks and populates missing shortId values for table blocks.
198 *
199 * @param array $blocks Parsed block tree to update in place.
200 * @return array Block tree with shortId attributes filled in where missing.
201 */
202 protected function ensure_short_ids_for_blocks( array $blocks ): array {
203 $target_blocks = array( 'tablebuilder/table-builder', 'tablebuilder/data-table', 'tablebuilder/post-table' );
204
205 foreach ( $blocks as $index => $block ) {
206 if ( ! empty( $block['innerBlocks'] ) ) {
207 $block['innerBlocks'] = $this->ensure_short_ids_for_blocks( $block['innerBlocks'] );
208 }
209
210 if ( in_array( $block['blockName'] ?? '', $target_blocks, true ) ) {
211 $attrs = $block['attrs'] ?? array();
212
213 if ( empty( $attrs['shortId'] ) ) {
214 $block_id = (string) ( $attrs['blockID'] ?? '' );
215
216 if ( '' !== $block_id ) {
217 $attrs['shortId'] = $this->generate_short_id( $block_id );
218 $block['attrs'] = $attrs;
219 }
220 }
221 }
222
223 $blocks[ $index ] = $block;
224 }
225
226 return $blocks;
227 }
228
229 /**
230 * Generate the same 6-digit shortId value used by the editor and list table.
231 *
232 * @param string $block_id Block's own ID (blockID attribute) to derive the hash from.
233 * @return string 6-digit zero-padded numeric ID.
234 */
235 protected function generate_short_id( string $block_id ): string {
236 $hash = 0;
237
238 foreach ( str_split( $block_id ) as $character ) {
239 $hash = ( ( $hash * 31 ) + ord( $character ) ) & 0xffffffff;
240
241 if ( $hash > 0x7fffffff ) {
242 $hash -= 0x100000000;
243 }
244 }
245
246 return str_pad( (string) ( abs( $hash ) % 1000000 ), 6, '0', STR_PAD_LEFT );
247 }
248
249 /**
250 * Generate assets for templates.
251 * Now with caching to improve performance.
252 *
253 * @return array $filtered_blocks The filtered blocks for FSE templates.
254 */
255 protected function generate_fse_assets() {
256 // Check cache first (expires after 6 hours).
257 $cached_blocks = get_transient( 'table_builder_fse_blocks' );
258 if ( false !== $cached_blocks && is_array( $cached_blocks ) ) {
259 return $cached_blocks;
260 }
261
262 $args = array(
263 'post_type' => array( 'wp_template_part', 'wp_template' ),
264 'posts_per_page' => 100, // Limit to 100 templates for performance.
265 'orderby' => 'modified',
266 'order' => 'DESC',
267 'no_found_rows' => true, // Improve query performance.
268 'update_post_meta_cache' => false, // Skip meta cache.
269 'update_post_term_cache' => false, // Skip term cache.
270 );
271
272 $posts = get_posts( $args );
273 $merged_blocks = array();
274
275 foreach ( $posts as $post ) {
276 $merged_blocks = array_merge( $merged_blocks, parse_blocks( $post->post_content ) );
277 }
278
279 $filtered_blocks = $this->filter_blocks( $merged_blocks );
280
281 // Cache for 6 hours.
282 set_transient( 'table_builder_fse_blocks', $filtered_blocks, 6 * HOUR_IN_SECONDS );
283
284 return $filtered_blocks;
285 }
286
287 /**
288 * Sets the fonts for a given post or Full Site Editing (FSE) template.
289 *
290 * @param int $post_id The ID of the post or FSE template.
291 * @param array $blocks An array of blocks.
292 * @param bool $fse Whether this is an FSE template.
293 * @return void
294 */
295 protected function set_fonts( $post_id, $blocks, $fse = false ) {
296 $fonts = array();
297
298 foreach ( $blocks as $block ) {
299 if ( isset( $block['attrs'] ) ) {
300 $typographies = array_filter(
301 $block['attrs'],
302 function ( $key ) {
303 return str_contains( strtolower( $key ), 'typography' );
304 },
305 ARRAY_FILTER_USE_KEY
306 );
307
308 if ( ! empty( $typographies ) ) {
309 foreach ( $typographies as $typography ) {
310 $font_weight = ! empty( $typography['fontWeight']['value'] ) ? $typography['fontWeight']['value'] : 400;
311 if ( ! empty( $typography['fontFamily']['value'] ) ) {
312 $fonts[ $typography['fontFamily']['value'] ][] = $font_weight;
313 }
314 }
315 }
316 }
317 }
318
319 // Update fonts.
320 if ( ! empty( $fonts ) ) {
321 if ( $fse ) {
322 update_option( 'table_builder_fse_fonts', $fonts );
323 } else {
324 update_post_meta( $post_id, 'table_builder_posts_fonts', $fonts );
325 }
326 } elseif ( $fse ) {
327 delete_option( 'table_builder_fse_fonts' );
328 } else {
329 delete_post_meta( $post_id, 'table_builder_posts_fonts' );
330 }
331 }
332
333 /**
334 * Combines block assets (CSS and JS) based on the used blocks.
335 *
336 * @param array $parsed_block The parsed block.
337 * @return string Combined CSS content.
338 */
339 protected function combine_blocks_assets( $parsed_block = array() ) {
340 $blocks_css = array();
341
342 if ( isset( $parsed_block['blockName'] ) && false !== strpos( $parsed_block['blockName'], 'tablebuilder' ) ) {
343 if ( isset( $parsed_block['attrs']['blocksCSS'] ) ) {
344 foreach ( $parsed_block['attrs']['blocksCSS'] as $device => $css ) {
345 if ( ! isset( $blocks_css[ $device ] ) ) {
346 $blocks_css[ $device ] = '';
347 }
348
349 if ( is_string( $css ) ) {
350 $blocks_css[ $device ] .= preg_replace( '/<[^>]*>?/', '', $css );
351 }
352 }
353 }
354
355 // block typography.
356 $this->set_typography( $parsed_block );
357
358 // block common style.
359 if ( isset( $parsed_block['attrs']['commonStyle'] ) ) {
360 foreach ( $parsed_block['attrs']['commonStyle'] as $device => $css ) {
361 if ( ! isset( $blocks_css[ $device ] ) ) {
362 $blocks_css[ $device ] = '';
363 }
364
365 if ( is_string( $css ) ) {
366 $blocks_css[ $device ] .= preg_replace( '/<[^>]*>?/', '', $css );
367 }
368 }
369 }
370 }
371
372 // Concatenate CSS content into a single string.
373 $css_content = '';
374 $is_custom_styles_added = false;
375 $device_list = \TableBuilder\Helpers\Utils::get_device_list();
376
377 if ( ! empty( $blocks_css ) ) {
378 foreach ( $device_list as $device ) {
379 foreach ( $blocks_css as $key => $block ) {
380 if ( ! empty( $block ) && '' !== trim( $block ) ) {
381 $direction = $device['direction'] ?? 'max';
382 $width = $device['value'] ?? '';
383 $device_key = strtolower( $device['slug'] ?? '' );
384
385 if ( 'base' === $device['value'] && 'desktop' === $key ) {
386 $css_content .= $block;
387 } elseif ( ! empty( $direction ) && ! empty( $width ) && $device_key === $key ) {
388 $css_content .= "@media ({$direction}-width: {$width}px) {" . trim( $block ) . '}';
389 }
390
391 if ( 'customStyles' === $key && ! $is_custom_styles_added ) {
392 $is_custom_styles_added = true;
393 $css_content .= $block;
394 }
395 }
396 }
397 }
398 }
399
400 return $css_content;
401 }
402
403 /**
404 * Collects font-family/font-weight pairs from a block's typography-related
405 * attributes into $this->fonts, for later Google Fonts URL generation.
406 *
407 * @param array $parsed_block The parsed block data.
408 * @return void
409 */
410 protected function set_typography( $parsed_block ) {
411 if ( isset( $parsed_block['attrs'] ) ) {
412 $typographies = array_filter(
413 $parsed_block['attrs'],
414 function ( $key ) {
415 $key = strtolower( $key );
416 return str_contains( $key, 'typography' ) || str_contains( $key, 'typo' );
417 },
418 ARRAY_FILTER_USE_KEY
419 );
420
421 if ( ! empty( $typographies ) ) {
422 foreach ( $typographies as $typography ) {
423 $font_weight = ! empty( $typography['fontWeight']['value'] ) ? $typography['fontWeight']['value'] : 400;
424 if ( ! empty( $typography['fontFamily']['value'] ) ) {
425 $this->fonts[ $typography['fontFamily']['value'] ][] = $font_weight;
426 }
427 }
428 }
429 }
430 }
431
432 /**
433 * Generate Google Fonts URL.
434 *
435 * @return string|bool Google Fonts URL or false if no fonts.
436 */
437 protected function generate_fonts_url() {
438 if ( ! empty( $this->fonts ) ) {
439 $font_families = array();
440 $font_url = 'https://fonts.googleapis.com/css2?family=';
441
442 // Remove duplicates and sort the fonts.
443 $all_fonts = array_map(
444 function ( $arr ) {
445 $arr = array_unique( $arr );
446 sort( $arr );
447 return $arr;
448 },
449 $this->fonts
450 );
451
452 foreach ( $all_fonts as $font => $weights ) {
453 $weights = array_map(
454 function ( $weight ) {
455 $invalid_list = array( 'normal', 'inherit', 'initial' );
456 return in_array( $weight, $invalid_list, true ) ? '400' : $weight;
457 },
458 $weights
459 );
460 sort( $weights );
461 $font_families[] = str_replace( ' ', '+', $font ) . ':wght@' . implode( ';', array_unique( $weights ) );
462 }
463
464 $font_url .= implode( '&family=', $font_families );
465 $font_url .= '&display=swap';
466
467 return $font_url;
468 }
469
470 return false;
471 }
472
473 /**
474 * Sets the CSS for the blocks.
475 *
476 * @param array $parsed_block The parsed block data.
477 * @return array The modified parsed block data.
478 */
479 public function set_blocks_css( $parsed_block ) {
480 $css_content = $this->combine_blocks_assets( $parsed_block );
481 if ( ! empty( $css_content ) ) {
482 $this->css .= $css_content;
483 }
484 return $parsed_block;
485 }
486
487 /**
488 * Adds a preconnect resource hint for Google Fonts.
489 *
490 * @param array $urls URLs to print for resource hints.
491 * @param string $relation_type The relation type the URLs are printed.
492 * @return array
493 */
494 public function fonts_resource_hints( $urls, $relation_type ) {
495 if ( wp_style_is( 'table-builder-google-fonts', 'queue' ) && 'preconnect' === $relation_type ) {
496 $urls[] = array(
497 'href' => 'https://fonts.gstatic.com',
498 'crossorigin',
499 );
500 }
501
502 return $urls;
503 }
504
505 /**
506 * Enqueues the Google Fonts stylesheet if available.
507 * Enqueues inline styles for the TableBuilder frontend.
508 *
509 * @return void
510 */
511 public function enqueue_scripts() {
512 global $post;
513
514 if ( ! wp_is_block_theme() && $post instanceof \WP_Post && ! empty( $post->post_content ) ) {
515 do_blocks( $post->post_content );
516 }
517
518 $fonts_url = $this->generate_fonts_url();
519 if ( $fonts_url ) {
520 wp_enqueue_style( 'table-builder-google-fonts', $fonts_url, false, TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
521 }
522
523 if ( $this->css ) {
524 wp_add_inline_style( 'table-builder-style', $this->css );
525 }
526 }
527
528 /**
529 * Enqueues block assets (CSS & JS).
530 *
531 * @return void
532 */
533 public function block_assets() {
534 wp_enqueue_style( 'table-builder-style', get_stylesheet_uri(), array(), TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
535 }
536 }
537