| @@ -12,11 +12,17 @@ | ||
| 12 | 12 | |
| 13 | 13 | $style_depends = []; |
| 14 | 14 | $scripts_depends = []; |
| 15 | 15 | $aBlocks = []; |
| 16 | + $seen_refs = []; | |
| 17 | + $global_classes = []; | |
| 16 | 18 | |
| 17 | - self::recursive_block_parser( $parse_blocks_content, $aBlocks, $style_depends, $scripts_depends ); | |
| 19 | + self::recursive_block_parser( $parse_blocks_content, $aBlocks, $style_depends, $scripts_depends, $seen_refs, $global_classes ); | |
| 18 | 20 | |
| 21 | + // Dedupe once, after the whole block tree has been walked. | |
| 22 | + $style_depends = array_unique( $style_depends ); | |
| 23 | + $scripts_depends = array_unique( $scripts_depends ); | |
| 24 | + | |
| 19 | 25 | $register_styles = RegisterScripts::get_register_styles(); |
| 20 | 26 | $register_scripts = RegisterScripts::get_register_scripts(); |
| 21 | 27 | $library_css = ''; |
| 22 | 28 | $static_css = ''; |
| @@ -39,10 +45,9 @@ | ||
| 39 | 45 | |
| 40 | 46 | // css |
| 41 | 47 | foreach ( $style_depends as $style_depend ) { |
| 42 | 48 | if ( isset( $register_styles[ $style_depend ]['path'] ) ) { |
| 43 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents | |
| 44 | - $library_css .= file_get_contents( $register_styles[ $style_depend ]['path'] ) . "\n"; | |
| 49 | + $library_css .= self::read_library_file( $register_styles[ $style_depend ]['path'] ) . "\n"; | |
| 45 | 50 | } |
| 46 | 51 | } |
| 47 | 52 | |
| 48 | 53 | // js |
| @@ -47,35 +52,64 @@ | ||
| 47 | 52 | |
| 48 | 53 | // js |
| 49 | 54 | foreach ( $scripts_depends as $script_depend ) { |
| 50 | 55 | if ( isset( $register_scripts[ $script_depend ]['path'] ) ) { |
| 51 | - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents | |
| 52 | - $library_js .= file_get_contents( $register_scripts[ $script_depend ]['path'] ) . "\n"; | |
| 56 | + $library_js .= self::read_library_file( $register_scripts[ $script_depend ]['path'] ) . "\n"; | |
| 53 | 57 | } |
| 54 | 58 | } |
| 55 | 59 | |
| 60 | + // Merge per-instance rules that share an identical body into a single | |
| 61 | + // grouped selector (see CssDedupe). Always applied during generation — | |
| 62 | + // it produces byte-for-byte equivalent styling, just smaller. Filterable | |
| 63 | + // as a code-level escape hatch (no UI setting). | |
| 64 | + if ( (bool) apply_filters( 'ablocks/perf/dedupe_css', true ) ) { | |
| 65 | + $dynamic_css = CssDedupe::process( $dynamic_css ); | |
| 66 | + } | |
| 67 | + | |
| 68 | + // Global classes, narrowed to the ones this page uses. They sit between | |
| 69 | + // the blocks' static CSS and their per-instance rules so a block's own | |
| 70 | + // styles still win over a class it carries. | |
| 71 | + $global_css = GlobalClasses::compiled_css_for( | |
| 72 | + array_values( array_unique( $global_classes ) ) | |
| 73 | + ); | |
| 74 | + | |
| 56 | 75 | $FileUpload = new FileUpload(); |
| 57 | 76 | $destination_folder = $FileUpload->get_upload_dir(); |
| 58 | 77 | self::copy_build_image_folder_to_uploads( $destination_folder ); |
| 59 | - $FileUpload->create_file( $file_name . '.min.css', $library_css . $static_css . $dynamic_css ); | |
| 78 | + $FileUpload->create_file( $file_name . '.min.css', $library_css . $static_css . $global_css . $dynamic_css ); | |
| 60 | 79 | $FileUpload->create_file( $file_name . '.min.js', $library_js . $static_js ); |
| 61 | 80 | |
| 62 | 81 | return $aBlocks; |
| 63 | 82 | } |
| 64 | 83 | |
| 65 | - public static function recursive_block_parser( $parse_content, &$aBlocks, &$style_depends, &$scripts_depends ) { | |
| 84 | + public static function recursive_block_parser( $parse_content, &$aBlocks, &$style_depends, &$scripts_depends, &$seen_refs = [], &$global_classes = [] ) { | |
| 66 | 85 | if ( count( $parse_content ) > 0 ) { |
| 67 | 86 | foreach ( $parse_content as $item ) { |
| 68 | 87 | if ( ! empty( $item['blockName'] ) ) { |
| 69 | 88 | // Handle reusable blocks or patterns using "ref" |
| 70 | 89 | if ( $item['blockName'] === 'core/block' && ! empty( $item['attrs']['ref'] ) ) { |
| 71 | - $ref_post_id = $item['attrs']['ref']; | |
| 90 | + $ref_post_id = (int) $item['attrs']['ref']; | |
| 91 | + // Guard against circular / repeated reusable-block references. | |
| 92 | + if ( in_array( $ref_post_id, $seen_refs, true ) ) { | |
| 93 | + continue; | |
| 94 | + } | |
| 95 | + $seen_refs[] = $ref_post_id; | |
| 72 | 96 | $ref_post = get_post( $ref_post_id ); // Get the reusable block or pattern |
| 73 | 97 | if ( $ref_post ) { |
| 74 | 98 | $ref_content = parse_blocks( $ref_post->post_content ); // Parse the reusable block's content |
| 75 | - self::recursive_block_parser( $ref_content, $aBlocks, $style_depends, $scripts_depends ); // Recursively parse the referenced block/pattern | |
| 99 | + self::recursive_block_parser( $ref_content, $aBlocks, $style_depends, $scripts_depends, $seen_refs, $global_classes ); // Recursively parse the referenced block/pattern | |
| 76 | 100 | } |
| 77 | 101 | } elseif ( strpos( $item['blockName'], 'ablocks' ) !== false ) { |
| 102 | + // Which global classes this page actually needs. Collected | |
| 103 | + // from the walk rather than from a stored per-post index: | |
| 104 | + // the walk is happening anyway, and an index would be one | |
| 105 | + // more thing to keep in sync with the content. | |
| 106 | + if ( ! empty( $item['attrs']['globalClasses'] ) && is_array( $item['attrs']['globalClasses'] ) ) { | |
| 107 | + foreach ( $item['attrs']['globalClasses'] as $global_class_id ) { | |
| 108 | + $global_classes[] = (string) $global_class_id; | |
| 109 | + } | |
| 110 | + } | |
| 111 | + | |
| 78 | 112 | $block_name_class = str_replace( ' ', '', ucwords( str_replace( '-', ' ', explode( '/', $item['blockName'] )[1] ) ) ); |
| 79 | 113 | |
| 80 | 114 | $dynamic_class = '\\ABlocks\\Blocks\\' . $block_name_class . '\\Block'; |
| 81 | 115 | if ( ! class_exists( $dynamic_class ) ) { |
| @@ -97,21 +131,37 @@ | ||
| 97 | 131 | } |
| 98 | 132 | // 'ablocks-animate-style', |
| 99 | 133 | // if animation is used then added ablocks-animate-style dependency |
| 100 | 134 | if ( isset( $attributes['_animation']['animationType'] ) && ! empty( $attributes['_animation']['animationType'] ) && $attributes['_animation']['animationType'] !== 'none' ) { |
| 101 | - $style_depends = array_unique( array_merge( $style_depends, [ 'ablocks-animate-style' ] ) ); | |
| 135 | + $style_depends[] = 'ablocks-animate-style'; | |
| 102 | 136 | } |
| 103 | - // Capture library scripts | |
| 104 | - $style_depends = array_unique( array_merge( $style_depends, $instance->get_style_depends() ) ); | |
| 105 | - $scripts_depends = array_unique( array_merge( $scripts_depends, $instance->get_script_depends() ) ); | |
| 137 | + // Capture library scripts. Just accumulate here; the caller | |
| 138 | + // dedups once after the full tree is walked (avoids O(n^2) | |
| 139 | + // array_unique on every block). | |
| 140 | + $style_depends = array_merge( $style_depends, $instance->get_style_depends() ); | |
| 141 | + $scripts_depends = array_merge( $scripts_depends, $instance->get_script_depends() ); | |
| 106 | 142 | |
| 107 | - // Capture dynamic CSS | |
| 143 | + // Capture dynamic CSS. | |
| 144 | + // | |
| 145 | + // A block can be reached twice in one walk: on an FSE theme | |
| 146 | + // `pre_render_block` collects EVERY block — parents and their | |
| 147 | + // children alike — and this parser also recurses into | |
| 148 | + // `innerBlocks`, so a nested block is seen once as a child and | |
| 149 | + // once as a collected top-level entry. Compiling it a second | |
| 150 | + // time is not idempotent for blocks whose CSS is emitted | |
| 151 | + // through a per-request dedupe (the atomic blocks' hashed | |
| 152 | + // `ablocks-s-*` rules): the repeat build returns an empty | |
| 153 | + // string because the rule was already emitted, and writing | |
| 154 | + // that over the first result stripped every nested atomic | |
| 155 | + // block's styles from the page. Keep the first result. | |
| 108 | 156 | if ( isset( $item['attrs']['ref'] ) || isset( $item['attrs']['block_id'] ) ) { |
| 109 | 157 | $block_id_or_ref = ! empty( $item['attrs']['block_id'] ) ? $item['attrs']['block_id'] : 'core_pattern_ref_' . $item['attrs']['ref']; |
| 110 | - $aBlocks[ $block_id_or_ref ] = [ | |
| 111 | - 'block_name' => $item['blockName'], | |
| 112 | - 'dynamic_style' => $instance->build_css( $attributes ), | |
| 113 | - ]; | |
| 158 | + if ( ! isset( $aBlocks[ $block_id_or_ref ]['dynamic_style'] ) ) { | |
| 159 | + $aBlocks[ $block_id_or_ref ] = [ | |
| 160 | + 'block_name' => $item['blockName'], | |
| 161 | + 'dynamic_style' => $instance->build_css( $attributes ), | |
| 162 | + ]; | |
| 163 | + } | |
| 114 | 164 | } |
| 115 | 165 | }//end if |
| 116 | 166 | }//end if |
| 117 | 167 | |
| @@ -116,13 +166,27 @@ | ||
| 116 | 166 | }//end if |
| 117 | 167 | |
| 118 | 168 | // Check for inner blocks and recursively process them |
| 119 | 169 | if ( is_array( $item['innerBlocks'] ) && count( $item['innerBlocks'] ) ) { |
| 120 | - self::recursive_block_parser( $item['innerBlocks'], $aBlocks, $style_depends, $scripts_depends ); | |
| 170 | + self::recursive_block_parser( $item['innerBlocks'], $aBlocks, $style_depends, $scripts_depends, $seen_refs, $global_classes ); | |
| 121 | 171 | } |
| 122 | 172 | }//end if |
| 123 | 173 | }//end foreach |
| 124 | 174 | }//end if |
| 175 | + } | |
| 176 | + | |
| 177 | + /** | |
| 178 | + * Read a static library asset file, cached per request so the same library | |
| 179 | + * file isn't re-read from disk when multiple pages are generated in one | |
| 180 | + * request (e.g. regenerate-all). | |
| 181 | + */ | |
| 182 | + private static $library_file_cache = []; | |
| 183 | + private static function read_library_file( $path ) { | |
| 184 | + if ( ! isset( self::$library_file_cache[ $path ] ) ) { | |
| 185 | + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents | |
| 186 | + self::$library_file_cache[ $path ] = (string) file_get_contents( $path ); | |
| 187 | + } | |
| 188 | + return self::$library_file_cache[ $path ]; | |
| 125 | 189 | } |
| 126 | 190 | |
| 127 | 191 | public static function minify_css( $css_string ) { |
| 128 | 192 | // Remove comments (/* ... */) using regex |