# table-builder-block/2.2.13/includes/Hooks/AssetGenerator.php

TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables &amp; Post Tables, version 2.2.13. 537 lines.

- Page: https://pluginprobe.com/plugins/table-builder-block/2.2.13/code/includes/Hooks/AssetGenerator.php
- Raw: https://pluginprobe.com/plugins/table-builder-block/2.2.13/raw/includes/Hooks/AssetGenerator.php
- Modified: 2026-08-31T10:59:32+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/table-builder-block/2.2.13/code/includes/Hooks/AssetGenerator.php#L10-L20`.

```php
<?php
/**
 * Generates and enqueues per-post/per-block CSS and font assets
 *
 * @package TableKit
 */

namespace TableBuilder\Hooks;

defined( 'ABSPATH' ) || exit;

/**
 * Collects block CSS/typography into inline styles and Google Fonts URLs,
 * and persists shortId attributes into saved block content.
 */
class AssetGenerator {


	use \TableBuilder\Traits\Singleton;

	/**
	 * Accumulated inline CSS for the current request.
	 *
	 * @var string
	 */
	public $css = '';

	/**
	 * Font-family => font-weight[] map collected from block typography attributes.
	 *
	 * @var array
	 */
	protected $fonts = array();

	/**
	 * AssetGenerator class constructor.
	 * private for singleton
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function __construct() {
		add_action( 'save_post', array( $this, 'save_post_hook' ), 10, 3 );
		add_filter( 'wp_insert_post_data', array( $this, 'persist_short_ids_in_content' ), 10, 2 );
		add_filter( 'render_block_data', array( $this, 'set_blocks_css' ), 10 );
		add_filter( 'wp_resource_hints', array( $this, 'fonts_resource_hints' ), 10, 2 );
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), 10 );
		add_action( 'enqueue_block_assets', array( $this, 'block_assets' ), 10 );

		// Clear FSE cache when templates are saved.
		add_action( 'save_post_wp_template', array( $this, 'clear_fse_cache' ) );
		add_action( 'save_post_wp_template_part', array( $this, 'clear_fse_cache' ) );
	}

	/**
	 * Clears the cached FSE (Full Site Editing) template block scan, hooked to
	 * saving a wp_template/wp_template_part post.
	 *
	 * @return void
	 */
	public function clear_fse_cache() {
		delete_transient( 'table_builder_fse_blocks' );
	}

	/**
	 * Filters an array of blocks and returns only those where the block name contains 'tablebuilder'.
	 *
	 * @param array $blocks An array of blocks. Each block is an associative array that must contain a 'blockName' key. Default is an empty array.
	 * @return array Returns an array of blocks where the block name contains 'tablebuilder'.
	 */
	public function filter_blocks( $blocks = array() ) {
		$filtered_blocks = array();

		foreach ( $blocks as $block ) {
			if ( isset( $block['blockName'] ) && false !== strpos( $block['blockName'], 'tablebuilder' ) ) {
				$filtered_blocks[] = $block;
			}

			if ( ! empty( $block['innerBlocks'] ) ) {
				$filtered_blocks = array_merge( $filtered_blocks, $this->filter_blocks( $block['innerBlocks'] ) );
			}
		}

		return $filtered_blocks;
	}

	/**
	 * Minifies CSS by condensing white spaces and removing comments.
	 *
	 * @param string $css The input CSS.
	 * @return string Minified CSS.
	 */
	public function minimize_css( $css ) {
		if ( '' === trim( $css ) ) {
			return $css;
		}

		return preg_replace(
			array(
				'#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')|\/\*(?!\!)(?>.*?\*\/)|^\s*|\s*$#s',
				'#("(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\'|\/\*(?>.*?\*\/))|\s*+;\s*+(})\s*+|\s*+([*$~^|]?+=|[{};,>~]|\s(?![0-9\.])|!important\b)\s*+|([[(:])\s++|\s++([])])|\s++(:)\s*+(?!(?>[^{}"\']++|"(?:[^"\\\]++|\\\.)*+"|\'(?:[^\'\\\\]++|\\\.)*+\')*+{)|^\s++|\s++\z|(\s)\s+#si',
				'#(?<=[\s:])(0)(cm|em|ex|in|mm|pc|pt|px|vh|vw|%)#si',
				'#:(0\s+0|0\s+0\s+0\s+0)(?=[;\}]|\!important)#i',
				'#(background-position):0(?=[;\}])#si',
				'#(?<=[\s:,\-])0+\.(\d+)#s',
				'#(\/\*(?>.*?\*\/))|(?<!content\:)([\'"])([a-z_][a-z0-9\-_]*?)\2(?=[\s\{\}\];,])#si',
				'#(\/\*(?>.*?\*\/))|(\burl\()([\'"])([^\s]+?)\3(\))#si',
				'#(?<=[\{;])(border|outline):none(?=[;\}\!])#',
				'#(\/\*(?>.*?\*\/))|(^|[\{\}])(?:[^\s\{\}]+)\{\}#s',
			),
			array(
				'$1',
				'$1$2$3$4$5$6$7',
				'$1',
				':0',
				'$1:0 0',
				'.$1',
				'$1$3',
				'$1$2$4$5',
				'$1$2$3',
				'$1:0',
				'$1$2',
			),
			$css
		);
	}

	/**
	 * Fires once a post has been saved.
	 *
	 * @param int     $post_id The ID of the post.
	 * @param WP_Post $post The post object.
	 * @param bool    $update Whether this is an existing post being updated.
	 * @return void
	 */
	public function save_post_hook( $post_id, $post, $update ) {
		// Bail out if is draft, revision, or autosave.
		if ( 'auto-draft' === $post->post_status || wp_is_post_revision( $post_id ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) ) {
			return;
		}

		// Skip if this is not an update (new post).
		if ( ! $update ) {
			return;
		}

		// Early bailout: Check if post content has tablebuilder blocks before parsing.
		if ( false === strpos( $post->post_content, 'tablebuilder' ) ) {
			return;
		}

		$post         = get_post( $post_id );
		$parse_blocks = $this->filter_blocks( parse_blocks( $post->post_content ) );

		if ( $parse_blocks ) {
			$fse = in_array( $post->post_type, array( 'wp_template_part', 'wp_template' ), true );
			if ( $fse ) {
				$this->set_fonts( null, $this->generate_fse_assets(), true );
			} else {
				$this->set_fonts( $post_id, $parse_blocks );
			}
		}
	}

	/**
	 * Persists shortId into saved block content before WordPress writes the post.
	 *
	 * @param array $data    Sanitized post data about to be inserted/updated.
	 * @param array $postarr Raw post data array as passed to wp_insert_post()/wp_update_post().
	 * @return array Modified post data.
	 */
	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.
		if ( empty( $data['post_content'] ) || false === strpos( (string) $data['post_content'], 'tablebuilder/' ) ) {
			return $data;
		}

		$content = wp_unslash( (string) $data['post_content'] );
		$blocks  = parse_blocks( $content );

		if ( empty( $blocks ) ) {
			return $data;
		}

		$updated_blocks  = $this->ensure_short_ids_for_blocks( $blocks );
		$updated_content = serialize_blocks( $updated_blocks );

		if ( '' === $updated_content || $updated_content === $content ) {
			return $data;
		}

		$data['post_content'] = wp_slash( $updated_content );

		return $data;
	}

	/**
	 * Recursively walks blocks and populates missing shortId values for table blocks.
	 *
	 * @param array $blocks Parsed block tree to update in place.
	 * @return array Block tree with shortId attributes filled in where missing.
	 */
	protected function ensure_short_ids_for_blocks( array $blocks ): array {
		$target_blocks = array( 'tablebuilder/table-builder', 'tablebuilder/data-table', 'tablebuilder/post-table' );

		foreach ( $blocks as $index => $block ) {
			if ( ! empty( $block['innerBlocks'] ) ) {
				$block['innerBlocks'] = $this->ensure_short_ids_for_blocks( $block['innerBlocks'] );
			}

			if ( in_array( $block['blockName'] ?? '', $target_blocks, true ) ) {
				$attrs = $block['attrs'] ?? array();

				if ( empty( $attrs['shortId'] ) ) {
					$block_id = (string) ( $attrs['blockID'] ?? '' );

					if ( '' !== $block_id ) {
						$attrs['shortId'] = $this->generate_short_id( $block_id );
						$block['attrs']   = $attrs;
					}
				}
			}

			$blocks[ $index ] = $block;
		}

		return $blocks;
	}

	/**
	 * Generate the same 6-digit shortId value used by the editor and list table.
	 *
	 * @param string $block_id Block's own ID (blockID attribute) to derive the hash from.
	 * @return string 6-digit zero-padded numeric ID.
	 */
	protected function generate_short_id( string $block_id ): string {
		$hash = 0;

		foreach ( str_split( $block_id ) as $character ) {
			$hash = ( ( $hash * 31 ) + ord( $character ) ) & 0xffffffff;

			if ( $hash > 0x7fffffff ) {
				$hash -= 0x100000000;
			}
		}

		return str_pad( (string) ( abs( $hash ) % 1000000 ), 6, '0', STR_PAD_LEFT );
	}

	/**
	 * Generate assets for templates.
	 * Now with caching to improve performance.
	 *
	 * @return array $filtered_blocks The filtered blocks for FSE templates.
	 */
	protected function generate_fse_assets() {
		// Check cache first (expires after 6 hours).
		$cached_blocks = get_transient( 'table_builder_fse_blocks' );
		if ( false !== $cached_blocks && is_array( $cached_blocks ) ) {
			return $cached_blocks;
		}

		$args = array(
			'post_type'              => array( 'wp_template_part', 'wp_template' ),
			'posts_per_page'         => 100, // Limit to 100 templates for performance.
			'orderby'                => 'modified',
			'order'                  => 'DESC',
			'no_found_rows'          => true, // Improve query performance.
			'update_post_meta_cache' => false, // Skip meta cache.
			'update_post_term_cache' => false, // Skip term cache.
		);

		$posts         = get_posts( $args );
		$merged_blocks = array();

		foreach ( $posts as $post ) {
			$merged_blocks = array_merge( $merged_blocks, parse_blocks( $post->post_content ) );
		}

		$filtered_blocks = $this->filter_blocks( $merged_blocks );

		// Cache for 6 hours.
		set_transient( 'table_builder_fse_blocks', $filtered_blocks, 6 * HOUR_IN_SECONDS );

		return $filtered_blocks;
	}

	/**
	 * Sets the fonts for a given post or Full Site Editing (FSE) template.
	 *
	 * @param int   $post_id The ID of the post or FSE template.
	 * @param array $blocks An array of blocks.
	 * @param bool  $fse    Whether this is an FSE template.
	 * @return void
	 */
	protected function set_fonts( $post_id, $blocks, $fse = false ) {
		$fonts = array();

		foreach ( $blocks as $block ) {
			if ( isset( $block['attrs'] ) ) {
				$typographies = array_filter(
					$block['attrs'],
					function ( $key ) {
						return str_contains( strtolower( $key ), 'typography' );
					},
					ARRAY_FILTER_USE_KEY
				);

				if ( ! empty( $typographies ) ) {
					foreach ( $typographies as $typography ) {
						$font_weight = ! empty( $typography['fontWeight']['value'] ) ? $typography['fontWeight']['value'] : 400;
						if ( ! empty( $typography['fontFamily']['value'] ) ) {
							$fonts[ $typography['fontFamily']['value'] ][] = $font_weight;
						}
					}
				}
			}
		}

		// Update fonts.
		if ( ! empty( $fonts ) ) {
			if ( $fse ) {
				update_option( 'table_builder_fse_fonts', $fonts );
			} else {
				update_post_meta( $post_id, 'table_builder_posts_fonts', $fonts );
			}
		} elseif ( $fse ) {
				delete_option( 'table_builder_fse_fonts' );
		} else {
			delete_post_meta( $post_id, 'table_builder_posts_fonts' );
		}
	}

	/**
	 * Combines block assets (CSS and JS) based on the used blocks.
	 *
	 * @param array $parsed_block The parsed block.
	 * @return string Combined CSS content.
	 */
	protected function combine_blocks_assets( $parsed_block = array() ) {
		$blocks_css = array();

		if ( isset( $parsed_block['blockName'] ) && false !== strpos( $parsed_block['blockName'], 'tablebuilder' ) ) {
			if ( isset( $parsed_block['attrs']['blocksCSS'] ) ) {
				foreach ( $parsed_block['attrs']['blocksCSS'] as $device => $css ) {
					if ( ! isset( $blocks_css[ $device ] ) ) {
						$blocks_css[ $device ] = '';
					}

					if ( is_string( $css ) ) {
						$blocks_css[ $device ] .= preg_replace( '/<[^>]*>?/', '', $css );
					}
				}
			}

			// block typography.
			$this->set_typography( $parsed_block );

			// block common style.
			if ( isset( $parsed_block['attrs']['commonStyle'] ) ) {
				foreach ( $parsed_block['attrs']['commonStyle'] as $device => $css ) {
					if ( ! isset( $blocks_css[ $device ] ) ) {
						$blocks_css[ $device ] = '';
					}

					if ( is_string( $css ) ) {
						$blocks_css[ $device ] .= preg_replace( '/<[^>]*>?/', '', $css );
					}
				}
			}
		}

		// Concatenate CSS content into a single string.
		$css_content            = '';
		$is_custom_styles_added = false;
		$device_list            = \TableBuilder\Helpers\Utils::get_device_list();

		if ( ! empty( $blocks_css ) ) {
			foreach ( $device_list as $device ) {
				foreach ( $blocks_css as $key => $block ) {
					if ( ! empty( $block ) && '' !== trim( $block ) ) {
						$direction  = $device['direction'] ?? 'max';
						$width      = $device['value'] ?? '';
						$device_key = strtolower( $device['slug'] ?? '' );

						if ( 'base' === $device['value'] && 'desktop' === $key ) {
							$css_content .= $block;
						} elseif ( ! empty( $direction ) && ! empty( $width ) && $device_key === $key ) {
							$css_content .= "@media ({$direction}-width: {$width}px) {" . trim( $block ) . '}';
						}

						if ( 'customStyles' === $key && ! $is_custom_styles_added ) {
							$is_custom_styles_added = true;
							$css_content           .= $block;
						}
					}
				}
			}
		}

		return $css_content;
	}

	/**
	 * Collects font-family/font-weight pairs from a block's typography-related
	 * attributes into $this->fonts, for later Google Fonts URL generation.
	 *
	 * @param array $parsed_block The parsed block data.
	 * @return void
	 */
	protected function set_typography( $parsed_block ) {
		if ( isset( $parsed_block['attrs'] ) ) {
			$typographies = array_filter(
				$parsed_block['attrs'],
				function ( $key ) {
					$key = strtolower( $key );
					return str_contains( $key, 'typography' ) || str_contains( $key, 'typo' );
				},
				ARRAY_FILTER_USE_KEY
			);

			if ( ! empty( $typographies ) ) {
				foreach ( $typographies as $typography ) {
					$font_weight = ! empty( $typography['fontWeight']['value'] ) ? $typography['fontWeight']['value'] : 400;
					if ( ! empty( $typography['fontFamily']['value'] ) ) {
						$this->fonts[ $typography['fontFamily']['value'] ][] = $font_weight;
					}
				}
			}
		}
	}

	/**
	 * Generate Google Fonts URL.
	 *
	 * @return string|bool Google Fonts URL or false if no fonts.
	 */
	protected function generate_fonts_url() {
		if ( ! empty( $this->fonts ) ) {
			$font_families = array();
			$font_url      = 'https://fonts.googleapis.com/css2?family=';

			// Remove duplicates and sort the fonts.
			$all_fonts = array_map(
				function ( $arr ) {
					$arr = array_unique( $arr );
					sort( $arr );
					return $arr;
				},
				$this->fonts
			);

			foreach ( $all_fonts as $font => $weights ) {
				$weights = array_map(
					function ( $weight ) {
						$invalid_list = array( 'normal', 'inherit', 'initial' );
						return in_array( $weight, $invalid_list, true ) ? '400' : $weight;
					},
					$weights
				);
				sort( $weights );
				$font_families[] = str_replace( ' ', '+', $font ) . ':wght@' . implode( ';', array_unique( $weights ) );
			}

			$font_url .= implode( '&family=', $font_families );
			$font_url .= '&display=swap';

			return $font_url;
		}

		return false;
	}

	/**
	 * Sets the CSS for the blocks.
	 *
	 * @param array $parsed_block The parsed block data.
	 * @return array The modified parsed block data.
	 */
	public function set_blocks_css( $parsed_block ) {
		$css_content = $this->combine_blocks_assets( $parsed_block );
		if ( ! empty( $css_content ) ) {
			$this->css .= $css_content;
		}
		return $parsed_block;
	}

	/**
	 * Adds a preconnect resource hint for Google Fonts.
	 *
	 * @param array  $urls URLs to print for resource hints.
	 * @param string $relation_type The relation type the URLs are printed.
	 * @return array
	 */
	public function fonts_resource_hints( $urls, $relation_type ) {
		if ( wp_style_is( 'table-builder-google-fonts', 'queue' ) && 'preconnect' === $relation_type ) {
			$urls[] = array(
				'href' => 'https://fonts.gstatic.com',
				'crossorigin',
			);
		}

		return $urls;
	}

	/**
	 * Enqueues the Google Fonts stylesheet if available.
	 * Enqueues inline styles for the TableBuilder frontend.
	 *
	 * @return void
	 */
	public function enqueue_scripts() {
		global $post;

		if ( ! wp_is_block_theme() && $post instanceof \WP_Post && ! empty( $post->post_content ) ) {
			do_blocks( $post->post_content );
		}

		$fonts_url = $this->generate_fonts_url();
		if ( $fonts_url ) {
			wp_enqueue_style( 'table-builder-google-fonts', $fonts_url, false, TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
		}

		if ( $this->css ) {
			wp_add_inline_style( 'table-builder-style', $this->css );
		}
	}

	/**
	 * Enqueues block assets (CSS & JS).
	 *
	 * @return void
	 */
	public function block_assets() {
		wp_enqueue_style( 'table-builder-style', get_stylesheet_uri(), array(), TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
	}
}

```
