| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Custom CSS block support. 4 * 5 * @package WordPress 6 */ 7 8 /** 9 * Render the custom CSS stylesheet and add class name to block as required. 10 * 11 * @since 7.0.0 12 * 13 * @param array $parsed_block The parsed block. 14 * @return array The same parsed block with custom CSS class name added if appropriate. 15 * 16 * @phpstan-param array{ 17 * blockName: string|null, 18 * attrs: array{ 19 * className?: string, 20 * style?: array{ 21 * css?: string, 22 * ... 23 * }, 24 * ... 25 * }, 26 * ... 27 * } $parsed_block 28 */ 29 function wp_render_custom_css_support_styles( $parsed_block ) { 30 $custom_css = $parsed_block['attrs']['style']['css'] ?? null; 31 if ( ! is_string( $custom_css ) || '' === trim( $custom_css ) ) { 32 return $parsed_block; 33 } 34 35 $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $parsed_block['blockName'] ); 36 if ( ! block_has_support( $block_type, 'customCSS', true ) ) { 37 return $parsed_block; 38 } 39 40 // Validate CSS doesn't contain HTML markup (same validation as global styles REST API). 41 if ( preg_match( '#</?\w+#', $custom_css ) ) { 42 return $parsed_block; 43 } 44 45 // Generate a unique class name for this block instance. 46 $class_name = wp_unique_id_from_values( $parsed_block, 'wp-custom-css-' ); 47 $existing_class_name = $parsed_block['attrs']['className'] ?? null; 48 $updated_class_name = is_string( $existing_class_name ) 49 ? "$existing_class_name $class_name" 50 : $class_name; 51 52 _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); 53 54 // Process the custom CSS using the same method as global styles. 55 $selector = '.' . $class_name; 56 $processed_css = WP_Theme_JSON::process_blocks_custom_css( $custom_css, $selector ); 57 58 if ( ! empty( $processed_css ) ) { 59 /* 60 * Register and add inline style for block custom CSS. 61 * The style depends on global-styles to ensure custom CSS loads after 62 * and can override global styles. 63 */ 64 wp_register_style( 'wp-block-custom-css', false, array( 'global-styles' ) ); 65 wp_add_inline_style( 'wp-block-custom-css', $processed_css ); 66 } 67 68 return $parsed_block; 69 } 70 71 /** 72 * Enqueues the block custom CSS styles. 73 * 74 * @since 7.0.0 75 */ 76 function wp_enqueue_block_custom_css() { 77 wp_enqueue_style( 'wp-block-custom-css' ); 78 } 79 80 /** 81 * Applies the custom CSS class name to the block's rendered HTML. 82 * 83 * The class name is generated in {@see wp_render_custom_css_support_styles()} 84 * and stored in block attributes. This filter adds it to the actual markup. 85 * 86 * @since 7.0.0 87 * 88 * @param string $block_content Rendered block content. 89 * @param array $block Block object. 90 * @return string Filtered block content. 91 * 92 * @phpstan-param array{ 93 * attrs: array{ 94 * className?: string, 95 * ... 96 * }, 97 * ... 98 * } $block 99 */ 100 function wp_render_custom_css_class_name( $block_content, $block ) { 101 $class_name_attr = $block['attrs']['className'] ?? null; 102 $class_name_prefix = 'wp-custom-css-'; 103 if ( ! is_string( $class_name_attr ) || ! str_contains( $class_name_attr, $class_name_prefix ) ) { 104 return $block_content; 105 } 106 107 // Parse out the 'wp-custom-css-*' class name added by wp_render_custom_css_support_styles(). 108 $matched_class_name = null; 109 $token_delimiter = " \t\f\r\n"; 110 $class_token = strtok( $class_name_attr, $token_delimiter ); 111 while ( false !== $class_token ) { 112 if ( str_starts_with( $class_token, $class_name_prefix ) ) { 113 $matched_class_name = $class_token; 114 break; 115 } 116 $class_token = strtok( $token_delimiter ); 117 } 118 if ( null === $matched_class_name ) { 119 return $block_content; 120 } 121 122 $tags = new WP_HTML_Tag_Processor( $block_content ); 123 if ( $tags->next_tag() ) { 124 $tags->add_class( 'has-custom-css' ); 125 $tags->add_class( $matched_class_name ); 126 } 127 128 return $tags->get_updated_html(); 129 } 130 131 add_filter( 'render_block', 'wp_render_custom_css_class_name', 10, 2 ); 132 add_filter( 'render_block_data', 'wp_render_custom_css_support_styles', 10, 1 ); 133 add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_custom_css', 1 ); 134 135 /** 136 * Registers the style block attribute for block types that support it. 137 * 138 * @param WP_Block_Type $block_type Block Type. 139 */ 140 function wp_register_custom_css_support( $block_type ) { 141 // Setup attributes and styles within that if needed. 142 if ( ! $block_type->attributes ) { 143 $block_type->attributes = array(); 144 } 145 146 // Check for existing style attribute definition e.g. from block.json. 147 if ( array_key_exists( 'style', $block_type->attributes ) ) { 148 return; 149 } 150 151 $has_custom_css_support = block_has_support( $block_type, array( 'customCSS' ), true ); 152 153 if ( $has_custom_css_support ) { 154 $block_type->attributes['style'] = array( 155 'type' => 'object', 156 ); 157 } 158 } 159 160 /** 161 * Strips custom CSS (`style.css` in attributes) from all blocks in post content. 162 * 163 * Uses {@see WP_Block_Parser::next_token()} to scan block tokens and surgically 164 * replace only the attribute JSON that changed — no parse_blocks() + 165 * serialize_blocks() round-trip needed. 166 * 167 * @since 7.0.0 168 * @access private 169 * 170 * @param string $content Post content to filter, expected to be escaped with slashes. 171 * @return string Filtered post content with block custom CSS removed. 172 */ 173 function wp_strip_custom_css_from_blocks( $content ) { 174 if ( ! has_blocks( $content ) ) { 175 return $content; 176 } 177 178 $unslashed = stripslashes( $content ); 179 180 $parser = new WP_Block_Parser(); 181 $parser->document = $unslashed; 182 $parser->offset = 0; 183 $end = strlen( $unslashed ); 184 $replacements = array(); 185 186 while ( $parser->offset < $end ) { 187 $next_token = $parser->next_token(); 188 189 if ( 'no-more-tokens' === $next_token[0] ) { 190 break; 191 } 192 193 list( $token_type, , $attrs, $start_offset, $token_length ) = $next_token; 194 195 $parser->offset = $start_offset + $token_length; 196 197 if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) { 198 continue; 199 } 200 201 if ( ! isset( $attrs['style']['css'] ) ) { 202 continue; 203 } 204 205 // Remove css and clean up empty style. 206 unset( $attrs['style']['css'] ); 207 if ( empty( $attrs['style'] ) ) { 208 unset( $attrs['style'] ); 209 } 210 211 // Locate the JSON portion within the token. 212 $token_string = substr( $unslashed, $start_offset, $token_length ); 213 $json_rel_start = strcspn( $token_string, '{' ); 214 $json_rel_end = strrpos( $token_string, '}' ); 215 216 $json_start = $start_offset + $json_rel_start; 217 $json_length = $json_rel_end - $json_rel_start + 1; 218 219 // Re-encode attributes. If attrs is now empty, remove JSON and trailing space. 220 if ( empty( $attrs ) ) { 221 // Remove the trailing space after JSON. 222 $replacements[] = array( $json_start, $json_length + 1, '' ); 223 } else { 224 $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) ); 225 } 226 } 227 228 if ( empty( $replacements ) ) { 229 return $content; 230 } 231 232 // Build the result by splicing replacements into the original string. 233 $result = ''; 234 $was_at = 0; 235 236 foreach ( $replacements as $replacement ) { 237 list( $offset, $length, $new_json ) = $replacement; 238 $result .= substr( $unslashed, $was_at, $offset - $was_at ) . $new_json; 239 $was_at = $offset + $length; 240 } 241 242 if ( $was_at < $end ) { 243 $result .= substr( $unslashed, $was_at ); 244 } 245 246 return addslashes( $result ); 247 } 248 249 /** 250 * Adds the filters to strip custom CSS from block content on save. 251 * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). 252 * 253 * @since 7.0.0 254 * @access private 255 */ 256 function wp_custom_css_kses_init_filters() { 257 add_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); 258 add_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); 259 } 260 261 /** 262 * Removes the filters that strip custom CSS from block content on save. 263 * Priority of 8 to run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). 264 * 265 * @since 7.0.0 266 * @access private 267 */ 268 function wp_custom_css_remove_filters() { 269 remove_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); 270 remove_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks', 8 ); 271 } 272 273 /** 274 * Registers the custom CSS content filters if the user does not have the edit_css capability. 275 * 276 * @since 7.0.0 277 * @access private 278 */ 279 function wp_custom_css_kses_init() { 280 wp_custom_css_remove_filters(); 281 if ( ! current_user_can( 'edit_css' ) ) { 282 wp_custom_css_kses_init_filters(); 283 } 284 } 285 286 /** 287 * Initializes custom CSS content filters when imported data should be filtered. 288 * 289 * Runs at priority 999 on {@see 'force_filtered_html_on_import'} to ensure it 290 * fires after general KSES initialization, independently of user capabilities. 291 * If the input of the filter is true it means we are in an import situation and should 292 * enable the custom CSS filters, independently of the user capabilities. 293 * 294 * @since 7.0.0 295 * @access private 296 * 297 * @param mixed $arg Input argument of the filter. 298 * @return mixed Input argument of the filter. 299 */ 300 function wp_custom_css_force_filtered_html_on_import_filter( $arg ) { 301 if ( $arg ) { 302 wp_custom_css_kses_init_filters(); 303 } 304 return $arg; 305 } 306 307 // Run before wp_filter_global_styles_post (priority 9) and wp_filter_post_kses (priority 10). 308 add_action( 'init', 'wp_custom_css_kses_init', 20 ); 309 add_action( 'set_current_user', 'wp_custom_css_kses_init' ); 310 add_filter( 'force_filtered_html_on_import', 'wp_custom_css_force_filtered_html_on_import_filter', 999 ); 311 312 // Register the block support. 313 WP_Block_Supports::get_instance()->register( 314 'custom-css', 315 array( 316 'register_attribute' => 'wp_register_custom_css_support', 317 ) 318 );
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Sat Jun 13 09:38:55 2026 | Cross-referenced by PHPXref |