68 * ) |
72 * ) |
69 * ``` |
73 * ``` |
70 * |
74 * |
71 * This contains the necessary metadata to process them: |
75 * This contains the necessary metadata to process them: |
72 * |
76 * |
73 * - path => where to find the preset within the settings section |
77 * - path => Where to find the preset within the settings section. |
74 * |
78 * - prevent_override => Disables override of default presets by theme presets. |
75 * - value_key => the key that represents the value |
79 * The relationship between whether to override the defaults |
76 * |
80 * and whether the defaults are enabled is inverse: |
77 * - css_var_infix => infix to use in generating the CSS Custom Property. Example: |
81 * - If defaults are enabled => theme presets should not be overriden |
78 * --wp--preset--<preset_infix>--<slug>: <preset_value> |
82 * - If defaults are disabled => theme presets should be overriden |
79 * |
83 * For example, a theme sets defaultPalette to false, |
80 * - classes => array containing a structure with the classes to |
84 * making the default palette hidden from the user. |
81 * generate for the presets. Each class should have |
85 * In that case, we want all the theme presets to be present, |
82 * the class suffix and the property name. Example: |
86 * so they should override the defaults by setting this false. |
83 * |
87 * - use_default_names => whether to use the default names |
84 * .has-<slug>-<class_suffix> { |
88 * - value_key => the key that represents the value |
85 * <property_name>: <preset_value> |
89 * - value_func => optionally, instead of value_key, a function to generate |
86 * } |
90 * the value that takes a preset as an argument |
87 * |
91 * (either value_key or value_func should be present) |
88 * @since 5.8.0 |
92 * - css_vars => template string to use in generating the CSS Custom Property. |
|
93 * Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined |
|
94 * substituting the $slug for the slug's value for each preset value. |
|
95 * - classes => array containing a structure with the classes to |
|
96 * generate for the presets, where for each array item |
|
97 * the key is the class name and the value the property name. |
|
98 * The "$slug" substring will be replaced by the slug of each preset. |
|
99 * For example: |
|
100 * 'classes' => array( |
|
101 * '.has-$slug-color' => 'color', |
|
102 * '.has-$slug-background-color' => 'background-color', |
|
103 * '.has-$slug-border-color' => 'border-color', |
|
104 * ) |
|
105 * - properties => array of CSS properties to be used by kses to |
|
106 * validate the content of each preset |
|
107 * by means of the remove_insecure_properties method. |
|
108 * |
|
109 * @since 5.8.0 |
|
110 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets, |
|
111 * `use_default_names` preset key, and simplified the metadata structure. |
|
112 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the |
|
113 * `prevent_overried` value for `color.duotone` to use `color.defaultDuotone`. |
89 * @var array |
114 * @var array |
90 */ |
115 */ |
91 const PRESETS_METADATA = array( |
116 const PRESETS_METADATA = array( |
92 array( |
117 array( |
93 'path' => array( 'color', 'palette' ), |
118 'path' => array( 'color', 'palette' ), |
94 'value_key' => 'color', |
119 'prevent_override' => array( 'color', 'defaultPalette' ), |
95 'css_var_infix' => 'color', |
120 'use_default_names' => false, |
96 'classes' => array( |
121 'value_key' => 'color', |
97 array( |
122 'css_vars' => '--wp--preset--color--$slug', |
98 'class_suffix' => 'color', |
123 'classes' => array( |
99 'property_name' => 'color', |
124 '.has-$slug-color' => 'color', |
100 ), |
125 '.has-$slug-background-color' => 'background-color', |
101 array( |
126 '.has-$slug-border-color' => 'border-color', |
102 'class_suffix' => 'background-color', |
|
103 'property_name' => 'background-color', |
|
104 ), |
|
105 ), |
127 ), |
|
128 'properties' => array( 'color', 'background-color', 'border-color' ), |
106 ), |
129 ), |
107 array( |
130 array( |
108 'path' => array( 'color', 'gradients' ), |
131 'path' => array( 'color', 'gradients' ), |
109 'value_key' => 'gradient', |
132 'prevent_override' => array( 'color', 'defaultGradients' ), |
110 'css_var_infix' => 'gradient', |
133 'use_default_names' => false, |
111 'classes' => array( |
134 'value_key' => 'gradient', |
112 array( |
135 'css_vars' => '--wp--preset--gradient--$slug', |
113 'class_suffix' => 'gradient-background', |
136 'classes' => array( '.has-$slug-gradient-background' => 'background' ), |
114 'property_name' => 'background', |
137 'properties' => array( 'background' ), |
115 ), |
|
116 ), |
|
117 ), |
138 ), |
118 array( |
139 array( |
119 'path' => array( 'typography', 'fontSizes' ), |
140 'path' => array( 'color', 'duotone' ), |
120 'value_key' => 'size', |
141 'prevent_override' => array( 'color', 'defaultDuotone' ), |
121 'css_var_infix' => 'font-size', |
142 'use_default_names' => false, |
122 'classes' => array( |
143 'value_func' => 'wp_get_duotone_filter_property', |
123 array( |
144 'css_vars' => '--wp--preset--duotone--$slug', |
124 'class_suffix' => 'font-size', |
145 'classes' => array(), |
125 'property_name' => 'font-size', |
146 'properties' => array( 'filter' ), |
126 ), |
147 ), |
127 ), |
148 array( |
|
149 'path' => array( 'typography', 'fontSizes' ), |
|
150 'prevent_override' => false, |
|
151 'use_default_names' => true, |
|
152 'value_key' => 'size', |
|
153 'css_vars' => '--wp--preset--font-size--$slug', |
|
154 'classes' => array( '.has-$slug-font-size' => 'font-size' ), |
|
155 'properties' => array( 'font-size' ), |
|
156 ), |
|
157 array( |
|
158 'path' => array( 'typography', 'fontFamilies' ), |
|
159 'prevent_override' => false, |
|
160 'use_default_names' => false, |
|
161 'value_key' => 'fontFamily', |
|
162 'css_vars' => '--wp--preset--font-family--$slug', |
|
163 'classes' => array( '.has-$slug-font-family' => 'font-family' ), |
|
164 'properties' => array( 'font-family' ), |
128 ), |
165 ), |
129 ); |
166 ); |
130 |
167 |
131 /** |
168 /** |
132 * Metadata for style properties. |
169 * Metadata for style properties. |
133 * |
170 * |
134 * Each property declares: |
171 * Each element is a direct mapping from the CSS property name to the |
135 * |
172 * path to the value in theme.json & block attributes. |
136 * - 'value': path to the value in theme.json and block attributes. |
173 * |
137 * |
174 * @since 5.8.0 |
138 * @since 5.8.0 |
175 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`, |
|
176 * `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`, |
|
177 * `text-decoration`, `text-transform`, and `filter` properties, |
|
178 * simplified the metadata structure. |
139 * @var array |
179 * @var array |
140 */ |
180 */ |
141 const PROPERTIES_METADATA = array( |
181 const PROPERTIES_METADATA = array( |
142 'background' => array( |
182 'background' => array( 'color', 'gradient' ), |
143 'value' => array( 'color', 'gradient' ), |
183 'background-color' => array( 'color', 'background' ), |
144 ), |
184 'border-radius' => array( 'border', 'radius' ), |
145 'background-color' => array( |
185 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), |
146 'value' => array( 'color', 'background' ), |
186 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), |
147 ), |
187 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), |
148 'color' => array( |
188 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), |
149 'value' => array( 'color', 'text' ), |
189 'border-color' => array( 'border', 'color' ), |
150 ), |
190 'border-width' => array( 'border', 'width' ), |
151 'font-size' => array( |
191 'border-style' => array( 'border', 'style' ), |
152 'value' => array( 'typography', 'fontSize' ), |
192 'color' => array( 'color', 'text' ), |
153 ), |
193 'font-family' => array( 'typography', 'fontFamily' ), |
154 'line-height' => array( |
194 'font-size' => array( 'typography', 'fontSize' ), |
155 'value' => array( 'typography', 'lineHeight' ), |
195 'font-style' => array( 'typography', 'fontStyle' ), |
156 ), |
196 'font-weight' => array( 'typography', 'fontWeight' ), |
157 'margin' => array( |
197 'letter-spacing' => array( 'typography', 'letterSpacing' ), |
158 'value' => array( 'spacing', 'margin' ), |
198 'line-height' => array( 'typography', 'lineHeight' ), |
159 'properties' => array( 'top', 'right', 'bottom', 'left' ), |
199 'margin' => array( 'spacing', 'margin' ), |
160 ), |
200 'margin-top' => array( 'spacing', 'margin', 'top' ), |
161 'padding' => array( |
201 'margin-right' => array( 'spacing', 'margin', 'right' ), |
162 'value' => array( 'spacing', 'padding' ), |
202 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), |
163 'properties' => array( 'top', 'right', 'bottom', 'left' ), |
203 'margin-left' => array( 'spacing', 'margin', 'left' ), |
164 ), |
204 'padding' => array( 'spacing', 'padding' ), |
|
205 'padding-top' => array( 'spacing', 'padding', 'top' ), |
|
206 'padding-right' => array( 'spacing', 'padding', 'right' ), |
|
207 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), |
|
208 'padding-left' => array( 'spacing', 'padding', 'left' ), |
|
209 '--wp--style--block-gap' => array( 'spacing', 'blockGap' ), |
|
210 'text-decoration' => array( 'typography', 'textDecoration' ), |
|
211 'text-transform' => array( 'typography', 'textTransform' ), |
|
212 'filter' => array( 'filter', 'duotone' ), |
165 ); |
213 ); |
166 |
214 |
167 /** |
215 /** |
168 * @since 5.8.0 |
216 * Protected style properties. |
169 * @var array |
217 * |
170 */ |
218 * These style properties are only rendered if a setting enables it |
171 const ALLOWED_TOP_LEVEL_KEYS = array( |
219 * via a value other than `null`. |
|
220 * |
|
221 * Each element maps the style property to the corresponding theme.json |
|
222 * setting key. |
|
223 * |
|
224 * @since 5.9.0 |
|
225 */ |
|
226 const PROTECTED_PROPERTIES = array( |
|
227 'spacing.blockGap' => array( 'spacing', 'blockGap' ), |
|
228 ); |
|
229 |
|
230 /** |
|
231 * The top-level keys a theme.json can have. |
|
232 * |
|
233 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`. |
|
234 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`, |
|
235 * added the `customTemplates` and `templateParts` values. |
|
236 * @var string[] |
|
237 */ |
|
238 const VALID_TOP_LEVEL_KEYS = array( |
|
239 'customTemplates', |
|
240 'patterns', |
172 'settings', |
241 'settings', |
173 'styles', |
242 'styles', |
|
243 'templateParts', |
174 'version', |
244 'version', |
|
245 'title', |
175 ); |
246 ); |
176 |
247 |
177 /** |
248 /** |
178 * @since 5.8.0 |
249 * The valid properties under the settings key. |
|
250 * |
|
251 * @since 5.8.0 As `ALLOWED_SETTINGS`. |
|
252 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`, |
|
253 * added new properties for `border`, `color`, `spacing`, |
|
254 * and `typography`, and renamed others according to the new schema. |
|
255 * @since 6.0.0 Added `color.defaultDuotone`. |
179 * @var array |
256 * @var array |
180 */ |
257 */ |
181 const ALLOWED_SETTINGS = array( |
258 const VALID_SETTINGS = array( |
182 'border' => array( |
259 'appearanceTools' => null, |
183 'customRadius' => null, |
260 'border' => array( |
|
261 'color' => null, |
|
262 'radius' => null, |
|
263 'style' => null, |
|
264 'width' => null, |
184 ), |
265 ), |
185 'color' => array( |
266 'color' => array( |
186 'custom' => null, |
267 'background' => null, |
187 'customDuotone' => null, |
268 'custom' => null, |
188 'customGradient' => null, |
269 'customDuotone' => null, |
189 'duotone' => null, |
270 'customGradient' => null, |
190 'gradients' => null, |
271 'defaultDuotone' => null, |
191 'link' => null, |
272 'defaultGradients' => null, |
192 'palette' => null, |
273 'defaultPalette' => null, |
|
274 'duotone' => null, |
|
275 'gradients' => null, |
|
276 'link' => null, |
|
277 'palette' => null, |
|
278 'text' => null, |
193 ), |
279 ), |
194 'custom' => null, |
280 'custom' => null, |
195 'layout' => array( |
281 'layout' => array( |
196 'contentSize' => null, |
282 'contentSize' => null, |
197 'wideSize' => null, |
283 'wideSize' => null, |
198 ), |
284 ), |
199 'spacing' => array( |
285 'spacing' => array( |
200 'customMargin' => null, |
286 'blockGap' => null, |
201 'customPadding' => null, |
287 'margin' => null, |
202 'units' => null, |
288 'padding' => null, |
|
289 'units' => null, |
203 ), |
290 ), |
204 'typography' => array( |
291 'typography' => array( |
205 'customFontSize' => null, |
292 'customFontSize' => null, |
206 'customLineHeight' => null, |
293 'dropCap' => null, |
207 'dropCap' => null, |
294 'fontFamilies' => null, |
208 'fontSizes' => null, |
295 'fontSizes' => null, |
|
296 'fontStyle' => null, |
|
297 'fontWeight' => null, |
|
298 'letterSpacing' => null, |
|
299 'lineHeight' => null, |
|
300 'textDecoration' => null, |
|
301 'textTransform' => null, |
209 ), |
302 ), |
210 ); |
303 ); |
211 |
304 |
212 /** |
305 /** |
213 * @since 5.8.0 |
306 * The valid properties under the styles key. |
|
307 * |
|
308 * @since 5.8.0 As `ALLOWED_STYLES`. |
|
309 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`, |
|
310 * added new properties for `border`, `filter`, `spacing`, |
|
311 * and `typography`. |
214 * @var array |
312 * @var array |
215 */ |
313 */ |
216 const ALLOWED_STYLES = array( |
314 const VALID_STYLES = array( |
217 'border' => array( |
315 'border' => array( |
|
316 'color' => null, |
218 'radius' => null, |
317 'radius' => null, |
|
318 'style' => null, |
|
319 'width' => null, |
219 ), |
320 ), |
220 'color' => array( |
321 'color' => array( |
221 'background' => null, |
322 'background' => null, |
222 'gradient' => null, |
323 'gradient' => null, |
223 'text' => null, |
324 'text' => null, |
224 ), |
325 ), |
|
326 'filter' => array( |
|
327 'duotone' => null, |
|
328 ), |
225 'spacing' => array( |
329 'spacing' => array( |
226 'margin' => array( |
330 'margin' => null, |
227 'top' => null, |
331 'padding' => null, |
228 'right' => null, |
332 'blockGap' => 'top', |
229 'bottom' => null, |
|
230 'left' => null, |
|
231 ), |
|
232 'padding' => array( |
|
233 'bottom' => null, |
|
234 'left' => null, |
|
235 'right' => null, |
|
236 'top' => null, |
|
237 ), |
|
238 ), |
333 ), |
239 'typography' => array( |
334 'typography' => array( |
240 'fontSize' => null, |
335 'fontFamily' => null, |
241 'lineHeight' => null, |
336 'fontSize' => null, |
|
337 'fontStyle' => null, |
|
338 'fontWeight' => null, |
|
339 'letterSpacing' => null, |
|
340 'lineHeight' => null, |
|
341 'textDecoration' => null, |
|
342 'textTransform' => null, |
242 ), |
343 ), |
243 ); |
344 ); |
244 |
345 |
245 /** |
346 /** |
246 * @since 5.8.0 |
347 * The valid elements that can be found under styles. |
247 * @var array |
348 * |
|
349 * @since 5.8.0 |
|
350 * @var string[] |
248 */ |
351 */ |
249 const ELEMENTS = array( |
352 const ELEMENTS = array( |
250 'link' => 'a', |
353 'link' => 'a', |
251 'h1' => 'h1', |
354 'h1' => 'h1', |
252 'h2' => 'h2', |
355 'h2' => 'h2', |
255 'h5' => 'h5', |
358 'h5' => 'h5', |
256 'h6' => 'h6', |
359 'h6' => 'h6', |
257 ); |
360 ); |
258 |
361 |
259 /** |
362 /** |
260 * @since 5.8.0 |
363 * Options that settings.appearanceTools enables. |
|
364 * |
|
365 * @since 6.0.0 |
|
366 * @var array |
|
367 */ |
|
368 const APPEARANCE_TOOLS_OPT_INS = array( |
|
369 array( 'border', 'color' ), |
|
370 array( 'border', 'radius' ), |
|
371 array( 'border', 'style' ), |
|
372 array( 'border', 'width' ), |
|
373 array( 'color', 'link' ), |
|
374 array( 'spacing', 'blockGap' ), |
|
375 array( 'spacing', 'margin' ), |
|
376 array( 'spacing', 'padding' ), |
|
377 array( 'typography', 'lineHeight' ), |
|
378 ); |
|
379 |
|
380 /** |
|
381 * The latest version of the schema in use. |
|
382 * |
|
383 * @since 5.8.0 |
|
384 * @since 5.9.0 Changed value from 1 to 2. |
261 * @var int |
385 * @var int |
262 */ |
386 */ |
263 const LATEST_SCHEMA = 1; |
387 const LATEST_SCHEMA = 2; |
264 |
388 |
265 /** |
389 /** |
266 * Constructor. |
390 * Constructor. |
267 * |
391 * |
268 * @since 5.8.0 |
392 * @since 5.8.0 |
269 * |
393 * |
270 * @param array $theme_json A structure that follows the theme.json schema. |
394 * @param array $theme_json A structure that follows the theme.json schema. |
271 * @param string $origin Optional. What source of data this object represents. |
395 * @param string $origin Optional. What source of data this object represents. |
272 * One of 'core', 'theme', or 'user'. Default 'theme'. |
396 * One of 'default', 'theme', or 'custom'. Default 'theme'. |
273 */ |
397 */ |
274 public function __construct( $theme_json = array(), $origin = 'theme' ) { |
398 public function __construct( $theme_json = array(), $origin = 'theme' ) { |
275 if ( ! in_array( $origin, self::VALID_ORIGINS, true ) ) { |
399 if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { |
276 $origin = 'theme'; |
400 $origin = 'theme'; |
277 } |
401 } |
278 |
402 |
279 if ( ! isset( $theme_json['version'] ) || self::LATEST_SCHEMA !== $theme_json['version'] ) { |
403 $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); |
280 $this->theme_json = array(); |
404 $valid_block_names = array_keys( static::get_blocks_metadata() ); |
281 return; |
405 $valid_element_names = array_keys( static::ELEMENTS ); |
282 } |
406 $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names ); |
283 |
407 $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); |
284 $this->theme_json = self::sanitize( $theme_json ); |
|
285 |
408 |
286 // Internally, presets are keyed by origin. |
409 // Internally, presets are keyed by origin. |
287 $nodes = self::get_setting_nodes( $this->theme_json ); |
410 $nodes = static::get_setting_nodes( $this->theme_json ); |
288 foreach ( $nodes as $node ) { |
411 foreach ( $nodes as $node ) { |
289 foreach ( self::PRESETS_METADATA as $preset ) { |
412 foreach ( static::PRESETS_METADATA as $preset_metadata ) { |
290 $path = array_merge( $node['path'], $preset['path'] ); |
413 $path = array_merge( $node['path'], $preset_metadata['path'] ); |
291 $preset = _wp_array_get( $this->theme_json, $path, null ); |
414 $preset = _wp_array_get( $this->theme_json, $path, null ); |
292 if ( null !== $preset ) { |
415 if ( null !== $preset ) { |
293 _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); |
416 // If the preset is not already keyed by origin. |
294 } |
417 if ( isset( $preset[0] ) || empty( $preset ) ) { |
295 } |
418 _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); |
296 } |
419 } |
|
420 } |
|
421 } |
|
422 } |
|
423 } |
|
424 |
|
425 /** |
|
426 * Enables some opt-in settings if theme declared support. |
|
427 * |
|
428 * @since 5.9.0 |
|
429 * |
|
430 * @param array $theme_json A theme.json structure to modify. |
|
431 * @return array The modified theme.json structure. |
|
432 */ |
|
433 protected static function maybe_opt_in_into_settings( $theme_json ) { |
|
434 $new_theme_json = $theme_json; |
|
435 |
|
436 if ( |
|
437 isset( $new_theme_json['settings']['appearanceTools'] ) && |
|
438 true === $new_theme_json['settings']['appearanceTools'] |
|
439 ) { |
|
440 static::do_opt_in_into_settings( $new_theme_json['settings'] ); |
|
441 } |
|
442 |
|
443 if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) { |
|
444 foreach ( $new_theme_json['settings']['blocks'] as &$block ) { |
|
445 if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) { |
|
446 static::do_opt_in_into_settings( $block ); |
|
447 } |
|
448 } |
|
449 } |
|
450 |
|
451 return $new_theme_json; |
|
452 } |
|
453 |
|
454 /** |
|
455 * Enables some settings. |
|
456 * |
|
457 * @since 5.9.0 |
|
458 * |
|
459 * @param array $context The context to which the settings belong. |
|
460 */ |
|
461 protected static function do_opt_in_into_settings( &$context ) { |
|
462 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) { |
|
463 // Use "unset prop" as a marker instead of "null" because |
|
464 // "null" can be a valid value for some props (e.g. blockGap). |
|
465 if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) { |
|
466 _wp_array_set( $context, $path, true ); |
|
467 } |
|
468 } |
|
469 |
|
470 unset( $context['appearanceTools'] ); |
297 } |
471 } |
298 |
472 |
299 /** |
473 /** |
300 * Sanitizes the input according to the schemas. |
474 * Sanitizes the input according to the schemas. |
301 * |
475 * |
302 * @since 5.8.0 |
476 * @since 5.8.0 |
303 * |
477 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. |
304 * @param array $input Structure to sanitize. |
478 * |
|
479 * @param array $input Structure to sanitize. |
|
480 * @param array $valid_block_names List of valid block names. |
|
481 * @param array $valid_element_names List of valid element names. |
305 * @return array The sanitized output. |
482 * @return array The sanitized output. |
306 */ |
483 */ |
307 private static function sanitize( $input ) { |
484 protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { |
308 $output = array(); |
485 $output = array(); |
309 |
486 |
310 if ( ! is_array( $input ) ) { |
487 if ( ! is_array( $input ) ) { |
311 return $output; |
488 return $output; |
312 } |
489 } |
313 |
490 |
314 $allowed_top_level_keys = self::ALLOWED_TOP_LEVEL_KEYS; |
491 $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); |
315 $allowed_settings = self::ALLOWED_SETTINGS; |
492 |
316 $allowed_styles = self::ALLOWED_STYLES; |
493 // Some styles are only meant to be available at the top-level (e.g.: blockGap), |
317 $allowed_blocks = array_keys( self::get_blocks_metadata() ); |
494 // hence, the schema for blocks & elements should not have them. |
318 $allowed_elements = array_keys( self::ELEMENTS ); |
495 $styles_non_top_level = static::VALID_STYLES; |
319 |
496 foreach ( array_keys( $styles_non_top_level ) as $section ) { |
320 $output = array_intersect_key( $input, array_flip( $allowed_top_level_keys ) ); |
497 foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { |
321 |
498 if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { |
322 // Build the schema. |
499 unset( $styles_non_top_level[ $section ][ $prop ] ); |
|
500 } |
|
501 } |
|
502 } |
|
503 |
|
504 // Build the schema based on valid block & element names. |
323 $schema = array(); |
505 $schema = array(); |
324 $schema_styles_elements = array(); |
506 $schema_styles_elements = array(); |
325 foreach ( $allowed_elements as $element ) { |
507 foreach ( $valid_element_names as $element ) { |
326 $schema_styles_elements[ $element ] = $allowed_styles; |
508 $schema_styles_elements[ $element ] = $styles_non_top_level; |
327 } |
509 } |
328 $schema_styles_blocks = array(); |
510 $schema_styles_blocks = array(); |
329 $schema_settings_blocks = array(); |
511 $schema_settings_blocks = array(); |
330 foreach ( $allowed_blocks as $block ) { |
512 foreach ( $valid_block_names as $block ) { |
331 $schema_settings_blocks[ $block ] = $allowed_settings; |
513 $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; |
332 $schema_styles_blocks[ $block ] = $allowed_styles; |
514 $schema_styles_blocks[ $block ] = $styles_non_top_level; |
333 $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; |
515 $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; |
334 } |
516 } |
335 $schema['styles'] = $allowed_styles; |
517 $schema['styles'] = static::VALID_STYLES; |
336 $schema['styles']['blocks'] = $schema_styles_blocks; |
518 $schema['styles']['blocks'] = $schema_styles_blocks; |
337 $schema['styles']['elements'] = $schema_styles_elements; |
519 $schema['styles']['elements'] = $schema_styles_elements; |
338 $schema['settings'] = $allowed_settings; |
520 $schema['settings'] = static::VALID_SETTINGS; |
339 $schema['settings']['blocks'] = $schema_settings_blocks; |
521 $schema['settings']['blocks'] = $schema_settings_blocks; |
340 |
522 |
341 // Remove anything that's not present in the schema. |
523 // Remove anything that's not present in the schema. |
342 foreach ( array( 'styles', 'settings' ) as $subtree ) { |
524 foreach ( array( 'styles', 'settings' ) as $subtree ) { |
343 if ( ! isset( $input[ $subtree ] ) ) { |
525 if ( ! isset( $input[ $subtree ] ) ) { |
375 * } |
557 * } |
376 * }, |
558 * }, |
377 * 'core/heading': { |
559 * 'core/heading': { |
378 * 'selector': 'h1', |
560 * 'selector': 'h1', |
379 * 'elements': {} |
561 * 'elements': {} |
380 * } |
562 * }, |
381 * 'core/group': { |
563 * 'core/image': { |
382 * 'selector': '.wp-block-group', |
564 * 'selector': '.wp-block-image', |
|
565 * 'duotone': 'img', |
383 * 'elements': {} |
566 * 'elements': {} |
384 * } |
567 * } |
385 * } |
568 * } |
386 * |
569 * |
387 * @since 5.8.0 |
570 * @since 5.8.0 |
|
571 * @since 5.9.0 Added `duotone` key with CSS selector. |
388 * |
572 * |
389 * @return array Block metadata. |
573 * @return array Block metadata. |
390 */ |
574 */ |
391 private static function get_blocks_metadata() { |
575 protected static function get_blocks_metadata() { |
392 if ( null !== self::$blocks_metadata ) { |
576 if ( null !== static::$blocks_metadata ) { |
393 return self::$blocks_metadata; |
577 return static::$blocks_metadata; |
394 } |
578 } |
395 |
579 |
396 self::$blocks_metadata = array(); |
580 static::$blocks_metadata = array(); |
397 |
581 |
398 $registry = WP_Block_Type_Registry::get_instance(); |
582 $registry = WP_Block_Type_Registry::get_instance(); |
399 $blocks = $registry->get_all_registered(); |
583 $blocks = $registry->get_all_registered(); |
400 foreach ( $blocks as $block_name => $block_type ) { |
584 foreach ( $blocks as $block_name => $block_type ) { |
401 if ( |
585 if ( |
402 isset( $block_type->supports['__experimentalSelector'] ) && |
586 isset( $block_type->supports['__experimentalSelector'] ) && |
403 is_string( $block_type->supports['__experimentalSelector'] ) |
587 is_string( $block_type->supports['__experimentalSelector'] ) |
404 ) { |
588 ) { |
405 self::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector']; |
589 static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector']; |
406 } else { |
590 } else { |
407 self::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ); |
591 static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ); |
408 } |
592 } |
409 |
593 |
410 /* |
594 if ( |
411 * Assign defaults, then overwrite those that the block sets by itself. |
595 isset( $block_type->supports['color']['__experimentalDuotone'] ) && |
412 * If the block selector is compounded, will append the element to each |
596 is_string( $block_type->supports['color']['__experimentalDuotone'] ) |
413 * individual block selector. |
597 ) { |
414 */ |
598 static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone']; |
415 $block_selectors = explode( ',', self::$blocks_metadata[ $block_name ]['selector'] ); |
599 } |
416 foreach ( self::ELEMENTS as $el_name => $el_selector ) { |
600 |
|
601 // Assign defaults, then overwrite those that the block sets by itself. |
|
602 // If the block selector is compounded, will append the element to each |
|
603 // individual block selector. |
|
604 $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] ); |
|
605 foreach ( static::ELEMENTS as $el_name => $el_selector ) { |
417 $element_selector = array(); |
606 $element_selector = array(); |
418 foreach ( $block_selectors as $selector ) { |
607 foreach ( $block_selectors as $selector ) { |
419 $element_selector[] = $selector . ' ' . $el_selector; |
608 $element_selector[] = $selector . ' ' . $el_selector; |
420 } |
609 } |
421 self::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); |
610 static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); |
422 } |
611 } |
423 } |
612 } |
424 |
613 |
425 return self::$blocks_metadata; |
614 return static::$blocks_metadata; |
426 } |
615 } |
427 |
616 |
428 /** |
617 /** |
429 * Given a tree, removes the keys that are not present in the schema. |
618 * Given a tree, removes the keys that are not present in the schema. |
430 * |
619 * |
491 /** |
680 /** |
492 * Returns the stylesheet that results of processing |
681 * Returns the stylesheet that results of processing |
493 * the theme.json structure this object represents. |
682 * the theme.json structure this object represents. |
494 * |
683 * |
495 * @since 5.8.0 |
684 * @since 5.8.0 |
496 * |
685 * @since 5.9.0 Removed the `$type` parameter`, added the `$types` and `$origins` parameters. |
497 * @param string $type Optional. Type of stylesheet we want. Accepts 'all', |
686 * |
498 * 'block_styles', and 'css_variables'. Default 'all'. |
687 * @param array $types Types of styles to load. Will load all by default. It accepts: |
|
688 * - `variables`: only the CSS Custom Properties for presets & custom ones. |
|
689 * - `styles`: only the styles section in theme.json. |
|
690 * - `presets`: only the classes for the presets. |
|
691 * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS. |
499 * @return string Stylesheet. |
692 * @return string Stylesheet. |
500 */ |
693 */ |
501 public function get_stylesheet( $type = 'all' ) { |
694 public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) { |
502 $blocks_metadata = self::get_blocks_metadata(); |
695 if ( null === $origins ) { |
503 $style_nodes = self::get_style_nodes( $this->theme_json, $blocks_metadata ); |
696 $origins = static::VALID_ORIGINS; |
504 $setting_nodes = self::get_setting_nodes( $this->theme_json, $blocks_metadata ); |
697 } |
505 |
698 |
506 switch ( $type ) { |
699 if ( is_string( $types ) ) { |
507 case 'block_styles': |
700 // Dispatch error and map old arguments to new ones. |
508 return $this->get_block_styles( $style_nodes, $setting_nodes ); |
701 _deprecated_argument( __FUNCTION__, '5.9.0' ); |
509 case 'css_variables': |
702 if ( 'block_styles' === $types ) { |
510 return $this->get_css_variables( $setting_nodes ); |
703 $types = array( 'styles', 'presets' ); |
511 default: |
704 } elseif ( 'css_variables' === $types ) { |
512 return $this->get_css_variables( $setting_nodes ) . $this->get_block_styles( $style_nodes, $setting_nodes ); |
705 $types = array( 'variables' ); |
513 } |
706 } else { |
514 |
707 $types = array( 'variables', 'styles', 'presets' ); |
|
708 } |
|
709 } |
|
710 |
|
711 $blocks_metadata = static::get_blocks_metadata(); |
|
712 $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata ); |
|
713 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); |
|
714 |
|
715 $stylesheet = ''; |
|
716 |
|
717 if ( in_array( 'variables', $types, true ) ) { |
|
718 $stylesheet .= $this->get_css_variables( $setting_nodes, $origins ); |
|
719 } |
|
720 |
|
721 if ( in_array( 'styles', $types, true ) ) { |
|
722 $stylesheet .= $this->get_block_classes( $style_nodes ); |
|
723 } |
|
724 |
|
725 if ( in_array( 'presets', $types, true ) ) { |
|
726 $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); |
|
727 } |
|
728 |
|
729 return $stylesheet; |
|
730 } |
|
731 |
|
732 /** |
|
733 * Returns the page templates of the active theme. |
|
734 * |
|
735 * @since 5.9.0 |
|
736 * |
|
737 * @return array |
|
738 */ |
|
739 public function get_custom_templates() { |
|
740 $custom_templates = array(); |
|
741 if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) { |
|
742 return $custom_templates; |
|
743 } |
|
744 |
|
745 foreach ( $this->theme_json['customTemplates'] as $item ) { |
|
746 if ( isset( $item['name'] ) ) { |
|
747 $custom_templates[ $item['name'] ] = array( |
|
748 'title' => isset( $item['title'] ) ? $item['title'] : '', |
|
749 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ), |
|
750 ); |
|
751 } |
|
752 } |
|
753 return $custom_templates; |
|
754 } |
|
755 |
|
756 /** |
|
757 * Returns the template part data of active theme. |
|
758 * |
|
759 * @since 5.9.0 |
|
760 * |
|
761 * @return array |
|
762 */ |
|
763 public function get_template_parts() { |
|
764 $template_parts = array(); |
|
765 if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) { |
|
766 return $template_parts; |
|
767 } |
|
768 |
|
769 foreach ( $this->theme_json['templateParts'] as $item ) { |
|
770 if ( isset( $item['name'] ) ) { |
|
771 $template_parts[ $item['name'] ] = array( |
|
772 'title' => isset( $item['title'] ) ? $item['title'] : '', |
|
773 'area' => isset( $item['area'] ) ? $item['area'] : '', |
|
774 ); |
|
775 } |
|
776 } |
|
777 return $template_parts; |
515 } |
778 } |
516 |
779 |
517 /** |
780 /** |
518 * Converts each style section into a list of rulesets |
781 * Converts each style section into a list of rulesets |
519 * containing the block styles to be appended to the stylesheet. |
782 * containing the block styles to be appended to the stylesheet. |
524 * |
787 * |
525 * block-selector { |
788 * block-selector { |
526 * style-property-one: value; |
789 * style-property-one: value; |
527 * } |
790 * } |
528 * |
791 * |
529 * Additionally, it'll also create new rulesets |
792 * @since 5.8.0 As `get_block_styles()`. |
530 * as classes for each preset value such as: |
793 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()` |
531 * |
794 * and no longer returns preset classes. |
532 * .has-value-color { |
795 * Removed the `$setting_nodes` parameter. |
533 * color: value; |
796 * |
534 * } |
797 * @param array $style_nodes Nodes with styles. |
535 * |
|
536 * .has-value-background-color { |
|
537 * background-color: value; |
|
538 * } |
|
539 * |
|
540 * .has-value-font-size { |
|
541 * font-size: value; |
|
542 * } |
|
543 * |
|
544 * .has-value-gradient-background { |
|
545 * background: value; |
|
546 * } |
|
547 * |
|
548 * p.has-value-gradient-background { |
|
549 * background: value; |
|
550 * } |
|
551 * |
|
552 * @since 5.8.0 |
|
553 * |
|
554 * @param array $style_nodes Nodes with styles. |
|
555 * @param array $setting_nodes Nodes with settings. |
|
556 * @return string The new stylesheet. |
798 * @return string The new stylesheet. |
557 */ |
799 */ |
558 private function get_block_styles( $style_nodes, $setting_nodes ) { |
800 protected function get_block_classes( $style_nodes ) { |
559 $block_rules = ''; |
801 $block_rules = ''; |
|
802 |
560 foreach ( $style_nodes as $metadata ) { |
803 foreach ( $style_nodes as $metadata ) { |
561 if ( null === $metadata['selector'] ) { |
804 if ( null === $metadata['selector'] ) { |
562 continue; |
805 continue; |
563 } |
806 } |
564 |
807 |
565 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); |
808 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); |
566 $selector = $metadata['selector']; |
809 $selector = $metadata['selector']; |
567 $declarations = self::compute_style_properties( $node ); |
810 $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); |
568 $block_rules .= self::to_ruleset( $selector, $declarations ); |
811 $declarations = static::compute_style_properties( $node, $settings ); |
569 } |
812 |
570 |
813 // 1. Separate the ones who use the general selector |
|
814 // and the ones who use the duotone selector. |
|
815 $declarations_duotone = array(); |
|
816 foreach ( $declarations as $index => $declaration ) { |
|
817 if ( 'filter' === $declaration['name'] ) { |
|
818 unset( $declarations[ $index ] ); |
|
819 $declarations_duotone[] = $declaration; |
|
820 } |
|
821 } |
|
822 |
|
823 /* |
|
824 * Reset default browser margin on the root body element. |
|
825 * This is set on the root selector **before** generating the ruleset |
|
826 * from the `theme.json`. This is to ensure that if the `theme.json` declares |
|
827 * `margin` in its `spacing` declaration for the `body` element then these |
|
828 * user-generated values take precedence in the CSS cascade. |
|
829 * @link https://github.com/WordPress/gutenberg/issues/36147. |
|
830 */ |
|
831 if ( static::ROOT_BLOCK_SELECTOR === $selector ) { |
|
832 $block_rules .= 'body { margin: 0; }'; |
|
833 } |
|
834 |
|
835 // 2. Generate the rules that use the general selector. |
|
836 $block_rules .= static::to_ruleset( $selector, $declarations ); |
|
837 |
|
838 // 3. Generate the rules that use the duotone selector. |
|
839 if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { |
|
840 $selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] ); |
|
841 $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); |
|
842 } |
|
843 |
|
844 if ( static::ROOT_BLOCK_SELECTOR === $selector ) { |
|
845 $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; |
|
846 $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; |
|
847 $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; |
|
848 |
|
849 $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; |
|
850 if ( $has_block_gap_support ) { |
|
851 $block_rules .= '.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }'; |
|
852 $block_rules .= '.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }'; |
|
853 } |
|
854 } |
|
855 } |
|
856 |
|
857 return $block_rules; |
|
858 } |
|
859 |
|
860 /** |
|
861 * Creates new rulesets as classes for each preset value such as: |
|
862 * |
|
863 * .has-value-color { |
|
864 * color: value; |
|
865 * } |
|
866 * |
|
867 * .has-value-background-color { |
|
868 * background-color: value; |
|
869 * } |
|
870 * |
|
871 * .has-value-font-size { |
|
872 * font-size: value; |
|
873 * } |
|
874 * |
|
875 * .has-value-gradient-background { |
|
876 * background: value; |
|
877 * } |
|
878 * |
|
879 * p.has-value-gradient-background { |
|
880 * background: value; |
|
881 * } |
|
882 * |
|
883 * @since 5.9.0 |
|
884 * |
|
885 * @param array $setting_nodes Nodes with settings. |
|
886 * @param array $origins List of origins to process presets from. |
|
887 * @return string The new stylesheet. |
|
888 */ |
|
889 protected function get_preset_classes( $setting_nodes, $origins ) { |
571 $preset_rules = ''; |
890 $preset_rules = ''; |
|
891 |
572 foreach ( $setting_nodes as $metadata ) { |
892 foreach ( $setting_nodes as $metadata ) { |
573 if ( null === $metadata['selector'] ) { |
893 if ( null === $metadata['selector'] ) { |
574 continue; |
894 continue; |
575 } |
895 } |
576 |
896 |
577 $selector = $metadata['selector']; |
897 $selector = $metadata['selector']; |
578 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); |
898 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); |
579 $preset_rules .= self::compute_preset_classes( $node, $selector ); |
899 $preset_rules .= static::compute_preset_classes( $node, $selector, $origins ); |
580 } |
900 } |
581 |
901 |
582 return $block_rules . $preset_rules; |
902 return $preset_rules; |
583 } |
903 } |
584 |
904 |
585 /** |
905 /** |
586 * Converts each styles section into a list of rulesets |
906 * Converts each styles section into a list of rulesets |
587 * to be appended to the stylesheet. |
907 * to be appended to the stylesheet. |
655 * |
977 * |
656 * @param string $selector Original selector. |
978 * @param string $selector Original selector. |
657 * @param string $to_append Selector to append. |
979 * @param string $to_append Selector to append. |
658 * @return string |
980 * @return string |
659 */ |
981 */ |
660 private static function append_to_selector( $selector, $to_append ) { |
982 protected static function append_to_selector( $selector, $to_append ) { |
661 $new_selectors = array(); |
983 $new_selectors = array(); |
662 $selectors = explode( ',', $selector ); |
984 $selectors = explode( ',', $selector ); |
663 foreach ( $selectors as $sel ) { |
985 foreach ( $selectors as $sel ) { |
664 $new_selectors[] = $sel . $to_append; |
986 $new_selectors[] = $sel . $to_append; |
665 } |
987 } |
666 |
988 |
667 return implode( ',', $new_selectors ); |
989 return implode( ',', $new_selectors ); |
668 } |
990 } |
669 |
991 |
670 /** |
992 /** |
671 * Given an array of presets keyed by origin and the value key of the preset, |
|
672 * it returns an array where each key is the preset slug and each value the preset value. |
|
673 * |
|
674 * @since 5.8.0 |
|
675 * |
|
676 * @param array $preset_per_origin Array of presets keyed by origin. |
|
677 * @param string $value_key The property of the preset that contains its value. |
|
678 * @return array Array of presets where each key is a slug and each value is the preset value. |
|
679 */ |
|
680 private static function get_merged_preset_by_slug( $preset_per_origin, $value_key ) { |
|
681 $result = array(); |
|
682 foreach ( self::VALID_ORIGINS as $origin ) { |
|
683 if ( ! isset( $preset_per_origin[ $origin ] ) ) { |
|
684 continue; |
|
685 } |
|
686 foreach ( $preset_per_origin[ $origin ] as $preset ) { |
|
687 /* |
|
688 * We don't want to use kebabCase here, |
|
689 * see https://github.com/WordPress/gutenberg/issues/32347 |
|
690 * However, we need to make sure the generated class or CSS variable |
|
691 * doesn't contain spaces. |
|
692 */ |
|
693 $result[ preg_replace( '/\s+/', '-', $preset['slug'] ) ] = $preset[ $value_key ]; |
|
694 } |
|
695 } |
|
696 return $result; |
|
697 } |
|
698 |
|
699 /** |
|
700 * Given a settings array, it returns the generated rulesets |
993 * Given a settings array, it returns the generated rulesets |
701 * for the preset classes. |
994 * for the preset classes. |
702 * |
995 * |
703 * @since 5.8.0 |
996 * @since 5.8.0 |
|
997 * @since 5.9.0 Added the `$origins` parameter. |
704 * |
998 * |
705 * @param array $settings Settings to process. |
999 * @param array $settings Settings to process. |
706 * @param string $selector Selector wrapping the classes. |
1000 * @param string $selector Selector wrapping the classes. |
|
1001 * @param array $origins List of origins to process. |
707 * @return string The result of processing the presets. |
1002 * @return string The result of processing the presets. |
708 */ |
1003 */ |
709 private static function compute_preset_classes( $settings, $selector ) { |
1004 protected static function compute_preset_classes( $settings, $selector, $origins ) { |
710 if ( self::ROOT_BLOCK_SELECTOR === $selector ) { |
1005 if ( static::ROOT_BLOCK_SELECTOR === $selector ) { |
711 // Classes at the global level do not need any CSS prefixed, |
1006 // Classes at the global level do not need any CSS prefixed, |
712 // and we don't want to increase its specificity. |
1007 // and we don't want to increase its specificity. |
713 $selector = ''; |
1008 $selector = ''; |
714 } |
1009 } |
715 |
1010 |
716 $stylesheet = ''; |
1011 $stylesheet = ''; |
717 foreach ( self::PRESETS_METADATA as $preset ) { |
1012 foreach ( static::PRESETS_METADATA as $preset_metadata ) { |
718 $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() ); |
1013 $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins ); |
719 $preset_by_slug = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] ); |
1014 foreach ( $preset_metadata['classes'] as $class => $property ) { |
720 foreach ( $preset['classes'] as $class ) { |
1015 foreach ( $slugs as $slug ) { |
721 foreach ( $preset_by_slug as $slug => $value ) { |
1016 $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); |
722 $stylesheet .= self::to_ruleset( |
1017 $class_name = static::replace_slug_in_string( $class, $slug ); |
723 self::append_to_selector( $selector, '.has-' . _wp_to_kebab_case( $slug ) . '-' . $class['class_suffix'] ), |
1018 $stylesheet .= static::to_ruleset( |
|
1019 static::append_to_selector( $selector, $class_name ), |
724 array( |
1020 array( |
725 array( |
1021 array( |
726 'name' => $class['property_name'], |
1022 'name' => $property, |
727 'value' => 'var(--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ) . ') !important', |
1023 'value' => 'var(' . $css_var . ') !important', |
728 ), |
1024 ), |
729 ) |
1025 ) |
730 ); |
1026 ); |
731 } |
1027 } |
732 } |
1028 } |
733 } |
1029 } |
734 |
1030 |
735 return $stylesheet; |
1031 return $stylesheet; |
|
1032 } |
|
1033 |
|
1034 /** |
|
1035 * Function that scopes a selector with another one. This works a bit like |
|
1036 * SCSS nesting except the `&` operator isn't supported. |
|
1037 * |
|
1038 * <code> |
|
1039 * $scope = '.a, .b .c'; |
|
1040 * $selector = '> .x, .y'; |
|
1041 * $merged = scope_selector( $scope, $selector ); |
|
1042 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' |
|
1043 * </code> |
|
1044 * |
|
1045 * @since 5.9.0 |
|
1046 * |
|
1047 * @param string $scope Selector to scope to. |
|
1048 * @param string $selector Original selector. |
|
1049 * @return string Scoped selector. |
|
1050 */ |
|
1051 protected static function scope_selector( $scope, $selector ) { |
|
1052 $scopes = explode( ',', $scope ); |
|
1053 $selectors = explode( ',', $selector ); |
|
1054 |
|
1055 $selectors_scoped = array(); |
|
1056 foreach ( $scopes as $outer ) { |
|
1057 foreach ( $selectors as $inner ) { |
|
1058 $selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner ); |
|
1059 } |
|
1060 } |
|
1061 |
|
1062 return implode( ', ', $selectors_scoped ); |
|
1063 } |
|
1064 |
|
1065 /** |
|
1066 * Gets preset values keyed by slugs based on settings and metadata. |
|
1067 * |
|
1068 * <code> |
|
1069 * $settings = array( |
|
1070 * 'typography' => array( |
|
1071 * 'fontFamilies' => array( |
|
1072 * array( |
|
1073 * 'slug' => 'sansSerif', |
|
1074 * 'fontFamily' => '"Helvetica Neue", sans-serif', |
|
1075 * ), |
|
1076 * array( |
|
1077 * 'slug' => 'serif', |
|
1078 * 'colors' => 'Georgia, serif', |
|
1079 * ) |
|
1080 * ), |
|
1081 * ), |
|
1082 * ); |
|
1083 * $meta = array( |
|
1084 * 'path' => array( 'typography', 'fontFamilies' ), |
|
1085 * 'value_key' => 'fontFamily', |
|
1086 * ); |
|
1087 * $values_by_slug = get_settings_values_by_slug(); |
|
1088 * // $values_by_slug === array( |
|
1089 * // 'sans-serif' => '"Helvetica Neue", sans-serif', |
|
1090 * // 'serif' => 'Georgia, serif', |
|
1091 * // ); |
|
1092 * </code> |
|
1093 * |
|
1094 * @since 5.9.0 |
|
1095 * |
|
1096 * @param array $settings Settings to process. |
|
1097 * @param array $preset_metadata One of the PRESETS_METADATA values. |
|
1098 * @param array $origins List of origins to process. |
|
1099 * @return array Array of presets where each key is a slug and each value is the preset value. |
|
1100 */ |
|
1101 protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) { |
|
1102 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); |
|
1103 |
|
1104 $result = array(); |
|
1105 foreach ( $origins as $origin ) { |
|
1106 if ( ! isset( $preset_per_origin[ $origin ] ) ) { |
|
1107 continue; |
|
1108 } |
|
1109 foreach ( $preset_per_origin[ $origin ] as $preset ) { |
|
1110 $slug = _wp_to_kebab_case( $preset['slug'] ); |
|
1111 |
|
1112 $value = ''; |
|
1113 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { |
|
1114 $value_key = $preset_metadata['value_key']; |
|
1115 $value = $preset[ $value_key ]; |
|
1116 } elseif ( |
|
1117 isset( $preset_metadata['value_func'] ) && |
|
1118 is_callable( $preset_metadata['value_func'] ) |
|
1119 ) { |
|
1120 $value_func = $preset_metadata['value_func']; |
|
1121 $value = call_user_func( $value_func, $preset ); |
|
1122 } else { |
|
1123 // If we don't have a value, then don't add it to the result. |
|
1124 continue; |
|
1125 } |
|
1126 |
|
1127 $result[ $slug ] = $value; |
|
1128 } |
|
1129 } |
|
1130 return $result; |
|
1131 } |
|
1132 |
|
1133 /** |
|
1134 * Similar to get_settings_values_by_slug, but doesn't compute the value. |
|
1135 * |
|
1136 * @since 5.9.0 |
|
1137 * |
|
1138 * @param array $settings Settings to process. |
|
1139 * @param array $preset_metadata One of the PRESETS_METADATA values. |
|
1140 * @param array $origins List of origins to process. |
|
1141 * @return array Array of presets where the key and value are both the slug. |
|
1142 */ |
|
1143 protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) { |
|
1144 if ( null === $origins ) { |
|
1145 $origins = static::VALID_ORIGINS; |
|
1146 } |
|
1147 |
|
1148 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); |
|
1149 |
|
1150 $result = array(); |
|
1151 foreach ( $origins as $origin ) { |
|
1152 if ( ! isset( $preset_per_origin[ $origin ] ) ) { |
|
1153 continue; |
|
1154 } |
|
1155 foreach ( $preset_per_origin[ $origin ] as $preset ) { |
|
1156 $slug = _wp_to_kebab_case( $preset['slug'] ); |
|
1157 |
|
1158 // Use the array as a set so we don't get duplicates. |
|
1159 $result[ $slug ] = $slug; |
|
1160 } |
|
1161 } |
|
1162 return $result; |
|
1163 } |
|
1164 |
|
1165 /** |
|
1166 * Transform a slug into a CSS Custom Property. |
|
1167 * |
|
1168 * @since 5.9.0 |
|
1169 * |
|
1170 * @param string $input String to replace. |
|
1171 * @param string $slug The slug value to use to generate the custom property. |
|
1172 * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`. |
|
1173 */ |
|
1174 protected static function replace_slug_in_string( $input, $slug ) { |
|
1175 return strtr( $input, array( '$slug' => $slug ) ); |
736 } |
1176 } |
737 |
1177 |
738 /** |
1178 /** |
739 * Given the block settings, it extracts the CSS Custom Properties |
1179 * Given the block settings, it extracts the CSS Custom Properties |
740 * for the presets and adds them to the $declarations array |
1180 * for the presets and adds them to the $declarations array |
862 * 'name' => 'property_name', |
1303 * 'name' => 'property_name', |
863 * 'value' => 'property_value, |
1304 * 'value' => 'property_value, |
864 * ) |
1305 * ) |
865 * |
1306 * |
866 * @since 5.8.0 |
1307 * @since 5.8.0 |
867 * |
1308 * @since 5.9.0 Added the `$settings` and `$properties` parameters. |
868 * @param array $styles Styles to process. |
1309 * |
|
1310 * @param array $styles Styles to process. |
|
1311 * @param array $settings Theme settings. |
|
1312 * @param array $properties Properties metadata. |
869 * @return array Returns the modified $declarations. |
1313 * @return array Returns the modified $declarations. |
870 */ |
1314 */ |
871 private static function compute_style_properties( $styles ) { |
1315 protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) { |
|
1316 if ( null === $properties ) { |
|
1317 $properties = static::PROPERTIES_METADATA; |
|
1318 } |
|
1319 |
872 $declarations = array(); |
1320 $declarations = array(); |
873 if ( empty( $styles ) ) { |
1321 if ( empty( $styles ) ) { |
874 return $declarations; |
1322 return $declarations; |
875 } |
1323 } |
876 |
1324 |
877 $properties = array(); |
1325 foreach ( $properties as $css_property => $value_path ) { |
878 foreach ( self::PROPERTIES_METADATA as $name => $metadata ) { |
1326 $value = static::get_property_value( $styles, $value_path ); |
879 /* |
1327 |
880 * Some properties can be shorthand properties, meaning that |
1328 // Look up protected properties, keyed by value path. |
881 * they contain multiple values instead of a single one. |
1329 // Skip protected properties that are explicitly set to `null`. |
882 * An example of this is the padding property. |
1330 if ( is_array( $value_path ) ) { |
883 */ |
1331 $path_string = implode( '.', $value_path ); |
884 if ( self::has_properties( $metadata ) ) { |
1332 if ( |
885 foreach ( $metadata['properties'] as $property ) { |
1333 array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) && |
886 $properties[] = array( |
1334 _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null |
887 'name' => $name . '-' . $property, |
1335 ) { |
888 'value' => array_merge( $metadata['value'], array( $property ) ), |
1336 continue; |
889 ); |
1337 } |
890 } |
1338 } |
891 } else { |
1339 |
892 $properties[] = array( |
1340 // Skip if empty and not "0" or value represents array of longhand values. |
893 'name' => $name, |
1341 $has_missing_value = empty( $value ) && ! is_numeric( $value ); |
894 'value' => $metadata['value'], |
1342 if ( $has_missing_value || is_array( $value ) ) { |
895 ); |
|
896 } |
|
897 } |
|
898 |
|
899 foreach ( $properties as $prop ) { |
|
900 $value = self::get_property_value( $styles, $prop['value'] ); |
|
901 if ( empty( $value ) ) { |
|
902 continue; |
1343 continue; |
903 } |
1344 } |
904 |
1345 |
905 $declarations[] = array( |
1346 $declarations[] = array( |
906 'name' => $prop['name'], |
1347 'name' => $css_property, |
907 'value' => $value, |
1348 'value' => $value, |
908 ); |
1349 ); |
909 } |
1350 } |
910 |
1351 |
911 return $declarations; |
1352 return $declarations; |
912 } |
|
913 |
|
914 /** |
|
915 * Whether the metadata contains a key named properties. |
|
916 * |
|
917 * @since 5.8.0 |
|
918 * |
|
919 * @param array $metadata Description of the style property. |
|
920 * @return bool True if properties exists, false otherwise. |
|
921 */ |
|
922 private static function has_properties( $metadata ) { |
|
923 if ( array_key_exists( 'properties', $metadata ) ) { |
|
924 return true; |
|
925 } |
|
926 |
|
927 return false; |
|
928 } |
1353 } |
929 |
1354 |
930 /** |
1355 /** |
931 * Returns the style property for the given path. |
1356 * Returns the style property for the given path. |
932 * |
1357 * |
933 * It also converts CSS Custom Property stored as |
1358 * It also converts CSS Custom Property stored as |
934 * "var:preset|color|secondary" to the form |
1359 * "var:preset|color|secondary" to the form |
935 * "--wp--preset--color--secondary". |
1360 * "--wp--preset--color--secondary". |
936 * |
1361 * |
937 * @since 5.8.0 |
1362 * @since 5.8.0 |
|
1363 * @since 5.9.0 Added support for values of array type, which are returned as is. |
938 * |
1364 * |
939 * @param array $styles Styles subtree. |
1365 * @param array $styles Styles subtree. |
940 * @param array $path Which property to process. |
1366 * @param array $path Which property to process. |
941 * @return string Style property value. |
1367 * @return string|array Style property value. |
942 */ |
1368 */ |
943 private static function get_property_value( $styles, $path ) { |
1369 protected static function get_property_value( $styles, $path ) { |
944 $value = _wp_array_get( $styles, $path, '' ); |
1370 $value = _wp_array_get( $styles, $path, '' ); |
945 |
1371 |
946 if ( '' === $value ) { |
1372 if ( '' === $value || is_array( $value ) ) { |
947 return $value; |
1373 return $value; |
948 } |
1374 } |
949 |
1375 |
950 $prefix = 'var:'; |
1376 $prefix = 'var:'; |
951 $prefix_len = strlen( $prefix ); |
1377 $prefix_len = strlen( $prefix ); |
1085 |
1518 |
1086 return $nodes; |
1519 return $nodes; |
1087 } |
1520 } |
1088 |
1521 |
1089 /** |
1522 /** |
|
1523 * For metadata values that can either be booleans or paths to booleans, gets the value. |
|
1524 * |
|
1525 * ```php |
|
1526 * $data = array( |
|
1527 * 'color' => array( |
|
1528 * 'defaultPalette' => true |
|
1529 * ) |
|
1530 * ); |
|
1531 * |
|
1532 * static::get_metadata_boolean( $data, false ); |
|
1533 * // => false |
|
1534 * |
|
1535 * static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) ); |
|
1536 * // => true |
|
1537 * ``` |
|
1538 * |
|
1539 * @since 6.0.0 |
|
1540 * |
|
1541 * @param array $data The data to inspect. |
|
1542 * @param bool|array $path Boolean or path to a boolean. |
|
1543 * @param bool $default Default value if the referenced path is missing. |
|
1544 * Default false. |
|
1545 * @return bool Value of boolean metadata. |
|
1546 */ |
|
1547 protected static function get_metadata_boolean( $data, $path, $default = false ) { |
|
1548 if ( is_bool( $path ) ) { |
|
1549 return $path; |
|
1550 } |
|
1551 |
|
1552 if ( is_array( $path ) ) { |
|
1553 $value = _wp_array_get( $data, $path ); |
|
1554 if ( null !== $value ) { |
|
1555 return $value; |
|
1556 } |
|
1557 } |
|
1558 |
|
1559 return $default; |
|
1560 } |
|
1561 |
|
1562 /** |
1090 * Merge new incoming data. |
1563 * Merge new incoming data. |
1091 * |
1564 * |
1092 * @since 5.8.0 |
1565 * @since 5.8.0 |
|
1566 * @since 5.9.0 Duotone preset also has origins. |
1093 * |
1567 * |
1094 * @param WP_Theme_JSON $incoming Data to merge. |
1568 * @param WP_Theme_JSON $incoming Data to merge. |
1095 */ |
1569 */ |
1096 public function merge( $incoming ) { |
1570 public function merge( $incoming ) { |
1097 $incoming_data = $incoming->get_raw_data(); |
1571 $incoming_data = $incoming->get_raw_data(); |
1098 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); |
1572 $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); |
1099 |
1573 |
1100 /* |
1574 /* |
1101 * The array_replace_recursive() algorithm merges at the leaf level. |
1575 * The array_replace_recursive algorithm merges at the leaf level, |
1102 * For leaf values that are arrays it will use the numeric indexes for replacement. |
1576 * but we don't want leaf arrays to be merged, so we overwrite it. |
1103 * In those cases, we want to replace the existing with the incoming value, if it exists. |
1577 * |
|
1578 * For leaf values that are sequential arrays it will use the numeric indexes for replacement. |
|
1579 * We rather replace the existing with the incoming value, if it exists. |
|
1580 * This is the case of spacing.units. |
|
1581 * |
|
1582 * For leaf values that are associative arrays it will merge them as expected. |
|
1583 * This is also not the behavior we want for the current associative arrays (presets). |
|
1584 * We rather replace the existing with the incoming value, if it exists. |
|
1585 * This happens, for example, when we merge data from theme.json upon existing |
|
1586 * theme supports or when we merge anything coming from the same source twice. |
|
1587 * This is the case of color.palette, color.gradients, color.duotone, |
|
1588 * typography.fontSizes, or typography.fontFamilies. |
|
1589 * |
|
1590 * Additionally, for some preset types, we also want to make sure the |
|
1591 * values they introduce don't conflict with default values. We do so |
|
1592 * by checking the incoming slugs for theme presets and compare them |
|
1593 * with the equivalent default presets: if a slug is present as a default |
|
1594 * we remove it from the theme presets. |
1104 */ |
1595 */ |
1105 $to_replace = array(); |
1596 $nodes = static::get_setting_nodes( $incoming_data ); |
1106 $to_replace[] = array( 'spacing', 'units' ); |
1597 $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) ); |
1107 $to_replace[] = array( 'color', 'duotone' ); |
1598 foreach ( $nodes as $node ) { |
1108 foreach ( self::VALID_ORIGINS as $origin ) { |
1599 $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); |
1109 $to_replace[] = array( 'color', 'palette', $origin ); |
1600 $slugs = array_merge_recursive( $slugs_global, $slugs_node ); |
1110 $to_replace[] = array( 'color', 'gradients', $origin ); |
1601 |
1111 $to_replace[] = array( 'typography', 'fontSizes', $origin ); |
1602 // Replace the spacing.units. |
1112 $to_replace[] = array( 'typography', 'fontFamilies', $origin ); |
1603 $path = array_merge( $node['path'], array( 'spacing', 'units' ) ); |
1113 } |
1604 $content = _wp_array_get( $incoming_data, $path, null ); |
1114 |
1605 if ( isset( $content ) ) { |
1115 $nodes = self::get_setting_nodes( $this->theme_json ); |
1606 _wp_array_set( $this->theme_json, $path, $content ); |
1116 foreach ( $nodes as $metadata ) { |
1607 } |
1117 foreach ( $to_replace as $property_path ) { |
1608 |
1118 $path = array_merge( $metadata['path'], $property_path ); |
1609 // Replace the presets. |
1119 $node = _wp_array_get( $incoming_data, $path, null ); |
1610 foreach ( static::PRESETS_METADATA as $preset ) { |
1120 if ( isset( $node ) ) { |
1611 $override_preset = ! static::get_metadata_boolean( $this->theme_json['settings'], $preset['prevent_override'], true ); |
1121 _wp_array_set( $this->theme_json, $path, $node ); |
1612 |
1122 } |
1613 foreach ( static::VALID_ORIGINS as $origin ) { |
1123 } |
1614 $base_path = array_merge( $node['path'], $preset['path'] ); |
1124 } |
1615 $path = array_merge( $base_path, array( $origin ) ); |
|
1616 $content = _wp_array_get( $incoming_data, $path, null ); |
|
1617 if ( ! isset( $content ) ) { |
|
1618 continue; |
|
1619 } |
|
1620 |
|
1621 if ( 'theme' === $origin && $preset['use_default_names'] ) { |
|
1622 foreach ( $content as &$item ) { |
|
1623 if ( ! array_key_exists( 'name', $item ) ) { |
|
1624 $name = static::get_name_from_defaults( $item['slug'], $base_path ); |
|
1625 if ( null !== $name ) { |
|
1626 $item['name'] = $name; |
|
1627 } |
|
1628 } |
|
1629 } |
|
1630 } |
|
1631 |
|
1632 if ( |
|
1633 ( 'theme' !== $origin ) || |
|
1634 ( 'theme' === $origin && $override_preset ) |
|
1635 ) { |
|
1636 _wp_array_set( $this->theme_json, $path, $content ); |
|
1637 } else { |
|
1638 $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() ); |
|
1639 $content = static::filter_slugs( $content, $slugs_for_preset ); |
|
1640 _wp_array_set( $this->theme_json, $path, $content ); |
|
1641 } |
|
1642 } |
|
1643 } |
|
1644 } |
|
1645 } |
|
1646 |
|
1647 /** |
|
1648 * Converts all filter (duotone) presets into SVGs. |
|
1649 * |
|
1650 * @since 5.9.1 |
|
1651 * |
|
1652 * @param array $origins List of origins to process. |
|
1653 * @return string SVG filters. |
|
1654 */ |
|
1655 public function get_svg_filters( $origins ) { |
|
1656 $blocks_metadata = static::get_blocks_metadata(); |
|
1657 $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); |
|
1658 |
|
1659 $filters = ''; |
|
1660 foreach ( $setting_nodes as $metadata ) { |
|
1661 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); |
|
1662 if ( empty( $node['color']['duotone'] ) ) { |
|
1663 continue; |
|
1664 } |
|
1665 |
|
1666 $duotone_presets = $node['color']['duotone']; |
|
1667 |
|
1668 foreach ( $origins as $origin ) { |
|
1669 if ( ! isset( $duotone_presets[ $origin ] ) ) { |
|
1670 continue; |
|
1671 } |
|
1672 foreach ( $duotone_presets[ $origin ] as $duotone_preset ) { |
|
1673 $filters .= wp_get_duotone_filter_svg( $duotone_preset ); |
|
1674 } |
|
1675 } |
|
1676 } |
|
1677 |
|
1678 return $filters; |
|
1679 } |
|
1680 |
|
1681 /** |
|
1682 * Returns whether a presets should be overridden or not. |
|
1683 * |
|
1684 * @since 5.9.0 |
|
1685 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead. |
|
1686 * |
|
1687 * @param array $theme_json The theme.json like structure to inspect. |
|
1688 * @param array $path Path to inspect. |
|
1689 * @param bool|array $override Data to compute whether to override the preset. |
|
1690 * @return boolean |
|
1691 */ |
|
1692 protected static function should_override_preset( $theme_json, $path, $override ) { |
|
1693 _deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' ); |
|
1694 |
|
1695 if ( is_bool( $override ) ) { |
|
1696 return $override; |
|
1697 } |
|
1698 |
|
1699 /* |
|
1700 * The relationship between whether to override the defaults |
|
1701 * and whether the defaults are enabled is inverse: |
|
1702 * |
|
1703 * - If defaults are enabled => theme presets should not be overridden |
|
1704 * - If defaults are disabled => theme presets should be overridden |
|
1705 * |
|
1706 * For example, a theme sets defaultPalette to false, |
|
1707 * making the default palette hidden from the user. |
|
1708 * In that case, we want all the theme presets to be present, |
|
1709 * so they should override the defaults. |
|
1710 */ |
|
1711 if ( is_array( $override ) ) { |
|
1712 $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); |
|
1713 if ( isset( $value ) ) { |
|
1714 return ! $value; |
|
1715 } |
|
1716 |
|
1717 // Search the top-level key if none was found for this node. |
|
1718 $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); |
|
1719 if ( isset( $value ) ) { |
|
1720 return ! $value; |
|
1721 } |
|
1722 |
|
1723 return true; |
|
1724 } |
|
1725 } |
|
1726 |
|
1727 /** |
|
1728 * Returns the default slugs for all the presets in an associative array |
|
1729 * whose keys are the preset paths and the leafs is the list of slugs. |
|
1730 * |
|
1731 * For example: |
|
1732 * |
|
1733 * array( |
|
1734 * 'color' => array( |
|
1735 * 'palette' => array( 'slug-1', 'slug-2' ), |
|
1736 * 'gradients' => array( 'slug-3', 'slug-4' ), |
|
1737 * ), |
|
1738 * ) |
|
1739 * |
|
1740 * @since 5.9.0 |
|
1741 * |
|
1742 * @param array $data A theme.json like structure. |
|
1743 * @param array $node_path The path to inspect. It's 'settings' by default. |
|
1744 * @return array |
|
1745 */ |
|
1746 protected static function get_default_slugs( $data, $node_path ) { |
|
1747 $slugs = array(); |
|
1748 |
|
1749 foreach ( static::PRESETS_METADATA as $metadata ) { |
|
1750 $path = array_merge( $node_path, $metadata['path'], array( 'default' ) ); |
|
1751 $preset = _wp_array_get( $data, $path, null ); |
|
1752 if ( ! isset( $preset ) ) { |
|
1753 continue; |
|
1754 } |
|
1755 |
|
1756 $slugs_for_preset = array(); |
|
1757 $slugs_for_preset = array_map( |
|
1758 static function( $value ) { |
|
1759 return isset( $value['slug'] ) ? $value['slug'] : null; |
|
1760 }, |
|
1761 $preset |
|
1762 ); |
|
1763 _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); |
|
1764 } |
|
1765 |
|
1766 return $slugs; |
|
1767 } |
|
1768 |
|
1769 /** |
|
1770 * Get a `default`'s preset name by a provided slug. |
|
1771 * |
|
1772 * @since 5.9.0 |
|
1773 * |
|
1774 * @param string $slug The slug we want to find a match from default presets. |
|
1775 * @param array $base_path The path to inspect. It's 'settings' by default. |
|
1776 * @return string|null |
|
1777 */ |
|
1778 protected function get_name_from_defaults( $slug, $base_path ) { |
|
1779 $path = array_merge( $base_path, array( 'default' ) ); |
|
1780 $default_content = _wp_array_get( $this->theme_json, $path, null ); |
|
1781 if ( ! $default_content ) { |
|
1782 return null; |
|
1783 } |
|
1784 foreach ( $default_content as $item ) { |
|
1785 if ( $slug === $item['slug'] ) { |
|
1786 return $item['name']; |
|
1787 } |
|
1788 } |
|
1789 return null; |
|
1790 } |
|
1791 |
|
1792 /** |
|
1793 * Removes the preset values whose slug is equal to any of given slugs. |
|
1794 * |
|
1795 * @since 5.9.0 |
|
1796 * |
|
1797 * @param array $node The node with the presets to validate. |
|
1798 * @param array $slugs The slugs that should not be overridden. |
|
1799 * @return array The new node. |
|
1800 */ |
|
1801 protected static function filter_slugs( $node, $slugs ) { |
|
1802 if ( empty( $slugs ) ) { |
|
1803 return $node; |
|
1804 } |
|
1805 |
|
1806 $new_node = array(); |
|
1807 foreach ( $node as $value ) { |
|
1808 if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { |
|
1809 $new_node[] = $value; |
|
1810 } |
|
1811 } |
|
1812 |
|
1813 return $new_node; |
|
1814 } |
|
1815 |
|
1816 /** |
|
1817 * Removes insecure data from theme.json. |
|
1818 * |
|
1819 * @since 5.9.0 |
|
1820 * |
|
1821 * @param array $theme_json Structure to sanitize. |
|
1822 * @return array Sanitized structure. |
|
1823 */ |
|
1824 public static function remove_insecure_properties( $theme_json ) { |
|
1825 $sanitized = array(); |
|
1826 |
|
1827 $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); |
|
1828 |
|
1829 $valid_block_names = array_keys( static::get_blocks_metadata() ); |
|
1830 $valid_element_names = array_keys( static::ELEMENTS ); |
|
1831 $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); |
|
1832 |
|
1833 $blocks_metadata = static::get_blocks_metadata(); |
|
1834 $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); |
|
1835 foreach ( $style_nodes as $metadata ) { |
|
1836 $input = _wp_array_get( $theme_json, $metadata['path'], array() ); |
|
1837 if ( empty( $input ) ) { |
|
1838 continue; |
|
1839 } |
|
1840 |
|
1841 $output = static::remove_insecure_styles( $input ); |
|
1842 if ( ! empty( $output ) ) { |
|
1843 _wp_array_set( $sanitized, $metadata['path'], $output ); |
|
1844 } |
|
1845 } |
|
1846 |
|
1847 $setting_nodes = static::get_setting_nodes( $theme_json ); |
|
1848 foreach ( $setting_nodes as $metadata ) { |
|
1849 $input = _wp_array_get( $theme_json, $metadata['path'], array() ); |
|
1850 if ( empty( $input ) ) { |
|
1851 continue; |
|
1852 } |
|
1853 |
|
1854 $output = static::remove_insecure_settings( $input ); |
|
1855 if ( ! empty( $output ) ) { |
|
1856 _wp_array_set( $sanitized, $metadata['path'], $output ); |
|
1857 } |
|
1858 } |
|
1859 |
|
1860 if ( empty( $sanitized['styles'] ) ) { |
|
1861 unset( $theme_json['styles'] ); |
|
1862 } else { |
|
1863 $theme_json['styles'] = $sanitized['styles']; |
|
1864 } |
|
1865 |
|
1866 if ( empty( $sanitized['settings'] ) ) { |
|
1867 unset( $theme_json['settings'] ); |
|
1868 } else { |
|
1869 $theme_json['settings'] = $sanitized['settings']; |
|
1870 } |
|
1871 |
|
1872 return $theme_json; |
|
1873 } |
|
1874 |
|
1875 /** |
|
1876 * Processes a setting node and returns the same node |
|
1877 * without the insecure settings. |
|
1878 * |
|
1879 * @since 5.9.0 |
|
1880 * |
|
1881 * @param array $input Node to process. |
|
1882 * @return array |
|
1883 */ |
|
1884 protected static function remove_insecure_settings( $input ) { |
|
1885 $output = array(); |
|
1886 foreach ( static::PRESETS_METADATA as $preset_metadata ) { |
|
1887 foreach ( static::VALID_ORIGINS as $origin ) { |
|
1888 $path_with_origin = array_merge( $preset_metadata['path'], array( $origin ) ); |
|
1889 $presets = _wp_array_get( $input, $path_with_origin, null ); |
|
1890 if ( null === $presets ) { |
|
1891 continue; |
|
1892 } |
|
1893 |
|
1894 $escaped_preset = array(); |
|
1895 foreach ( $presets as $preset ) { |
|
1896 if ( |
|
1897 esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && |
|
1898 sanitize_html_class( $preset['slug'] ) === $preset['slug'] |
|
1899 ) { |
|
1900 $value = null; |
|
1901 if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { |
|
1902 $value = $preset[ $preset_metadata['value_key'] ]; |
|
1903 } elseif ( |
|
1904 isset( $preset_metadata['value_func'] ) && |
|
1905 is_callable( $preset_metadata['value_func'] ) |
|
1906 ) { |
|
1907 $value = call_user_func( $preset_metadata['value_func'], $preset ); |
|
1908 } |
|
1909 |
|
1910 $preset_is_valid = true; |
|
1911 foreach ( $preset_metadata['properties'] as $property ) { |
|
1912 if ( ! static::is_safe_css_declaration( $property, $value ) ) { |
|
1913 $preset_is_valid = false; |
|
1914 break; |
|
1915 } |
|
1916 } |
|
1917 |
|
1918 if ( $preset_is_valid ) { |
|
1919 $escaped_preset[] = $preset; |
|
1920 } |
|
1921 } |
|
1922 } |
|
1923 |
|
1924 if ( ! empty( $escaped_preset ) ) { |
|
1925 _wp_array_set( $output, $path_with_origin, $escaped_preset ); |
|
1926 } |
|
1927 } |
|
1928 } |
|
1929 return $output; |
|
1930 } |
|
1931 |
|
1932 /** |
|
1933 * Processes a style node and returns the same node |
|
1934 * without the insecure styles. |
|
1935 * |
|
1936 * @since 5.9.0 |
|
1937 * |
|
1938 * @param array $input Node to process. |
|
1939 * @return array |
|
1940 */ |
|
1941 protected static function remove_insecure_styles( $input ) { |
|
1942 $output = array(); |
|
1943 $declarations = static::compute_style_properties( $input ); |
|
1944 |
|
1945 foreach ( $declarations as $declaration ) { |
|
1946 if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { |
|
1947 $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; |
|
1948 |
|
1949 // Check the value isn't an array before adding so as to not |
|
1950 // double up shorthand and longhand styles. |
|
1951 $value = _wp_array_get( $input, $path, array() ); |
|
1952 if ( ! is_array( $value ) ) { |
|
1953 _wp_array_set( $output, $path, $value ); |
|
1954 } |
|
1955 } |
|
1956 } |
|
1957 return $output; |
|
1958 } |
|
1959 |
|
1960 /** |
|
1961 * Checks that a declaration provided by the user is safe. |
|
1962 * |
|
1963 * @since 5.9.0 |
|
1964 * |
|
1965 * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. |
|
1966 * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. |
|
1967 * @return bool |
|
1968 */ |
|
1969 protected static function is_safe_css_declaration( $property_name, $property_value ) { |
|
1970 $style_to_validate = $property_name . ': ' . $property_value; |
|
1971 $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); |
|
1972 return ! empty( trim( $filtered ) ); |
1125 } |
1973 } |
1126 |
1974 |
1127 /** |
1975 /** |
1128 * Returns the raw data. |
1976 * Returns the raw data. |
1129 * |
1977 * |
1218 |
2066 |
1219 if ( isset( $settings['enableCustomSpacing'] ) ) { |
2067 if ( isset( $settings['enableCustomSpacing'] ) ) { |
1220 if ( ! isset( $theme_settings['settings']['spacing'] ) ) { |
2068 if ( ! isset( $theme_settings['settings']['spacing'] ) ) { |
1221 $theme_settings['settings']['spacing'] = array(); |
2069 $theme_settings['settings']['spacing'] = array(); |
1222 } |
2070 } |
1223 $theme_settings['settings']['spacing']['customPadding'] = $settings['enableCustomSpacing']; |
2071 $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; |
1224 } |
|
1225 |
|
1226 // Things that didn't land in core yet, so didn't have a setting assigned. |
|
1227 if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) { |
|
1228 if ( ! isset( $theme_settings['settings']['color'] ) ) { |
|
1229 $theme_settings['settings']['color'] = array(); |
|
1230 } |
|
1231 $theme_settings['settings']['color']['link'] = true; |
|
1232 } |
2072 } |
1233 |
2073 |
1234 return $theme_settings; |
2074 return $theme_settings; |
1235 } |
2075 } |
1236 |
2076 |
|
2077 /** |
|
2078 * Returns the current theme's wanted patterns(slugs) to be |
|
2079 * registered from Pattern Directory. |
|
2080 * |
|
2081 * @since 6.0.0 |
|
2082 * |
|
2083 * @return string[] |
|
2084 */ |
|
2085 public function get_patterns() { |
|
2086 if ( isset( $this->theme_json['patterns'] ) && is_array( $this->theme_json['patterns'] ) ) { |
|
2087 return $this->theme_json['patterns']; |
|
2088 } |
|
2089 return array(); |
|
2090 } |
|
2091 |
|
2092 /** |
|
2093 * Returns a valid theme.json as provided by a theme. |
|
2094 * |
|
2095 * Unlike get_raw_data() this returns the presets flattened, as provided by a theme. |
|
2096 * This also uses appearanceTools instead of their opt-ins if all of them are true. |
|
2097 * |
|
2098 * @since 6.0.0 |
|
2099 * |
|
2100 * @return array |
|
2101 */ |
|
2102 public function get_data() { |
|
2103 $output = $this->theme_json; |
|
2104 $nodes = static::get_setting_nodes( $output ); |
|
2105 |
|
2106 /** |
|
2107 * Flatten the theme & custom origins into a single one. |
|
2108 * |
|
2109 * For example, the following: |
|
2110 * |
|
2111 * { |
|
2112 * "settings": { |
|
2113 * "color": { |
|
2114 * "palette": { |
|
2115 * "theme": [ {} ], |
|
2116 * "custom": [ {} ] |
|
2117 * } |
|
2118 * } |
|
2119 * } |
|
2120 * } |
|
2121 * |
|
2122 * will be converted to: |
|
2123 * |
|
2124 * { |
|
2125 * "settings": { |
|
2126 * "color": { |
|
2127 * "palette": [ {} ] |
|
2128 * } |
|
2129 * } |
|
2130 * } |
|
2131 */ |
|
2132 foreach ( $nodes as $node ) { |
|
2133 foreach ( static::PRESETS_METADATA as $preset_metadata ) { |
|
2134 $path = array_merge( $node['path'], $preset_metadata['path'] ); |
|
2135 $preset = _wp_array_get( $output, $path, null ); |
|
2136 if ( null === $preset ) { |
|
2137 continue; |
|
2138 } |
|
2139 |
|
2140 $items = array(); |
|
2141 if ( isset( $preset['theme'] ) ) { |
|
2142 foreach ( $preset['theme'] as $item ) { |
|
2143 $slug = $item['slug']; |
|
2144 unset( $item['slug'] ); |
|
2145 $items[ $slug ] = $item; |
|
2146 } |
|
2147 } |
|
2148 if ( isset( $preset['custom'] ) ) { |
|
2149 foreach ( $preset['custom'] as $item ) { |
|
2150 $slug = $item['slug']; |
|
2151 unset( $item['slug'] ); |
|
2152 $items[ $slug ] = $item; |
|
2153 } |
|
2154 } |
|
2155 $flattened_preset = array(); |
|
2156 foreach ( $items as $slug => $value ) { |
|
2157 $flattened_preset[] = array_merge( array( 'slug' => $slug ), $value ); |
|
2158 } |
|
2159 _wp_array_set( $output, $path, $flattened_preset ); |
|
2160 } |
|
2161 } |
|
2162 |
|
2163 // If all of the static::APPEARANCE_TOOLS_OPT_INS are true, |
|
2164 // this code unsets them and sets 'appearanceTools' instead. |
|
2165 foreach ( $nodes as $node ) { |
|
2166 $all_opt_ins_are_set = true; |
|
2167 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { |
|
2168 $full_path = array_merge( $node['path'], $opt_in_path ); |
|
2169 // Use "unset prop" as a marker instead of "null" because |
|
2170 // "null" can be a valid value for some props (e.g. blockGap). |
|
2171 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); |
|
2172 if ( 'unset prop' === $opt_in_value ) { |
|
2173 $all_opt_ins_are_set = false; |
|
2174 break; |
|
2175 } |
|
2176 } |
|
2177 |
|
2178 if ( $all_opt_ins_are_set ) { |
|
2179 _wp_array_set( $output, array_merge( $node['path'], array( 'appearanceTools' ) ), true ); |
|
2180 foreach ( static::APPEARANCE_TOOLS_OPT_INS as $opt_in_path ) { |
|
2181 $full_path = array_merge( $node['path'], $opt_in_path ); |
|
2182 // Use "unset prop" as a marker instead of "null" because |
|
2183 // "null" can be a valid value for some props (e.g. blockGap). |
|
2184 $opt_in_value = _wp_array_get( $output, $full_path, 'unset prop' ); |
|
2185 if ( true !== $opt_in_value ) { |
|
2186 continue; |
|
2187 } |
|
2188 |
|
2189 // The following could be improved to be path independent. |
|
2190 // At the moment it relies on a couple of assumptions: |
|
2191 // |
|
2192 // - all opt-ins having a path of size 2. |
|
2193 // - there's two sources of settings: the top-level and the block-level. |
|
2194 if ( |
|
2195 ( 1 === count( $node['path'] ) ) && |
|
2196 ( 'settings' === $node['path'][0] ) |
|
2197 ) { |
|
2198 // Top-level settings. |
|
2199 unset( $output['settings'][ $opt_in_path[0] ][ $opt_in_path[1] ] ); |
|
2200 if ( empty( $output['settings'][ $opt_in_path[0] ] ) ) { |
|
2201 unset( $output['settings'][ $opt_in_path[0] ] ); |
|
2202 } |
|
2203 } elseif ( |
|
2204 ( 3 === count( $node['path'] ) ) && |
|
2205 ( 'settings' === $node['path'][0] ) && |
|
2206 ( 'blocks' === $node['path'][1] ) |
|
2207 ) { |
|
2208 // Block-level settings. |
|
2209 $block_name = $node['path'][2]; |
|
2210 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ][ $opt_in_path[1] ] ); |
|
2211 if ( empty( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ) ) { |
|
2212 unset( $output['settings']['blocks'][ $block_name ][ $opt_in_path[0] ] ); |
|
2213 } |
|
2214 } |
|
2215 } |
|
2216 } |
|
2217 } |
|
2218 |
|
2219 wp_recursive_ksort( $output ); |
|
2220 |
|
2221 return $output; |
|
2222 } |
|
2223 |
1237 } |
2224 } |