PluginProbe
Gutenberg / trunk
Gutenberg vtrunk
24.1.0 24.0.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 All 404 releases
gutenberg / lib / block-supports / custom-css.php

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

337 lines 10.1 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 * @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 gutenberg_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 $parsed_block['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_Gutenberg::process_blocks_custom_css( $custom_css, $selector );
57
58 if ( ! empty( $processed_css ) ) {
59 /**
60 * Reuse one handle so identical custom CSS is enqueued only once via
61 * {@see wp_unique_id_from_values()}. Explicitly declare the `wp-block-library`
62 * dependency so `global-styles` is guaranteed to print after it, preventing
63 * block default styles from unintentionally overriding global styles.
64 */
65 $handle = 'wp-block-custom-css';
66 if ( ! wp_style_is( $handle, 'registered' ) ) {
67 wp_register_style( $handle, false, array( 'wp-block-library', 'global-styles' ) );
68 }
69 $after_styles = wp_styles()->get_data( $handle, 'after' );
70 if ( ! is_array( $after_styles ) ) {
71 $after_styles = array();
72 }
73 if ( ! in_array( $processed_css, $after_styles, true ) ) {
74 wp_add_inline_style( $handle, $processed_css );
75 }
76 }
77
78 return $parsed_block;
79 }
80
81 /**
82 * Enqueues the block custom CSS styles.
83 *
84 * @since 7.0.0
85 */
86 function gutenberg_enqueue_block_custom_css() {
87 wp_enqueue_style( 'wp-block-custom-css' );
88 }
89
90 /**
91 * Applies the custom CSS class name to the block's rendered HTML.
92 *
93 * The class name is generated in {@see gutenberg_render_custom_css_support_styles()}
94 * and stored in block attributes. This filter adds it to the actual markup.
95 *
96 * @since 7.0.0
97 *
98 * @param string $block_content Rendered block content.
99 * @param array $block Block object.
100 * @return string Filtered block content.
101 *
102 * @phpstan-param array{
103 * attrs: array{
104 * className?: string,
105 * ...
106 * },
107 * ...
108 * } $block
109 */
110 function gutenberg_render_custom_css_class_name( $block_content, $block ) {
111 $class_name_attr = $block['attrs']['className'] ?? null;
112 $class_name_prefix = 'wp-custom-css-';
113 if ( ! is_string( $class_name_attr ) || ! str_contains( $class_name_attr, $class_name_prefix ) ) {
114 return $block_content;
115 }
116
117 // Parse out the 'wp-custom-css-*' class name added by gutenberg_render_custom_css_support_styles().
118 $matched_class_name = null;
119 $token_delimiter = " \t\f\r\n";
120 $class_token = strtok( $class_name_attr, $token_delimiter );
121 while ( false !== $class_token ) {
122 if ( str_starts_with( $class_token, $class_name_prefix ) ) {
123 $matched_class_name = $class_token;
124 break;
125 }
126 $class_token = strtok( $token_delimiter );
127 }
128 if ( null === $matched_class_name ) {
129 return $block_content;
130 }
131
132 $tags = new WP_HTML_Tag_Processor( $block_content );
133 if ( $tags->next_tag() ) {
134 $tags->add_class( 'has-custom-css' );
135 $tags->add_class( $matched_class_name );
136 }
137
138 return $tags->get_updated_html();
139 }
140
141 // Remove core filters and action to avoid rendering duplicate custom CSS styles.
142 if ( function_exists( 'wp_render_custom_css_class_name' ) ) {
143 remove_filter( 'render_block', 'wp_render_custom_css_class_name' );
144 }
145 if ( function_exists( 'wp_render_custom_css_support_styles' ) ) {
146 remove_filter( 'render_block_data', 'wp_render_custom_css_support_styles' );
147 }
148 if ( function_exists( 'wp_enqueue_block_custom_css' ) ) {
149 remove_action( 'wp_enqueue_scripts', 'wp_enqueue_block_custom_css' );
150 }
151
152 // Add Gutenberg filters and action.
153 add_filter( 'render_block', 'gutenberg_render_custom_css_class_name', 10, 2 );
154 add_filter( 'render_block_data', 'gutenberg_render_custom_css_support_styles', 10, 1 );
155 add_action( 'wp_enqueue_scripts', 'gutenberg_enqueue_block_custom_css', 1 );
156
157 /**
158 * Registers the style block attribute for block types that support it.
159 *
160 * @since 7.0.0
161 *
162 * @param WP_Block_Type $block_type Block Type.
163 */
164 function gutenberg_register_custom_css_support( $block_type ) {
165 // Setup attributes and styles within that if needed.
166 if ( ! $block_type->attributes ) {
167 $block_type->attributes = array();
168 }
169
170 // Check for existing style attribute definition e.g. from block.json.
171 if ( array_key_exists( 'style', $block_type->attributes ) ) {
172 return;
173 }
174
175 $has_custom_css_support = block_has_support( $block_type, array( 'customCSS' ), true );
176
177 if ( $has_custom_css_support ) {
178 $block_type->attributes['style'] = array(
179 'type' => 'object',
180 );
181 }
182 }
183
184 /**
185 * Strips `style.css` attributes from all blocks in post content.
186 *
187 * Uses WP_Block_Parser::next_token() to scan block tokens and surgically
188 * replace only the attribute JSON that changed — no parse_blocks() +
189 * serialize_blocks() round-trip needed.
190 *
191 * @since 7.0.0
192 *
193 * @param string $content Post content to filter, expected to be escaped with slashes.
194 * @return string Filtered post content with block custom CSS removed.
195 */
196 function gutenberg_strip_custom_css_from_blocks( $content ) {
197 if ( ! has_blocks( $content ) ) {
198 return $content;
199 }
200
201 $unslashed = stripslashes( $content );
202
203 $parser = new WP_Block_Parser();
204 $parser->document = $unslashed;
205 $parser->offset = 0;
206 $end = strlen( $unslashed );
207 $replacements = array();
208
209 while ( $parser->offset < $end ) {
210 $next_token = $parser->next_token();
211 list( $token_type, , $attrs, $start_offset, $token_length ) = $next_token;
212
213 if ( 'no-more-tokens' === $token_type ) {
214 break;
215 }
216
217 $parser->offset = $start_offset + $token_length;
218
219 if ( 'block-opener' !== $token_type && 'void-block' !== $token_type ) {
220 continue;
221 }
222
223 if ( ! isset( $attrs['style']['css'] ) ) {
224 continue;
225 }
226
227 // Remove css and clean up empty style.
228 unset( $attrs['style']['css'] );
229 if ( empty( $attrs['style'] ) ) {
230 unset( $attrs['style'] );
231 }
232
233 // Locate the JSON portion within the token.
234 $token_string = substr( $unslashed, $start_offset, $token_length );
235 $json_rel_start = strcspn( $token_string, '{' );
236 $json_rel_end = strrpos( $token_string, '}' );
237
238 $json_start = $start_offset + $json_rel_start;
239 $json_length = $json_rel_end - $json_rel_start + 1;
240
241 // Re-encode attributes. If attrs is now empty, remove JSON and trailing space.
242 if ( empty( $attrs ) ) {
243 // Remove the trailing space after JSON: `{"style":{"css":"x"}} ` → ``
244 $replacements[] = array( $json_start, $json_length + 1, '' );
245 } else {
246 $replacements[] = array( $json_start, $json_length, serialize_block_attributes( $attrs ) );
247 }
248 }
249
250 if ( empty( $replacements ) ) {
251 return $content;
252 }
253
254 // Build the result by splicing replacements into the original string.
255 $result = '';
256 $was_at = 0;
257
258 foreach ( $replacements as $replacement ) {
259 list( $offset, $length, $new_json ) = $replacement;
260 $result .= substr( $unslashed, $was_at, $offset - $was_at ) . $new_json;
261 $was_at = $offset + $length;
262 }
263
264 if ( $was_at < $end ) {
265 $result .= substr( $unslashed, $was_at );
266 }
267
268 return addslashes( $result );
269 }
270
271 /**
272 * Adds the filters to strip custom CSS from block content on save.
273 *
274 * @since 7.0.0
275 * @access private
276 */
277 function gutenberg_custom_css_kses_init_filters() {
278 add_filter( 'content_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
279 add_filter( 'content_filtered_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
280 }
281
282 /**
283 * Removes the filters that strip custom CSS from block content on save.
284 *
285 * @since 7.0.0
286 * @access private
287 */
288 function gutenberg_custom_css_remove_filters() {
289 remove_filter( 'content_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
290 remove_filter( 'content_filtered_save_pre', 'gutenberg_strip_custom_css_from_blocks', 8 );
291 }
292
293 /**
294 * Registers the custom CSS content filters if the user does not have the edit_css capability.
295 *
296 * @since 7.0.0
297 * @access private
298 */
299 function gutenberg_custom_css_kses_init() {
300 gutenberg_custom_css_remove_filters();
301 if ( ! current_user_can( 'edit_css' ) ) {
302 gutenberg_custom_css_kses_init_filters();
303 }
304 }
305
306 /**
307 * Initializes custom CSS content filters when imported data should be filtered.
308 *
309 * This filter is the last being executed on force_filtered_html_on_import.
310 * If the input of the filter is true it means we are in an import situation and should
311 * enable the custom CSS filters, independently of the user capabilities.
312 *
313 * @since 7.0.0
314 * @access private
315 *
316 * @param mixed $arg Input argument of the filter.
317 * @return mixed Input argument of the filter.
318 */
319 function gutenberg_custom_css_force_filtered_html_on_import_filter( $arg ) {
320 if ( $arg ) {
321 gutenberg_custom_css_kses_init_filters();
322 }
323 return $arg;
324 }
325
326 add_action( 'init', 'gutenberg_custom_css_kses_init', 20 );
327 add_action( 'set_current_user', 'gutenberg_custom_css_kses_init' );
328 add_filter( 'force_filtered_html_on_import', 'gutenberg_custom_css_force_filtered_html_on_import_filter', 999 );
329
330 // Register the block support.
331 WP_Block_Supports::get_instance()->register(
332 'custom-css',
333 array(
334 'register_attribute' => 'gutenberg_register_custom_css_support',
335 )
336 );
337