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

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