PluginProbe
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables / 2.3.0
TableKit – WordPress Table Builder for Data Tables, WooCommerce Product Tables & Post Tables v2.3.0
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
← All changes | includes/Hooks/AssetGenerator.php +449 -7 2.2.10 → 2.3.0 View file →
@@ -32,8 +32,52 @@
32 32 */
33 33 protected $fonts = array();
34 34
35 35 /**
36 + * Handle used for the theme-stylesheet backward-compat fallback (frontend only).
37 + *
38 + * @var string
39 + */
40 + const THEME_STYLESHEET_FALLBACK_HANDLE = 'tablekit-theme-stylesheet-fallback';
41 +
42 + /**
43 + * Max nesting depth when following synced patterns / registered patterns.
44 + *
45 + * @var int
46 + */
47 + const MAX_PATTERN_DEPTH = 10;
48 +
49 + /**
50 + * Keys of table blocks whose CSS has already been collected/output this request.
51 + *
52 + * @var array<string,bool>
53 + */
54 + protected $emitted_blocks = array();
55 +
56 + /**
57 + * CSS of table blocks rendered after the <head> CSS was finalised, keyed by
58 + * block key, waiting to be printed right before that block's markup.
59 + *
60 + * @var array<string,string>
61 + */
62 + protected $pending_late_css = array();
63 +
64 + /**
65 + * Whether the collected CSS has already been handed to WordPress for <head>.
66 + *
67 + * @var bool
68 + */
69 + protected $head_css_added = false;
70 +
71 + /**
72 + * Nesting counter for renders that print their own CSS (e.g. [tableKit]
73 + * shortcode, Elementor widget). While > 0, late CSS is not printed.
74 + *
75 + * @var int
76 + */
77 + protected $late_css_suspended = 0;
78 +
79 + /**
36 80 * AssetGenerator class constructor.
37 81 * private for singleton
38 82 *
39 83 * @return void
@@ -45,8 +89,10 @@
45 89 add_filter( 'render_block_data', array( $this, 'set_blocks_css' ), 10 );
46 90 add_filter( 'wp_resource_hints', array( $this, 'fonts_resource_hints' ), 10, 2 );
47 91 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ), 10 );
48 92 add_action( 'enqueue_block_assets', array( $this, 'block_assets' ), 10 );
93 + add_action( 'wp_enqueue_scripts', array( $this, 'maybe_dequeue_theme_stylesheet_fallback' ), 999 );
94 + add_filter( 'render_block', array( $this, 'prepend_late_block_css' ), 1000, 2 );
49 95
50 96 // Clear FSE cache when templates are saved.
51 97 add_action( 'save_post_wp_template', array( $this, 'clear_fse_cache' ) );
52 98 add_action( 'save_post_wp_template_part', array( $this, 'clear_fse_cache' ) );
@@ -346,9 +392,9 @@
346 392 $blocks_css[ $device ] = '';
347 393 }
348 394
349 395 if ( is_string( $css ) ) {
350 - $blocks_css[ $device ] .= $css;
396 + $blocks_css[ $device ] .= preg_replace( '/<[^>]*>?/', '', $css );
351 397 }
352 398 }
353 399 }
354 400
@@ -361,9 +407,11 @@
361 407 if ( ! isset( $blocks_css[ $device ] ) ) {
362 408 $blocks_css[ $device ] = '';
363 409 }
364 410
365 - $blocks_css[ $device ] .= $css;
411 + if ( is_string( $css ) ) {
412 + $blocks_css[ $device ] .= preg_replace( '/<[^>]*>?/', '', $css );
413 + }
366 414 }
367 415 }
368 416 }
369 417
@@ -474,16 +522,287 @@
474 522 * @param array $parsed_block The parsed block data.
475 523 * @return array The modified parsed block data.
476 524 */
477 525 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;
526 + if ( ! is_array( $parsed_block ) || ! $this->is_table_block( $parsed_block ) ) {
527 + return $parsed_block;
481 528 }
529 +
530 + $key = $this->get_block_key( $parsed_block );
531 +
532 + if ( isset( $this->emitted_blocks[ $key ] ) ) {
533 + return $parsed_block;
534 + }
535 +
536 + // Before the <head> CSS is finalised (block themes render templates early,
537 + // classic themes are pre-collected in enqueue_scripts()): add to the inline CSS.
538 + if ( ! $this->head_css_added ) {
539 + $this->emitted_blocks[ $key ] = true;
540 +
541 + $css_content = $this->combine_blocks_assets( $parsed_block );
542 + if ( ! empty( $css_content ) ) {
543 + $this->css .= $css_content;
544 + }
545 +
546 + return $parsed_block;
547 + }
548 +
549 + // Rendered after <head> (widgets, archive loops, nested queries...): collect
550 + // this block's whole subtree once and print it before the block markup.
551 + $late_css = $this->collect_late_tree_css( $parsed_block );
552 +
553 + if ( '' !== $late_css && 0 === $this->late_css_suspended ) {
554 + $this->pending_late_css[ $key ] = $late_css;
555 + }
556 +
482 557 return $parsed_block;
483 558 }
484 559
485 560 /**
561 + * Collects CSS for a table block and all its inner table blocks that haven't
562 + * been output yet, marking each one as output.
563 + *
564 + * @param array $parsed_block Parsed block.
565 + * @return string Combined CSS.
566 + */
567 + protected function collect_late_tree_css( array $parsed_block ): string {
568 + $css = '';
569 +
570 + if ( $this->is_table_block( $parsed_block ) ) {
571 + $key = $this->get_block_key( $parsed_block );
572 +
573 + if ( ! isset( $this->emitted_blocks[ $key ] ) ) {
574 + $this->emitted_blocks[ $key ] = true;
575 + $css .= $this->combine_blocks_assets( $parsed_block );
576 + }
577 + }
578 +
579 + foreach ( $parsed_block['innerBlocks'] ?? array() as $inner_block ) {
580 + if ( is_array( $inner_block ) ) {
581 + $css .= $this->collect_late_tree_css( $inner_block );
582 + }
583 + }
584 +
585 + return $css;
586 + }
587 +
588 + /**
589 + * Prints CSS collected for a late-rendered table block right before its markup.
590 + * Hooked to "render_block" at a late priority so other markup filters
591 + * (which target the block's first tag) run first.
592 + *
593 + * @param string $block_content Rendered block HTML.
594 + * @param array $parsed_block Parsed block data.
595 + * @return string
596 + */
597 + public function prepend_late_block_css( $block_content, $parsed_block ) {
598 + if ( empty( $this->pending_late_css ) || ! is_array( $parsed_block ) || ! $this->is_table_block( $parsed_block ) ) {
599 + return $block_content;
600 + }
601 +
602 + $key = $this->get_block_key( $parsed_block );
603 +
604 + if ( ! isset( $this->pending_late_css[ $key ] ) ) {
605 + return $block_content;
606 + }
607 +
608 + $css = $this->pending_late_css[ $key ];
609 + unset( $this->pending_late_css[ $key ] );
610 +
611 + return '<style id="tablekit-css-' . esc_attr( sanitize_html_class( $key ) ) . '">' . $css . '</style>' . $block_content;
612 + }
613 +
614 + /**
615 + * Suspends late CSS printing, for renderers that print their own CSS
616 + * (e.g. the [tableKit] shortcode or the Elementor widget). Must be paired
617 + * with resume_late_css().
618 + *
619 + * @return void
620 + */
621 + public function suspend_late_css(): void {
622 + ++$this->late_css_suspended;
623 + }
624 +
625 + /**
626 + * Resumes late CSS printing after suspend_late_css().
627 + *
628 + * @return void
629 + */
630 + public function resume_late_css(): void {
631 + $this->late_css_suspended = max( 0, $this->late_css_suspended - 1 );
632 + }
633 +
634 + /**
635 + * Whether a parsed block is one of TableKit's blocks.
636 + *
637 + * @param array $parsed_block Parsed block.
638 + * @return bool
639 + */
640 + protected function is_table_block( array $parsed_block ): bool {
641 + return isset( $parsed_block['blockName'] ) && is_string( $parsed_block['blockName'] ) && false !== strpos( $parsed_block['blockName'], 'tablebuilder' );
642 + }
643 +
644 + /**
645 + * Stable per-request key for a table block (its blockID, or a hash of its attributes).
646 + *
647 + * @param array $parsed_block Parsed block.
648 + * @return string
649 + */
650 + protected function get_block_key( array $parsed_block ): string {
651 + $block_id = $parsed_block['attrs']['blockID'] ?? '';
652 +
653 + if ( is_string( $block_id ) && '' !== $block_id ) {
654 + return $parsed_block['blockName'] . ':' . $block_id;
655 + }
656 +
657 + return $parsed_block['blockName'] . ':' . md5( (string) wp_json_encode( $parsed_block['attrs'] ?? array() ) );
658 + }
659 +
660 + /**
661 + * Collects TableKit CSS (and enqueues block assets) for the current post on
662 + * classic themes WITHOUT rendering any block.
663 + *
664 + * IMPORTANT: never render blocks here (no do_blocks, render_block or the_content). Rendering
665 + * third-party blocks early triggers their one-time side effects (e.g. Formidable
666 + * Forms prints and marks its stylesheet as loaded inside the discarded output,
667 + * so forms lose their styling) and processes form submissions twice.
668 + *
669 + * @param \WP_Post $post Post to collect CSS for.
670 + * @return void
671 + */
672 + protected function collect_post_css( \WP_Post $post ): void {
673 + $content = (string) $post->post_content;
674 +
675 + if ( '' === $content
676 + || ( false === strpos( $content, 'tablebuilder' )
677 + && false === strpos( $content, '<!-- wp:block ' )
678 + && false === strpos( $content, '<!-- wp:pattern ' ) )
679 + ) {
680 + return;
681 + }
682 +
683 + $seen = array();
684 + $this->collect_css_from_blocks( parse_blocks( $content ), $seen, 0 );
685 + }
686 +
687 + /**
688 + * Walks a parsed block tree (following synced patterns and registered
689 + * patterns) and collects CSS for every TableKit block, in document order.
690 + *
691 + * @param array $blocks Parsed blocks.
692 + * @param array $seen Pattern references already followed (loop guard).
693 + * @param int $depth Current pattern nesting depth.
694 + * @return void
695 + */
696 + protected function collect_css_from_blocks( array $blocks, array &$seen, int $depth ): void {
697 + foreach ( $blocks as $block ) {
698 + if ( ! is_array( $block ) ) {
699 + continue;
700 + }
701 +
702 + $name = $block['blockName'] ?? '';
703 +
704 + if ( $this->is_table_block( $block ) ) {
705 + $this->set_blocks_css( $block );
706 + $this->enqueue_table_block_assets( $name );
707 + }
708 +
709 + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
710 + $this->collect_css_from_blocks( $block['innerBlocks'], $seen, $depth );
711 + }
712 +
713 + if ( $depth >= self::MAX_PATTERN_DEPTH ) {
714 + continue;
715 + }
716 +
717 + $pattern_content = $this->get_pattern_content( $block, $seen );
718 +
719 + if ( '' !== $pattern_content ) {
720 + $this->collect_css_from_blocks( parse_blocks( $pattern_content ), $seen, $depth + 1 );
721 + }
722 + }
723 + }
724 +
725 + /**
726 + * Returns the content of a synced pattern (core/block) or registered pattern
727 + * (core/pattern) block, once per request.
728 + *
729 + * @param array $block Parsed block.
730 + * @param array $seen Pattern references already followed (loop guard).
731 + * @return string Pattern content, or an empty string.
732 + */
733 + protected function get_pattern_content( array $block, array &$seen ): string {
734 + $name = $block['blockName'] ?? '';
735 +
736 + if ( 'core/block' === $name && ! empty( $block['attrs']['ref'] ) ) {
737 + $ref = 'ref:' . absint( $block['attrs']['ref'] );
738 +
739 + if ( isset( $seen[ $ref ] ) ) {
740 + return '';
741 + }
742 + $seen[ $ref ] = true;
743 +
744 + $pattern = get_post( absint( $block['attrs']['ref'] ) );
745 +
746 + if ( $pattern instanceof \WP_Post && 'wp_block' === $pattern->post_type && 'publish' === $pattern->post_status ) {
747 + return (string) $pattern->post_content;
748 + }
749 +
750 + return '';
751 + }
752 +
753 + if ( 'core/pattern' === $name && ! empty( $block['attrs']['slug'] ) && class_exists( '\\WP_Block_Patterns_Registry' ) ) {
754 + $slug = 'slug:' . (string) $block['attrs']['slug'];
755 +
756 + if ( isset( $seen[ $slug ] ) ) {
757 + return '';
758 + }
759 + $seen[ $slug ] = true;
760 +
761 + $registered = \WP_Block_Patterns_Registry::get_instance()->get_registered( (string) $block['attrs']['slug'] );
762 +
763 + return is_array( $registered ) && ! empty( $registered['content'] ) ? (string) $registered['content'] : '';
764 + }
765 +
766 + return '';
767 + }
768 +
769 + /**
770 + * Enqueues a TableKit block's registered frontend style/view-script handles
771 + * up front, so they still print in <head> exactly as they did when the block
772 + * was rendered early (prevents a flash of unstyled table).
773 + *
774 + * @param string $block_name Block name, e.g. "tablebuilder/table-builder".
775 + * @return void
776 + */
777 + protected function enqueue_table_block_assets( string $block_name ): void {
778 + static $done = array();
779 +
780 + if ( isset( $done[ $block_name ] ) ) {
781 + return;
782 + }
783 + $done[ $block_name ] = true;
784 +
785 + $block_type = \WP_Block_Type_Registry::get_instance()->get_registered( $block_name );
786 +
787 + if ( ! $block_type ) {
788 + return;
789 + }
790 +
791 + foreach ( (array) ( $block_type->style_handles ?? array() ) as $handle ) {
792 + if ( is_string( $handle ) && wp_style_is( $handle, 'registered' ) ) {
793 + wp_enqueue_style( $handle );
794 + }
795 + }
796 +
797 + foreach ( (array) ( $block_type->view_script_handles ?? array() ) as $handle ) {
798 + if ( is_string( $handle ) && wp_script_is( $handle, 'registered' ) ) {
799 + wp_enqueue_script( $handle );
800 + }
801 + }
802 + }
803 +
804 + /**
486 805 * Adds a preconnect resource hint for Google Fonts.
487 806 *
488 807 * @param array $urls URLs to print for resource hints.
489 808 * @param string $relation_type The relation type the URLs are printed.
@@ -508,10 +827,12 @@
508 827 */
509 828 public function enqueue_scripts() {
510 829 global $post;
511 830
831 + // Classic themes render the post after <head>, so collect its TableKit CSS
832 + // now by reading block attributes only, never by rendering (see collect_post_css()).
512 833 if ( ! wp_is_block_theme() && $post instanceof \WP_Post && ! empty( $post->post_content ) ) {
513 - do_blocks( $post->post_content );
834 + $this->collect_post_css( $post );
514 835 }
515 836
516 837 $fonts_url = $this->generate_fonts_url();
517 838 if ( $fonts_url ) {
@@ -519,16 +840,137 @@
519 840 }
520 841
521 842 if ( $this->css ) {
522 843 wp_add_inline_style( 'table-builder-style', $this->css );
844 + $this->print_inline_css_after_block_styles();
523 845 }
846 +
847 + $this->head_css_added = true;
524 848 }
525 849
526 850 /**
851 + * Makes TableKit's per-block inline CSS print AFTER the TableKit block
852 + * stylesheets (style-index.css) that are enqueued on this page.
853 + *
854 + * The per-block CSS generated from block settings uses selectors with the
855 + * same specificity as the block's base stylesheet, so it only wins when it
856 + * comes later in the page. Classic themes (e.g. Kadence) enqueue things in
857 + * an order where the inline CSS was printed first, so user-chosen colours,
858 + * paddings etc. were silently overridden by the defaults.
859 + *
860 + * @return void
861 + */
862 + protected function print_inline_css_after_block_styles(): void {
863 + $styles = wp_styles();
864 +
865 + if ( empty( $styles->registered['table-builder-style'] ) ) {
866 + return;
867 + }
868 +
869 + $deps = array();
870 +
871 + foreach ( \WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_name => $block_type ) {
872 + if ( false === strpos( (string) $block_name, 'tablebuilder' ) ) {
873 + continue;
874 + }
875 +
876 + foreach ( (array) ( $block_type->style_handles ?? array() ) as $handle ) {
877 + if ( is_string( $handle ) && 'table-builder-style' !== $handle && wp_style_is( $handle, 'enqueued' ) ) {
878 + $deps[] = $handle;
879 + }
880 + }
881 + }
882 +
883 + if ( empty( $deps ) ) {
884 + return;
885 + }
886 +
887 + $style = $styles->registered['table-builder-style'];
888 + $style->deps = array_values( array_unique( array_merge( (array) $style->deps, $deps ) ) );
889 + }
890 +
891 + /**
527 892 * Enqueues block assets (CSS & JS).
528 893 *
529 894 * @return void
530 895 */
531 896 public function block_assets() {
532 - wp_enqueue_style( 'table-builder-style', get_stylesheet_uri(), array(), TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
897 + // Block editor: unchanged behaviour.
898 + if ( is_admin() ) {
899 + wp_enqueue_style( 'table-builder-style', get_stylesheet_uri(), array(), TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
900 + return;
901 + }
902 +
903 + /**
904 + * Frontend backward-compat: some (child) themes never enqueue their own
905 + * style.css and may have relied on TableKit loading it. It is enqueued here
906 + * (same position as before) and dequeued again in
907 + * maybe_dequeue_theme_stylesheet_fallback() when the theme loads it itself,
908 + * so it is never printed twice.
909 + *
910 + * @since 2.2.15
911 + *
912 + * @param bool $load Whether to load the theme stylesheet as a fallback. Default true.
913 + */
914 + if ( apply_filters( 'tablekit_load_theme_stylesheet_fallback', true ) ) {
915 + wp_enqueue_style( self::THEME_STYLESHEET_FALLBACK_HANDLE, get_stylesheet_uri(), array(), TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
916 + }
917 +
918 + // Source-less handle that only carries TableKit's inline CSS.
919 + wp_register_style( 'table-builder-style', false, array(), TABLE_BUILDER_BLOCK_PLUGIN_VERSION );
920 + wp_enqueue_style( 'table-builder-style' );
921 + }
922 +
923 + /**
924 + * Dequeues the theme-stylesheet fallback when the theme (or anything else)
925 + * already enqueues the same stylesheet, directly or as a dependency.
926 + *
927 + * @return void
928 + */
929 + public function maybe_dequeue_theme_stylesheet_fallback() {
930 + if ( is_admin() || ! wp_style_is( self::THEME_STYLESHEET_FALLBACK_HANDLE, 'enqueued' ) ) {
931 + return;
932 + }
933 +
934 + $target = $this->normalize_style_src( get_stylesheet_uri() );
935 + $styles = wp_styles();
936 + $stack = (array) $styles->queue;
937 + $seen = array();
938 +
939 + while ( ! empty( $stack ) ) {
940 + $handle = array_pop( $stack );
941 +
942 + if ( ! is_string( $handle ) || isset( $seen[ $handle ] ) || self::THEME_STYLESHEET_FALLBACK_HANDLE === $handle ) {
943 + continue;
944 + }
945 + $seen[ $handle ] = true;
946 +
947 + $style = $styles->registered[ $handle ] ?? null;
948 +
949 + if ( ! $style ) {
950 + continue;
951 + }
952 +
953 + if ( is_string( $style->src ) && '' !== $style->src && $this->normalize_style_src( $style->src ) === $target ) {
954 + wp_dequeue_style( self::THEME_STYLESHEET_FALLBACK_HANDLE );
955 + return;
956 + }
957 +
958 + foreach ( (array) $style->deps as $dep ) {
959 + $stack[] = $dep;
960 + }
961 + }
962 + }
963 +
964 + /**
965 + * Normalises a stylesheet URL for comparison (drops scheme, host and query).
966 + *
967 + * @param string $src Stylesheet URL (absolute, protocol-relative or root-relative).
968 + * @return string
969 + */
970 + protected function normalize_style_src( string $src ): string {
971 + $src = strtok( $src, '?#' );
972 + $src = preg_replace( '#^(?:https?:)?//[^/]+#i', '', (string) $src );
973 +
974 + return strtolower( untrailingslashit( (string) $src ) );
533 975 }
534 976 }