PluginProbe
Gutenberg / 23.5.2
Gutenberg v23.5.2
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / lib / block-supports / custom-css.php

custom-css.php in Gutenberg 23.5.2, at lib/block-supports/custom-css.php

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