PluginProbe
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables / 2.2.10
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables v2.2.10
2.3.0 2.2.14 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.10, at includes/Hooks/AssetGenerator.php

535 lines 16.1 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 ] .= $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 $blocks_css[ $device ] .= $css;
366 }
367 }
368 }
369
370 // Concatenate CSS content into a single string.
371 $css_content = '';
372 $is_custom_styles_added = false;
373 $device_list = \TableBuilder\Helpers\Utils::get_device_list();
374
375 if ( ! empty( $blocks_css ) ) {
376 foreach ( $device_list as $device ) {
377 foreach ( $blocks_css as $key => $block ) {
378 if ( ! empty( $block ) && '' !== trim( $block ) ) {
379 $direction = $device['direction'] ?? 'max';
380 $width = $device['value'] ?? '';
381 $device_key = strtolower( $device['slug'] ?? '' );
382
383 if ( 'base' === $device['value'] && 'desktop' === $key ) {
384 $css_content .= $block;
385 } elseif ( ! empty( $direction ) && ! empty( $width ) && $device_key === $key ) {
386 $css_content .= "@media ({$direction}-width: {$width}px) {" . trim( $block ) . '}';
387 }
388
389 if ( 'customStyles' === $key && ! $is_custom_styles_added ) {
390 $is_custom_styles_added = true;
391 $css_content .= $block;
392 }
393 }
394 }
395 }
396 }
397
398 return $css_content;
399 }
400
401 /**
402 * Collects font-family/font-weight pairs from a block's typography-related
403 * attributes into $this->fonts, for later Google Fonts URL generation.
404 *
405 * @param array $parsed_block The parsed block data.
406 * @return void
407 */
408 protected function set_typography( $parsed_block ) {
409 if ( isset( $parsed_block['attrs'] ) ) {
410 $typographies = array_filter(
411 $parsed_block['attrs'],
412 function ( $key ) {
413 $key = strtolower( $key );
414 return str_contains( $key, 'typography' ) || str_contains( $key, 'typo' );
415 },
416 ARRAY_FILTER_USE_KEY
417 );
418
419 if ( ! empty( $typographies ) ) {
420 foreach ( $typographies as $typography ) {
421 $font_weight = ! empty( $typography['fontWeight']['value'] ) ? $typography['fontWeight']['value'] : 400;
422 if ( ! empty( $typography['fontFamily']['value'] ) ) {
423 $this->fonts[ $typography['fontFamily']['value'] ][] = $font_weight;
424 }
425 }
426 }
427 }
428 }
429
430 /**
431 * Generate Google Fonts URL.
432 *
433 * @return string|bool Google Fonts URL or false if no fonts.
434 */
435 protected function generate_fonts_url() {
436 if ( ! empty( $this->fonts ) ) {
437 $font_families = array();
438 $font_url = 'https://fonts.googleapis.com/css2?family=';
439
440 // Remove duplicates and sort the fonts.
441 $all_fonts = array_map(
442 function ( $arr ) {
443 $arr = array_unique( $arr );
444 sort( $arr );
445 return $arr;
446 },
447 $this->fonts
448 );
449
450 foreach ( $all_fonts as $font => $weights ) {
451 $weights = array_map(
452 function ( $weight ) {
453 $invalid_list = array( 'normal', 'inherit', 'initial' );
454 return in_array( $weight, $invalid_list, true ) ? '400' : $weight;
455 },
456 $weights
457 );
458 sort( $weights );
459 $font_families[] = str_replace( ' ', '+', $font ) . ':wght@' . implode( ';', array_unique( $weights ) );
460 }
461
462 $font_url .= implode( '&family=', $font_families );
463 $font_url .= '&display=swap';
464
465 return $font_url;
466 }
467
468 return false;
469 }
470
471 /**
472 * Sets the CSS for the blocks.
473 *
474 * @param array $parsed_block The parsed block data.
475 * @return array The modified parsed block data.
476 */
477 public function set_blocks_css( $parsed_block ) {
478 $css_content = $this->combine_blocks_assets( $parsed_block );
479 if ( ! empty( $css_content ) ) {
480 $this->css .= $css_content;
481 }
482 return $parsed_block;
483 }
484
485 /**
486 * Adds a preconnect resource hint for Google Fonts.
487 *
488 * @param array $urls URLs to print for resource hints.
489 * @param string $relation_type The relation type the URLs are printed.
490 * @return array
491 */
492 public function fonts_resource_hints( $urls, $relation_type ) {
493 if ( wp_style_is( 'table-builder-google-fonts', 'queue' ) && 'preconnect' === $relation_type ) {
494 $urls[] = array(
495 'href' => 'https://fonts.gstatic.com',
496 'crossorigin',
497 );
498 }
499
500 return $urls;
501 }
502
503 /**
504 * Enqueues the Google Fonts stylesheet if available.
505 * Enqueues inline styles for the TableBuilder frontend.
506 *
507 * @return void
508 */
509 public function enqueue_scripts() {
510 global $post;
511
512 if ( ! wp_is_block_theme() && $post instanceof \WP_Post && ! empty( $post->post_content ) ) {
513 do_blocks( $post->post_content );
514 }
515
516 $fonts_url = $this->generate_fonts_url();
517 if ( $fonts_url ) {
518 wp_enqueue_style( 'table-builder-google-fonts', $fonts_url, false, TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
519 }
520
521 if ( $this->css ) {
522 wp_add_inline_style( 'table-builder-style', $this->css );
523 }
524 }
525
526 /**
527 * Enqueues block assets (CSS & JS).
528 *
529 * @return void
530 */
531 public function block_assets() {
532 wp_enqueue_style( 'table-builder-style', get_stylesheet_uri(), array(), TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
533 }
534 }
535