PluginProbe
Gutenberg / 23.3.0
Gutenberg v23.3.0
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.3.0, at lib/block-supports/custom-css.php

310 lines 9.5 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
97 if ( ! is_string( $class_name_attr ) || ! str_contains( $class_name_attr, 'wp-custom-css-' ) ) {
98 return $block_content;
99 }
100
101 // Parse out the 'wp-custom-css-*' class name added by gutenberg_render_custom_css_support_styles().
102 $custom_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, 'wp-custom-css-' ) ) {
107 $custom_class_name = $class_token;
108 break;
109 }
110 $class_token = strtok( $token_delimiter );
111 }
112 if ( null === $custom_class_name ) {
113 return $block_content;
114 }
115
116 $tags = new WP_HTML_Tag_Processor( $block_content );
117
118 if ( $tags->next_tag() ) {
119 $tags->add_class( 'has-custom-css' );
120 $tags->add_class( $custom_class_name );
121 }
122
123 return $tags->get_updated_html();
124 }
125
126 // Remove core filters and action to avoid rendering duplicate custom CSS styles.
127 if ( function_exists( 'wp_render_custom_css_class_name' ) ) {
128 remove_filter( 'render_block', 'wp_render_custom_css_class_name' );
129 }
130 if ( function_exists( 'wp_render_custom_css_support_styles' ) ) {
131 remove_filter( 'render_block_data', 'wp_render_custom_css_support_styles' );
132 }
133 if ( function_exists( 'wp_enqueue_block_custom_css' ) ) {
134 remove_action( 'wp_enqueue_scripts', 'wp_enqueue_block_custom_css' );
135 }
136
137 // Add Gutenberg filters and action.
138 add_filter( 'render_block', 'gutenberg_render_custom_css_class_name', 10, 2 );
139 add_filter( 'render_block_data', 'gutenberg_render_custom_css_support_styles', 10, 1 );
140 add_action( 'wp_enqueue_scripts', 'gutenberg_enqueue_block_custom_css', 1 );
141
142 /**
143 * Registers the style block attribute for block types that support it.
144 *
145 * @param WP_Block_Type $block_type Block Type.
146 */
147 function gutenberg_register_custom_css_support( $block_type ) {
148 // Setup attributes and styles within that if needed.
149 if ( ! $block_type->attributes ) {
150 $block_type->attributes = array();
151 }
152
153 // Check for existing style attribute definition e.g. from block.json.
154 if ( array_key_exists( 'style', $block_type->attributes ) ) {
155 return;
156 }
157
158 $has_custom_css_support = block_has_support( $block_type, array( 'customCSS' ), true );
159
160 if ( $has_custom_css_support ) {
161 $block_type->attributes['style'] = array(
162 'type' => 'object',
163 );
164 }
165 }
166
167 /**
168 * Strips `style.css` attributes from all blocks in post content.
169 *
170 * Uses WP_Block_Parser::next_token() to scan block tokens and surgically
171 * replace only the attribute JSON that changed — no parse_blocks() +
172 * serialize_blocks() round-trip needed.
173 *
174 * @param string $content Post content to filter, expected to be escaped with slashes.
175 * @return string Filtered post content with block custom CSS removed.
176 */
177 function gutenberg_strip_custom_css_from_blocks( $content ) {
178 if ( ! has_blocks( $content ) ) {
179 return $content;
180 }
181
182 $unslashed = stripslashes( $content );
183
184 $parser = new WP_Block_Parser();
185 $parser->document = $unslashed;
186 $parser->offset = 0;
187 $end = strlen( $unslashed );
188 $replacements = array();
189
190 while ( $parser->offset < $end ) {
191 $next_token = $parser->next_token();
192 list( $token_type, , $attrs, $start_offset, $token_length ) = $next_token;
193
194 if ( 'no-more-tokens' === $token_type ) {
195 break;
196 }
197
198 $parser->offset = $start_offset + $token_length;
199
200 if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) {
201 continue;
202 }
203
204 if ( ! isset( $attrs['style']['css'] ) ) {
205 continue;
206 }
207
208 // Remove css and clean up empty style.
209 unset( $attrs['style']['css'] );
210 if ( empty( $attrs['style'] ) ) {
211 unset( $attrs['style'] );
212 }
213
214 // Locate the JSON portion within the token.
215 $token_string = substr( $unslashed, $start_offset, $token_length );
216 $json_rel_start = strcspn( $token_string, '{' );
217 $json_rel_end = strrpos( $token_string, '}' );
218
219 $json_start = $start_offset + $json_rel_start;
220 $json_length = $json_rel_end - $json_rel_start + 1;
221
222 // Re-encode attributes. If attrs is now empty, remove JSON and trailing space.
223 if ( empty( $attrs ) ) {
224 // Remove the trailing space after JSON: `{"style":{"css":"x"}} ` → ``
225 $replacements[] = array( $json_start, $json_length + 1, '' );
226 } else {
227 $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) );
228 }
229 }
230
231 if ( empty( $replacements ) ) {
232 return $content;
233 }
234
235 // Build the result by splicing replacements into the original string.
236 $result = '';
237 $was_at = 0;
238
239 foreach ( $replacements as $replacement ) {
240 list( $offset, $length, $new_json ) = $replacement;
241 $result .= substr( $unslashed, $was_at, $offset - $was_at ) . $new_json;
242 $was_at = $offset + $length;
243 }
244
245 if ( $was_at < $end ) {
246 $result .= substr( $unslashed, $was_at );
247 }
248
249 return addslashes( $result );
250 }
251
252 /**
253 * Adds the filters to strip custom CSS from block content on save.
254 * @access private
255 */
256 function gutenberg_custom_css_kses_init_filters() {
257 add_filter( 'content_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
258 add_filter( 'content_filtered_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
259 }
260
261 /**
262 * Removes the filters that strip custom CSS from block content on save.
263 * @access private
264 */
265 function gutenberg_custom_css_remove_filters() {
266 remove_filter( 'content_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
267 remove_filter( 'content_filtered_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
268 }
269
270 /**
271 * Registers the custom CSS content filters if the user does not have the edit_css capability.
272 * @access private
273 */
274 function gutenberg_custom_css_kses_init() {
275 gutenberg_custom_css_remove_filters();
276 if ( ! current_user_can( 'edit_css' ) ) {
277 gutenberg_custom_css_kses_init_filters();
278 }
279 }
280
281 /**
282 * Initializes custom CSS content filters when imported data should be filtered.
283 *
284 * This filter is the last being executed on force_filtered_html_on_import.
285 * If the input of the filter is true it means we are in an import situation and should
286 * enable the custom CSS filters, independently of the user capabilities.
287 * @access private
288 *
289 * @param mixed $arg Input argument of the filter.
290 * @return mixed Input argument of the filter.
291 */
292 function gutenberg_custom_css_force_filtered_html_on_import_filter( $arg ) {
293 if ( $arg ) {
294 gutenberg_custom_css_kses_init_filters();
295 }
296 return $arg;
297 }
298
299 add_action( 'init', 'gutenberg_custom_css_kses_init', 20 );
300 add_action( 'set_current_user', 'gutenberg_custom_css_kses_init' );
301 add_filter( 'force_filtered_html_on_import', 'gutenberg_custom_css_force_filtered_html_on_import_filter', 999 );
302
303 // Register the block support.
304 WP_Block_Supports::get_instance()->register(
305 'custom-css',
306 array(
307 'register_attribute' => 'gutenberg_register_custom_css_support',
308 )
309 );
310