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

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