Changeset 52049 for trunk/src/wp-includes/class-wp-theme-json.php
- Timestamp:
- 11/08/2021 07:18:39 PM (3 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/class-wp-theme-json.php
r51657 r52049 10 10 /** 11 11 * Class that encapsulates the processing of structures that adhere to the theme.json spec. 12 * 13 * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes). 14 * This is a low-level API that may need to do breaking changes. Please, 15 * use get_global_settings, get_global_styles, and get_global_stylesheet instead. 12 16 * 13 17 * @access private … … 71 75 * This contains the necessary metadata to process them: 72 76 * 73 * - path => where to find the preset within the settings section 74 * 75 * - value_key => the key that represents the value 76 * 77 * - css_var_infix => infix to use in generating the CSS Custom Property. Example: 78 * --wp--preset--<preset_infix>--<slug>: <preset_value> 79 * 80 * - classes => array containing a structure with the classes to 81 * generate for the presets. Each class should have 82 * the class suffix and the property name. Example: 83 * 84 * .has-<slug>-<class_suffix> { 85 * <property_name>: <preset_value> 86 * } 87 * 88 * @since 5.8.0 77 * - path => where to find the preset within the settings section 78 * - value_key => the key that represents the value 79 * - value_func => the callback to render the value (either value_key or value_func should be present) 80 * - css_vars => template string to use in generating the CSS Custom Property. 81 * Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined 82 * substituting the $slug for the slug's value for each preset value. 83 * - classes => array containing a structure with the classes to generate for the presets. 84 * Each key is a template string to resolve similarly to the css_vars and each value is the CSS property to use for that class. 85 * Example output: ".has-blue-color { color: <value> }" 86 * - properties => a list of CSS properties to be used by kses to check the preset value is safe. 87 * 88 * @since 5.8.0 89 * @since 5.9.0 Added new presets and simplified the metadata structure. 89 90 * @var array 90 91 */ 91 92 const PRESETS_METADATA = array( 92 93 array( 93 'path' => array( 'color', 'palette' ), 94 'value_key' => 'color', 95 'css_var_infix' => 'color', 96 'classes' => array( 97 array( 98 'class_suffix' => 'color', 99 'property_name' => 'color', 100 ), 101 array( 102 'class_suffix' => 'background-color', 103 'property_name' => 'background-color', 104 ), 94 'path' => array( 'color', 'palette' ), 95 'value_key' => 'color', 96 'css_vars' => '--wp--preset--color--$slug', 97 'classes' => array( 98 '.has-$slug-color' => 'color', 99 '.has-$slug-background-color' => 'background-color', 100 '.has-$slug-border-color' => 'border-color', 105 101 ), 102 'properties' => array( 'color', 'background-color', 'border-color' ), 106 103 ), 107 104 array( 108 'path' => array( 'color', 'gradients' ), 109 'value_key' => 'gradient', 110 'css_var_infix' => 'gradient', 111 'classes' => array( 112 array( 113 'class_suffix' => 'gradient-background', 114 'property_name' => 'background', 115 ), 116 ), 105 'path' => array( 'color', 'gradients' ), 106 'value_key' => 'gradient', 107 'css_vars' => '--wp--preset--gradient--$slug', 108 'classes' => array( '.has-$slug-gradient-background' => 'background' ), 109 'properties' => array( 'background' ), 117 110 ), 118 111 array( 119 'path' => array( 'typography', 'fontSizes' ), 120 'value_key' => 'size', 121 'css_var_infix' => 'font-size', 122 'classes' => array( 123 array( 124 'class_suffix' => 'font-size', 125 'property_name' => 'font-size', 126 ), 127 ), 112 'path' => array( 'color', 'duotone' ), 113 'value_func' => 'wp_render_duotone_filter_preset', 114 'css_vars' => '--wp--preset--duotone--$slug', 115 'classes' => array(), 116 'properties' => array( 'filter' ), 117 ), 118 array( 119 'path' => array( 'typography', 'fontSizes' ), 120 'value_key' => 'size', 121 'css_vars' => '--wp--preset--font-size--$slug', 122 'classes' => array( '.has-$slug-font-size' => 'font-size' ), 123 'properties' => array( 'font-size' ), 124 ), 125 array( 126 'path' => array( 'typography', 'fontFamilies' ), 127 'value_key' => 'fontFamily', 128 'css_vars' => '--wp--preset--font-family--$slug', 129 'classes' => array( '.has-$slug-font-family' => 'font-family' ), 130 'properties' => array( 'font-family' ), 128 131 ), 129 132 ); … … 132 135 * Metadata for style properties. 133 136 * 134 * Each property declares:135 * 136 * - 'value': path to the value in theme.json and block attributes.137 * 138 * @since 5. 8.0137 * Each element is a direct mapping from the CSS property name to the 138 * path to the value in theme.json & block attributes. 139 * 140 * @since 5.8.0 141 * @since 5.9.0 Added new properties and simplified the metadata structure. 139 142 * @var array 140 143 */ 141 144 const PROPERTIES_METADATA = array( 142 'background' => array( 143 'value' => array( 'color', 'gradient' ), 144 ), 145 'background-color' => array( 146 'value' => array( 'color', 'background' ), 147 ), 148 'color' => array( 149 'value' => array( 'color', 'text' ), 150 ), 151 'font-size' => array( 152 'value' => array( 'typography', 'fontSize' ), 153 ), 154 'line-height' => array( 155 'value' => array( 'typography', 'lineHeight' ), 156 ), 157 'margin' => array( 158 'value' => array( 'spacing', 'margin' ), 159 'properties' => array( 'top', 'right', 'bottom', 'left' ), 160 ), 161 'padding' => array( 162 'value' => array( 'spacing', 'padding' ), 163 'properties' => array( 'top', 'right', 'bottom', 'left' ), 164 ), 145 'background' => array( 'color', 'gradient' ), 146 'background-color' => array( 'color', 'background' ), 147 'border-radius' => array( 'border', 'radius' ), 148 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), 149 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), 150 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), 151 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), 152 'border-color' => array( 'border', 'color' ), 153 'border-width' => array( 'border', 'width' ), 154 'border-style' => array( 'border', 'style' ), 155 'color' => array( 'color', 'text' ), 156 'font-family' => array( 'typography', 'fontFamily' ), 157 'font-size' => array( 'typography', 'fontSize' ), 158 'font-style' => array( 'typography', 'fontStyle' ), 159 'font-weight' => array( 'typography', 'fontWeight' ), 160 'letter-spacing' => array( 'typography', 'letterSpacing' ), 161 'line-height' => array( 'typography', 'lineHeight' ), 162 'margin' => array( 'spacing', 'margin' ), 163 'margin-top' => array( 'spacing', 'margin', 'top' ), 164 'margin-right' => array( 'spacing', 'margin', 'right' ), 165 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), 166 'margin-left' => array( 'spacing', 'margin', 'left' ), 167 'padding' => array( 'spacing', 'padding' ), 168 'padding-top' => array( 'spacing', 'padding', 'top' ), 169 'padding-right' => array( 'spacing', 'padding', 'right' ), 170 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), 171 'padding-left' => array( 'spacing', 'padding', 'left' ), 172 '--wp--style--block-gap' => array( 'spacing', 'blockGap' ), 173 'text-decoration' => array( 'typography', 'textDecoration' ), 174 'text-transform' => array( 'typography', 'textTransform' ), 175 'filter' => array( 'filter', 'duotone' ), 165 176 ); 166 177 167 178 /** 168 * @since 5.8.0 179 * Protected style properties. 180 * 181 * These style properties are only rendered if a setting enables it 182 * via a value other than `null`. 183 * 184 * Each element maps the style property to the corresponding theme.json 185 * setting key. 186 * 187 * @since 5.9.0 188 */ 189 const PROTECTED_PROPERTIES = array( 190 'spacing.blockGap' => array( 'spacing', 'blockGap' ), 191 ); 192 193 /** 194 * The top-level keys a theme.json can have. 195 * 196 * @since 5.8.0 197 * @since 5.9.0 Renamed from ALLOWED_TOP_LEVEL_KEYS and added new values. 169 198 * @var string[] 170 199 */ 171 const ALLOWED_TOP_LEVEL_KEYS = array( 200 const VALID_TOP_LEVEL_KEYS = array( 201 'customTemplates', 172 202 'settings', 173 203 'styles', 204 'templateParts', 174 205 'version', 175 206 ); 176 207 177 208 /** 178 * @since 5.8.0 209 * The valid properties under the settings key. 210 * 211 * @since 5.8.0 212 * @since 5.9.0 Renamed from ALLOWED_SETTINGS, gained new properties, and renamed others according to the new schema. 179 213 * @var array 180 214 */ 181 const ALLOWED_SETTINGS = array(215 const VALID_SETTINGS = array( 182 216 'border' => array( 183 'customRadius' => null, 217 'color' => null, 218 'radius' => null, 219 'style' => null, 220 'width' => null, 184 221 ), 185 222 'color' => array( 223 'background' => null, 186 224 'custom' => null, 187 225 'customDuotone' => null, … … 191 229 'link' => null, 192 230 'palette' => null, 231 'text' => null, 193 232 ), 194 233 'custom' => null, … … 198 237 ), 199 238 'spacing' => array( 200 'customMargin' => null, 201 'customPadding' => null, 202 'units' => null, 239 'blockGap' => null, 240 'margin' => null, 241 'padding' => null, 242 'units' => null, 203 243 ), 204 244 'typography' => array( 205 'customFontSize' => null, 206 'customLineHeight' => null, 207 'dropCap' => null, 208 'fontSizes' => null, 245 'customFontSize' => null, 246 'dropCap' => null, 247 'fontFamilies' => null, 248 'fontSizes' => null, 249 'fontStyle' => null, 250 'fontWeight' => null, 251 'letterSpacing' => null, 252 'lineHeight' => null, 253 'textDecoration' => null, 254 'textTransform' => null, 209 255 ), 210 256 ); 211 257 212 258 /** 213 * @since 5.8.0 259 * The valid properties under the styles key. 260 * 261 * @since 5.8.0 262 * @since 5.9.0 Renamed from ALLOWED_SETTINGS, gained new properties. 214 263 * @var array 215 264 */ 216 const ALLOWED_STYLES = array(265 const VALID_STYLES = array( 217 266 'border' => array( 267 'color' => null, 218 268 'radius' => null, 269 'style' => null, 270 'width' => null, 219 271 ), 220 272 'color' => array( … … 223 275 'text' => null, 224 276 ), 277 'filter' => array( 278 'duotone' => null, 279 ), 225 280 'spacing' => array( 226 'margin' => array( 227 'top' => null, 228 'right' => null, 229 'bottom' => null, 230 'left' => null, 231 ), 232 'padding' => array( 233 'bottom' => null, 234 'left' => null, 235 'right' => null, 236 'top' => null, 237 ), 281 'margin' => null, 282 'padding' => null, 283 'blockGap' => null, 238 284 ), 239 285 'typography' => array( 240 'fontSize' => null, 241 'lineHeight' => null, 286 'fontFamily' => null, 287 'fontSize' => null, 288 'fontStyle' => null, 289 'fontWeight' => null, 290 'letterSpacing' => null, 291 'lineHeight' => null, 292 'textDecoration' => null, 293 'textTransform' => null, 242 294 ), 243 295 ); … … 258 310 259 311 /** 260 * @since 5.8.0 312 * The latest version of the schema in use. 313 * 314 * @since 5.8.0 315 * @since 5.9.0 Changed value. 261 316 * @var int 262 317 */ 263 const LATEST_SCHEMA = 1;318 const LATEST_SCHEMA = 2; 264 319 265 320 /** … … 277 332 } 278 333 279 if ( ! isset( $theme_json['version'] ) || self::LATEST_SCHEMA !== $theme_json['version'] ) { 280 $this->theme_json = array(); 281 return; 282 } 283 284 $this->theme_json = self::sanitize( $theme_json ); 334 $this->theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); 335 $valid_block_names = array_keys( self::get_blocks_metadata() ); 336 $valid_element_names = array_keys( self::ELEMENTS ); 337 $this->theme_json = self::sanitize( $this->theme_json, $valid_block_names, $valid_element_names ); 285 338 286 339 // Internally, presets are keyed by origin. 287 340 $nodes = self::get_setting_nodes( $this->theme_json ); 288 341 foreach ( $nodes as $node ) { 289 foreach ( self::PRESETS_METADATA as $preset ) {290 $path = array_merge( $node['path'], $preset ['path'] );342 foreach ( self::PRESETS_METADATA as $preset_metadata ) { 343 $path = array_merge( $node['path'], $preset_metadata['path'] ); 291 344 $preset = _wp_array_get( $this->theme_json, $path, null ); 292 345 if ( null !== $preset ) { … … 301 354 * 302 355 * @since 5.8.0 356 * @since 5.9.0 Has new parameters. 303 357 * 304 358 * @param array $input Structure to sanitize. 359 * @param array $valid_block_names List of valid block names. 360 * @param array $valid_element_names List of valid element names. 305 361 * @return array The sanitized output. 306 362 */ 307 private static function sanitize( $input ) {363 private static function sanitize( $input, $valid_block_names, $valid_element_names ) { 308 364 $output = array(); 309 365 … … 312 368 } 313 369 314 $allowed_top_level_keys = self::ALLOWED_TOP_LEVEL_KEYS; 315 $allowed_settings = self::ALLOWED_SETTINGS; 316 $allowed_styles = self::ALLOWED_STYLES; 317 $allowed_blocks = array_keys( self::get_blocks_metadata() ); 318 $allowed_elements = array_keys( self::ELEMENTS ); 319 320 $output = array_intersect_key( $input, array_flip( $allowed_top_level_keys ) ); 321 322 // Build the schema. 370 $output = array_intersect_key( $input, array_flip( self::VALID_TOP_LEVEL_KEYS ) ); 371 372 // Build the schema based on valid block & element names. 323 373 $schema = array(); 324 374 $schema_styles_elements = array(); 325 foreach ( $ allowed_elements as $element ) {326 $schema_styles_elements[ $element ] = $allowed_styles;375 foreach ( $valid_element_names as $element ) { 376 $schema_styles_elements[ $element ] = self::VALID_STYLES; 327 377 } 328 378 $schema_styles_blocks = array(); 329 379 $schema_settings_blocks = array(); 330 foreach ( $ allowed_blocks as $block ) {331 $schema_settings_blocks[ $block ] = $allowed_settings;332 $schema_styles_blocks[ $block ] = $allowed_styles;380 foreach ( $valid_block_names as $block ) { 381 $schema_settings_blocks[ $block ] = self::VALID_SETTINGS; 382 $schema_styles_blocks[ $block ] = self::VALID_STYLES; 333 383 $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; 334 384 } 335 $schema['styles'] = $allowed_styles;385 $schema['styles'] = self::VALID_STYLES; 336 386 $schema['styles']['blocks'] = $schema_styles_blocks; 337 387 $schema['styles']['elements'] = $schema_styles_elements; 338 $schema['settings'] = $allowed_settings;388 $schema['settings'] = self::VALID_SETTINGS; 339 389 $schema['settings']['blocks'] = $schema_settings_blocks; 340 390 … … 361 411 return $output; 362 412 } 363 364 413 /** 365 414 * Returns the metadata for each block. … … 378 427 * 'selector': 'h1', 379 428 * 'elements': {} 380 * } 381 * 'core/group': { 382 * 'selector': '.wp-block-group', 429 * }, 430 * 'core/image': { 431 * 'selector': '.wp-block-image', 432 * 'duotone': 'img', 383 433 * 'elements': {} 384 434 * } … … 386 436 * 387 437 * @since 5.8.0 438 * @since 5.9.0 Added duotone key with CSS selector. 388 439 * 389 440 * @return array Block metadata. … … 408 459 } 409 460 410 /* 411 * Assign defaults, then overwrite those that the block sets by itself. 412 * If the block selector is compounded, will append the element to each 413 * individual block selector. 414 */ 461 if ( 462 isset( $block_type->supports['color']['__experimentalDuotone'] ) && 463 is_string( $block_type->supports['color']['__experimentalDuotone'] ) 464 ) { 465 self::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone']; 466 } 467 468 // Assign defaults, then overwrite those that the block sets by itself. 469 // If the block selector is compounded, will append the element to each 470 // individual block selector. 415 471 $block_selectors = explode( ',', self::$blocks_metadata[ $block_name ]['selector'] ); 416 472 foreach ( self::ELEMENTS as $el_name => $el_selector ) { … … 494 550 * 495 551 * @since 5.8.0 496 * 497 * @param string $type Optional. Type of stylesheet we want. Accepts 'all', 498 * 'block_styles', and 'css_variables'. Default 'all'. 552 * @since 5.9.0 Changed the arguments passed to the function. 553 * 554 * @param array $types Types of styles to load. Will load all by default. It accepts: 555 * 'variables': only the CSS Custom Properties for presets & custom ones. 556 * 'styles': only the styles section in theme.json. 557 * 'presets': only the classes for the presets. 558 * @param array $origins A list of origins to include. By default it includes self::VALID_ORIGINS. 499 559 * @return string Stylesheet. 500 560 */ 501 public function get_stylesheet( $type = 'all' ) { 561 public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = self::VALID_ORIGINS ) { 562 if ( is_string( $types ) ) { 563 // Dispatch error and map old arguments to new ones. 564 _deprecated_argument( __FUNCTION__, '5.9' ); 565 if ( 'block_styles' === $types ) { 566 $types = array( 'styles', 'presets' ); 567 } elseif ( 'css_variables' === $types ) { 568 $types = array( 'variables' ); 569 } else { 570 $types = array( 'variables', 'styles', 'presets' ); 571 } 572 } 573 502 574 $blocks_metadata = self::get_blocks_metadata(); 503 575 $style_nodes = self::get_style_nodes( $this->theme_json, $blocks_metadata ); 504 576 $setting_nodes = self::get_setting_nodes( $this->theme_json, $blocks_metadata ); 505 577 506 switch ( $type ) { 507 case 'block_styles': 508 return $this->get_block_styles( $style_nodes, $setting_nodes ); 509 case 'css_variables': 510 return $this->get_css_variables( $setting_nodes ); 511 default: 512 return $this->get_css_variables( $setting_nodes ) . $this->get_block_styles( $style_nodes, $setting_nodes ); 513 } 514 578 $stylesheet = ''; 579 580 if ( in_array( 'variables', $types, true ) ) { 581 $stylesheet .= $this->get_css_variables( $setting_nodes, $origins ); 582 } 583 584 if ( in_array( 'styles', $types, true ) ) { 585 $stylesheet .= $this->get_block_classes( $style_nodes ); 586 } 587 588 if ( in_array( 'presets', $types, true ) ) { 589 $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); 590 } 591 592 return $stylesheet; 593 } 594 595 /** 596 * Returns the page templates of the current theme. 597 * 598 * @since 5.9.0 599 * 600 * @return array 601 */ 602 public function get_custom_templates() { 603 $custom_templates = array(); 604 if ( ! isset( $this->theme_json['customTemplates'] ) ) { 605 return $custom_templates; 606 } 607 608 foreach ( $this->theme_json['customTemplates'] as $item ) { 609 if ( isset( $item['name'] ) ) { 610 $custom_templates[ $item['name'] ] = array( 611 'title' => isset( $item['title'] ) ? $item['title'] : '', 612 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ), 613 ); 614 } 615 } 616 return $custom_templates; 617 } 618 619 /** 620 * Returns the template part data of current theme. 621 * 622 * @since 5.9.0 623 * 624 * @return array 625 */ 626 public function get_template_parts() { 627 $template_parts = array(); 628 if ( ! isset( $this->theme_json['templateParts'] ) ) { 629 return $template_parts; 630 } 631 632 foreach ( $this->theme_json['templateParts'] as $item ) { 633 if ( isset( $item['name'] ) ) { 634 $template_parts[ $item['name'] ] = array( 635 'title' => isset( $item['title'] ) ? $item['title'] : '', 636 'area' => isset( $item['area'] ) ? $item['area'] : '', 637 ); 638 } 639 } 640 return $template_parts; 515 641 } 516 642 … … 527 653 * } 528 654 * 529 * Additionally, it'll also create new rulesets 530 * as classes for each preset value such as: 531 * 532 * .has-value-color { 533 * color: value; 534 * } 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. 655 * @since 5.8.0 656 * @since 5.9.0 Renamed to get_block_classes and no longer returns preset classes. 657 * 658 * @param array $style_nodes Nodes with styles. 556 659 * @return string The new stylesheet. 557 660 */ 558 private function get_block_ styles( $style_nodes, $setting_nodes ) {661 private function get_block_classes( $style_nodes ) { 559 662 $block_rules = ''; 663 560 664 foreach ( $style_nodes as $metadata ) { 561 665 if ( null === $metadata['selector'] ) { … … 565 669 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 566 670 $selector = $metadata['selector']; 567 $declarations = self::compute_style_properties( $node ); 671 $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); 672 $declarations = self::compute_style_properties( $node, $settings ); 673 674 // 1. Separate the ones who use the general selector 675 // and the ones who use the duotone selector. 676 $declarations_duotone = array(); 677 foreach ( $declarations as $index => $declaration ) { 678 if ( 'filter' === $declaration['name'] ) { 679 unset( $declarations[ $index ] ); 680 $declarations_duotone[] = $declaration; 681 } 682 } 683 684 // 2. Generate the rules that use the general selector. 568 685 $block_rules .= self::to_ruleset( $selector, $declarations ); 569 } 570 686 687 // 3. Generate the rules that use the duotone selector. 688 if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { 689 $selector_duotone = self::scope_selector( $metadata['selector'], $metadata['duotone'] ); 690 $block_rules .= self::to_ruleset( $selector_duotone, $declarations_duotone ); 691 } 692 693 if ( self::ROOT_BLOCK_SELECTOR === $selector ) { 694 $block_rules .= 'body { margin: 0; }'; 695 $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; 696 $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; 697 $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; 698 699 $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; 700 if ( $has_block_gap_support ) { 701 $block_rules .= '.wp-site-blocks > * { margin-top: 0; margin-bottom: 0; }'; 702 $block_rules .= '.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); }'; 703 } 704 } 705 } 706 707 return $block_rules; 708 } 709 710 /** 711 * Creates new rulesets as classes for each preset value such as: 712 * 713 * .has-value-color { 714 * color: value; 715 * } 716 * 717 * .has-value-background-color { 718 * background-color: value; 719 * } 720 * 721 * .has-value-font-size { 722 * font-size: value; 723 * } 724 * 725 * .has-value-gradient-background { 726 * background: value; 727 * } 728 * 729 * p.has-value-gradient-background { 730 * background: value; 731 * } 732 * 733 * @since 5.9.0 734 * 735 * @param array $setting_nodes Nodes with settings. 736 * @param array $origins List of origins to process presets from. 737 * @return string The new stylesheet. 738 */ 739 private function get_preset_classes( $setting_nodes, $origins ) { 571 740 $preset_rules = ''; 741 572 742 foreach ( $setting_nodes as $metadata ) { 573 743 if ( null === $metadata['selector'] ) { … … 577 747 $selector = $metadata['selector']; 578 748 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 579 $preset_rules .= self::compute_preset_classes( $node, $selector );580 } 581 582 return $ block_rules . $preset_rules;749 $preset_rules .= self::compute_preset_classes( $node, $selector, $origins ); 750 } 751 752 return $preset_rules; 583 753 } 584 754 … … 598 768 * 599 769 * @since 5.8.0 770 * @since 5.9.0 Added origins parameter. 600 771 * 601 772 * @param array $nodes Nodes with settings. 773 * @param array $origins List of origins to process. 602 774 * @return string The new stylesheet. 603 775 */ 604 private function get_css_variables( $nodes ) {776 private function get_css_variables( $nodes, $origins ) { 605 777 $stylesheet = ''; 606 778 foreach ( $nodes as $metadata ) { … … 612 784 613 785 $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); 614 $declarations = array_merge( self::compute_preset_vars( $node ), self::compute_theme_vars( $node ) );786 $declarations = array_merge( self::compute_preset_vars( $node, $origins ), self::compute_theme_vars( $node ) ); 615 787 616 788 $stylesheet .= self::to_ruleset( $selector, $declarations ); … … 669 841 670 842 /** 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.0675 *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/32347690 * However, we need to make sure the generated class or CSS variable691 * doesn't contain spaces.692 */693 $result[ preg_replace( '/\s+/', '-', $preset['slug'] ) ] = $preset[ $value_key ];694 }695 }696 return $result;697 }698 699 /**700 843 * Given a settings array, it returns the generated rulesets 701 844 * for the preset classes. 702 845 * 703 846 * @since 5.8.0 847 * @since 5.9.0 Added origins parameter. 704 848 * 705 849 * @param array $settings Settings to process. 706 850 * @param string $selector Selector wrapping the classes. 851 * @param array $origins List of origins to process. 707 852 * @return string The result of processing the presets. 708 853 */ 709 private static function compute_preset_classes( $settings, $selector ) {854 private static function compute_preset_classes( $settings, $selector, $origins ) { 710 855 if ( self::ROOT_BLOCK_SELECTOR === $selector ) { 711 856 // Classes at the global level do not need any CSS prefixed, … … 715 860 716 861 $stylesheet = ''; 717 foreach ( self::PRESETS_METADATA as $preset ) { 718 $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() ); 719 $preset_by_slug = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] ); 720 foreach ( $preset['classes'] as $class ) { 721 foreach ( $preset_by_slug as $slug => $value ) { 862 foreach ( self::PRESETS_METADATA as $preset_metadata ) { 863 $slugs = self::get_settings_slugs( $settings, $preset_metadata, $origins ); 864 foreach ( $preset_metadata['classes'] as $class => $property ) { 865 foreach ( $slugs as $slug ) { 866 $css_var = self::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); 867 $class_name = self::replace_slug_in_string( $class, $slug ); 722 868 $stylesheet .= self::to_ruleset( 723 self::append_to_selector( $selector, '.has-' . _wp_to_kebab_case( $slug ) . '-' . $class['class_suffix']),869 self::append_to_selector( $selector, $class_name ), 724 870 array( 725 871 array( 726 'name' => $ class['property_name'],727 'value' => 'var( --wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case( $slug ). ') !important',872 'name' => $property, 873 'value' => 'var(' . $css_var . ') !important', 728 874 ), 729 875 ) … … 737 883 738 884 /** 885 * Function that scopes a selector with another one. This works a bit like 886 * SCSS nesting except the `&` operator isn't supported. 887 * 888 * <code> 889 * $scope = '.a, .b .c'; 890 * $selector = '> .x, .y'; 891 * $merged = scope_selector( $scope, $selector ); 892 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' 893 * </code> 894 * 895 * @since 5.9.0 896 * 897 * @param string $scope Selector to scope to. 898 * @param string $selector Original selector. 899 * 900 * @return string Scoped selector. 901 */ 902 private static function scope_selector( $scope, $selector ) { 903 $scopes = explode( ',', $scope ); 904 $selectors = explode( ',', $selector ); 905 906 $selectors_scoped = array(); 907 foreach ( $scopes as $outer ) { 908 foreach ( $selectors as $inner ) { 909 $selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner ); 910 } 911 } 912 913 return implode( ', ', $selectors_scoped ); 914 } 915 916 /** 917 * Gets preset values keyed by slugs based on settings and metadata. 918 * 919 * <code> 920 * $settings = array( 921 * 'typography' => array( 922 * 'fontFamilies' => array( 923 * array( 924 * 'slug' => 'sansSerif', 925 * 'fontFamily' => '"Helvetica Neue", sans-serif', 926 * ), 927 * array( 928 * 'slug' => 'serif', 929 * 'colors' => 'Georgia, serif', 930 * ) 931 * ), 932 * ), 933 * ); 934 * $meta = array( 935 * 'path' => array( 'typography', 'fontFamilies' ), 936 * 'value_key' => 'fontFamily', 937 * ); 938 * $values_by_slug = get_settings_values_by_slug(); 939 * // $values_by_slug === array( 940 * // 'sans-serif' => '"Helvetica Neue", sans-serif', 941 * // 'serif' => 'Georgia, serif', 942 * // ); 943 * </code> 944 * 945 * @since 5.9.0 946 * 947 * @param array $settings Settings to process. 948 * @param array $preset_metadata One of the PRESETS_METADATA values. 949 * @param array $origins List of origins to process. 950 * @return array Array of presets where each key is a slug and each value is the preset value. 951 */ 952 private static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) { 953 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); 954 955 $result = array(); 956 foreach ( $origins as $origin ) { 957 if ( ! isset( $preset_per_origin[ $origin ] ) ) { 958 continue; 959 } 960 foreach ( $preset_per_origin[ $origin ] as $preset ) { 961 $slug = _wp_to_kebab_case( $preset['slug'] ); 962 963 $value = ''; 964 if ( isset( $preset_metadata['value_key'] ) ) { 965 $value_key = $preset_metadata['value_key']; 966 $value = $preset[ $value_key ]; 967 } elseif ( 968 isset( $preset_metadata['value_func'] ) && 969 is_callable( $preset_metadata['value_func'] ) 970 ) { 971 $value_func = $preset_metadata['value_func']; 972 $value = call_user_func( $value_func, $preset ); 973 } else { 974 // If we don't have a value, then don't add it to the result. 975 continue; 976 } 977 978 $result[ $slug ] = $value; 979 } 980 } 981 return $result; 982 } 983 984 /** 985 * Similar to get_settings_values_by_slug, but doesn't compute the value. 986 * 987 * @since 5.9.0 988 * 989 * @param array $settings Settings to process. 990 * @param array $preset_metadata One of the PRESETS_METADATA values. 991 * @param array $origins List of origins to process. 992 * @return array Array of presets where the key and value are both the slug. 993 */ 994 private static function get_settings_slugs( $settings, $preset_metadata, $origins = self::VALID_ORIGINS ) { 995 $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); 996 997 $result = array(); 998 foreach ( $origins as $origin ) { 999 if ( ! isset( $preset_per_origin[ $origin ] ) ) { 1000 continue; 1001 } 1002 foreach ( $preset_per_origin[ $origin ] as $preset ) { 1003 $slug = _wp_to_kebab_case( $preset['slug'] ); 1004 1005 // Use the array as a set so we don't get duplicates. 1006 $result[ $slug ] = $slug; 1007 } 1008 } 1009 return $result; 1010 } 1011 1012 /** 1013 * Transform a slug into a CSS Custom Property. 1014 * 1015 * @since 5.9.0 1016 * 1017 * @param string $input String to replace. 1018 * @param string $slug The slug value to use to generate the custom property. 1019 * @return string The CSS Custom Property. Something along the lines of --wp--preset--color--black. 1020 */ 1021 private static function replace_slug_in_string( $input, $slug ) { 1022 return strtr( $input, array( '$slug' => $slug ) ); 1023 } 1024 1025 /** 739 1026 * Given the block settings, it extracts the CSS Custom Properties 740 1027 * for the presets and adds them to the $declarations array … … 749 1036 * 750 1037 * @param array $settings Settings to process. 1038 * @param array $origins List of origins to process. 751 1039 * @return array Returns the modified $declarations. 752 1040 */ 753 private static function compute_preset_vars( $settings ) {1041 private static function compute_preset_vars( $settings, $origins ) { 754 1042 $declarations = array(); 755 foreach ( self::PRESETS_METADATA as $preset ) { 756 $preset_per_origin = _wp_array_get( $settings, $preset['path'], array() ); 757 $preset_by_slug = self::get_merged_preset_by_slug( $preset_per_origin, $preset['value_key'] ); 758 foreach ( $preset_by_slug as $slug => $value ) { 1043 foreach ( self::PRESETS_METADATA as $preset_metadata ) { 1044 $values_by_slug = self::get_settings_values_by_slug( $settings, $preset_metadata, $origins ); 1045 foreach ( $values_by_slug as $slug => $value ) { 759 1046 $declarations[] = array( 760 'name' => '--wp--preset--' . $preset['css_var_infix'] . '--' . _wp_to_kebab_case($slug ),1047 'name' => self::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), 761 1048 'value' => $value, 762 1049 ); … … 865 1152 * 866 1153 * @since 5.8.0 1154 * @since 5.9.0 Added theme setting and properties parameters. 867 1155 * 868 1156 * @param array $styles Styles to process. 1157 * @param array $settings Theme settings. 1158 * @param array $properties Properties metadata. 869 1159 * @return array Returns the modified $declarations. 870 1160 */ 871 private static function compute_style_properties( $styles ) {1161 private static function compute_style_properties( $styles, $settings = array(), $properties = self::PROPERTIES_METADATA ) { 872 1162 $declarations = array(); 873 1163 if ( empty( $styles ) ) { … … 875 1165 } 876 1166 877 $properties = array(); 878 foreach ( self::PROPERTIES_METADATA as $name => $metadata ) { 879 /* 880 * Some properties can be shorthand properties, meaning that 881 * they contain multiple values instead of a single one. 882 * An example of this is the padding property. 883 */ 884 if ( self::has_properties( $metadata ) ) { 885 foreach ( $metadata['properties'] as $property ) { 886 $properties[] = array( 887 'name' => $name . '-' . $property, 888 'value' => array_merge( $metadata['value'], array( $property ) ), 889 ); 1167 foreach ( $properties as $css_property => $value_path ) { 1168 $value = self::get_property_value( $styles, $value_path ); 1169 1170 // Look up protected properties, keyed by value path. 1171 // Skip protected properties that are explicitly set to `null`. 1172 if ( is_array( $value_path ) ) { 1173 $path_string = implode( '.', $value_path ); 1174 if ( 1175 array_key_exists( $path_string, self::PROTECTED_PROPERTIES ) && 1176 _wp_array_get( $settings, self::PROTECTED_PROPERTIES[ $path_string ], null ) === null 1177 ) { 1178 continue; 890 1179 } 891 } else { 892 $properties[] = array( 893 'name' => $name, 894 'value' => $metadata['value'], 895 ); 896 } 897 } 898 899 foreach ( $properties as $prop ) { 900 $value = self::get_property_value( $styles, $prop['value'] ); 901 if ( empty( $value ) ) { 1180 } 1181 1182 // Skip if empty and not "0" or value represents array of longhand values. 1183 $has_missing_value = empty( $value ) && ! is_numeric( $value ); 1184 if ( $has_missing_value || is_array( $value ) ) { 902 1185 continue; 903 1186 } 904 1187 905 1188 $declarations[] = array( 906 'name' => $ prop['name'],1189 'name' => $css_property, 907 1190 'value' => $value, 908 1191 ); … … 910 1193 911 1194 return $declarations; 912 }913 914 /**915 * Whether the metadata contains a key named properties.916 *917 * @since 5.8.0918 *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 1195 } 929 1196 … … 936 1203 * 937 1204 * @since 5.8.0 1205 * @since 5.9.0 Consider $value that are arrays as well. 938 1206 * 939 1207 * @param array $styles Styles subtree. … … 944 1212 $value = _wp_array_get( $styles, $path, '' ); 945 1213 946 if ( '' === $value ) {1214 if ( '' === $value || is_array( $value ) ) { 947 1215 return $value; 948 1216 } … … 1016 1284 } 1017 1285 1018 1019 1286 /** 1020 1287 * Builds metadata for the style nodes, which returns in the form of: … … 1023 1290 * [ 1024 1291 * 'path' => [ 'path', 'to', 'some', 'node' ], 1025 * 'selector' => 'CSS selector for some node' 1292 * 'selector' => 'CSS selector for some node', 1293 * 'duotone' => 'CSS selector for duotone for some node' 1026 1294 * ], 1027 1295 * [ 1028 1296 * 'path' => ['path', 'to', 'other', 'node' ], 1029 * 'selector' => 'CSS selector for other node' 1297 * 'selector' => 'CSS selector for other node', 1298 * 'duotone' => null 1030 1299 * ], 1031 1300 * ] … … 1069 1338 } 1070 1339 1340 $duotone_selector = null; 1341 if ( isset( $selectors[ $name ]['duotone'] ) ) { 1342 $duotone_selector = $selectors[ $name ]['duotone']; 1343 } 1344 1071 1345 $nodes[] = array( 1072 1346 'path' => array( 'styles', 'blocks', $name ), 1073 1347 'selector' => $selector, 1348 'duotone' => $duotone_selector, 1074 1349 ); 1075 1350 … … 1091 1366 * 1092 1367 * @since 5.8.0 1368 * @since 5.9.0 Duotone preset also has origins. 1093 1369 * 1094 1370 * @param WP_Theme_JSON $incoming Data to merge. … … 1099 1375 1100 1376 /* 1101 * The array_replace_recursive ()algorithm merges at the leaf level.1377 * The array_replace_recursive algorithm merges at the leaf level. 1102 1378 * For leaf values that are arrays it will use the numeric indexes for replacement. 1103 1379 * In those cases, we want to replace the existing with the incoming value, if it exists. … … 1105 1381 $to_replace = array(); 1106 1382 $to_replace[] = array( 'spacing', 'units' ); 1107 $to_replace[] = array( 'color', 'duotone' );1108 1383 foreach ( self::VALID_ORIGINS as $origin ) { 1384 $to_replace[] = array( 'color', 'duotone', $origin ); 1109 1385 $to_replace[] = array( 'color', 'palette', $origin ); 1110 1386 $to_replace[] = array( 'color', 'gradients', $origin ); … … 1123 1399 } 1124 1400 } 1401 1402 } 1403 1404 /** 1405 * Removes insecure data from theme.json. 1406 * 1407 * @since 5.9.0 1408 * 1409 * @param array $theme_json Structure to sanitize. 1410 * @return array Sanitized structure. 1411 */ 1412 public static function remove_insecure_properties( $theme_json ) { 1413 $sanitized = array(); 1414 1415 $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); 1416 1417 $valid_block_names = array_keys( self::get_blocks_metadata() ); 1418 $valid_element_names = array_keys( self::ELEMENTS ); 1419 $theme_json = self::sanitize( $theme_json, $valid_block_names, $valid_element_names ); 1420 1421 $blocks_metadata = self::get_blocks_metadata(); 1422 $style_nodes = self::get_style_nodes( $theme_json, $blocks_metadata ); 1423 foreach ( $style_nodes as $metadata ) { 1424 $input = _wp_array_get( $theme_json, $metadata['path'], array() ); 1425 if ( empty( $input ) ) { 1426 continue; 1427 } 1428 1429 $output = self::remove_insecure_styles( $input ); 1430 if ( ! empty( $output ) ) { 1431 _wp_array_set( $sanitized, $metadata['path'], $output ); 1432 } 1433 } 1434 1435 $setting_nodes = self::get_setting_nodes( $theme_json ); 1436 foreach ( $setting_nodes as $metadata ) { 1437 $input = _wp_array_get( $theme_json, $metadata['path'], array() ); 1438 if ( empty( $input ) ) { 1439 continue; 1440 } 1441 1442 $output = self::remove_insecure_settings( $input ); 1443 if ( ! empty( $output ) ) { 1444 _wp_array_set( $sanitized, $metadata['path'], $output ); 1445 } 1446 } 1447 1448 if ( empty( $sanitized['styles'] ) ) { 1449 unset( $theme_json['styles'] ); 1450 } else { 1451 $theme_json['styles'] = $sanitized['styles']; 1452 } 1453 1454 if ( empty( $sanitized['settings'] ) ) { 1455 unset( $theme_json['settings'] ); 1456 } else { 1457 $theme_json['settings'] = $sanitized['settings']; 1458 } 1459 1460 return $theme_json; 1461 } 1462 1463 /** 1464 * Processes a setting node and returns the same node 1465 * without the insecure settings. 1466 * 1467 * @since 5.9.0 1468 * 1469 * @param array $input Node to process. 1470 * @return array 1471 */ 1472 private static function remove_insecure_settings( $input ) { 1473 $output = array(); 1474 foreach ( self::PRESETS_METADATA as $preset_metadata ) { 1475 $presets = _wp_array_get( $input, $preset_metadata['path'], null ); 1476 if ( null === $presets ) { 1477 continue; 1478 } 1479 1480 $escaped_preset = array(); 1481 foreach ( $presets as $preset ) { 1482 if ( 1483 esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && 1484 sanitize_html_class( $preset['slug'] ) === $preset['slug'] 1485 ) { 1486 $value = null; 1487 if ( isset( $preset_metadata['value_key'] ) ) { 1488 $value = $preset[ $preset_metadata['value_key'] ]; 1489 } elseif ( 1490 isset( $preset_metadata['value_func'] ) && 1491 is_callable( $preset_metadata['value_func'] ) 1492 ) { 1493 $value = call_user_func( $preset_metadata['value_func'], $preset ); 1494 } 1495 1496 $preset_is_valid = true; 1497 foreach ( $preset_metadata['properties'] as $property ) { 1498 if ( ! self::is_safe_css_declaration( $property, $value ) ) { 1499 $preset_is_valid = false; 1500 break; 1501 } 1502 } 1503 1504 if ( $preset_is_valid ) { 1505 $escaped_preset[] = $preset; 1506 } 1507 } 1508 } 1509 1510 if ( ! empty( $escaped_preset ) ) { 1511 _wp_array_set( $output, $preset_metadata['path'], $escaped_preset ); 1512 } 1513 } 1514 1515 return $output; 1516 } 1517 1518 /** 1519 * Processes a style node and returns the same node 1520 * without the insecure styles. 1521 * 1522 * @since 5.9.0 1523 * 1524 * @param array $input Node to process. 1525 * @return array 1526 */ 1527 private static function remove_insecure_styles( $input ) { 1528 $output = array(); 1529 $declarations = self::compute_style_properties( $input ); 1530 1531 foreach ( $declarations as $declaration ) { 1532 if ( self::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { 1533 $path = self::PROPERTIES_METADATA[ $declaration['name'] ]; 1534 1535 // Check the value isn't an array before adding so as to not 1536 // double up shorthand and longhand styles. 1537 $value = _wp_array_get( $input, $path, array() ); 1538 if ( ! is_array( $value ) ) { 1539 _wp_array_set( $output, $path, $value ); 1540 } 1541 } 1542 } 1543 return $output; 1544 } 1545 1546 /** 1547 * Checks that a declaration provided by the user is safe. 1548 * 1549 * @since 5.9.0 1550 * 1551 * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. 1552 * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. 1553 * @return boolean 1554 */ 1555 private static function is_safe_css_declaration( $property_name, $property_value ) { 1556 $style_to_validate = $property_name . ': ' . $property_value; 1557 $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); 1558 return ! empty( trim( $filtered ) ); 1125 1559 } 1126 1560 … … 1177 1611 $theme_settings['settings']['typography'] = array(); 1178 1612 } 1179 $theme_settings['settings']['typography'][' customLineHeight'] = $settings['enableCustomLineHeight'];1613 $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight']; 1180 1614 } 1181 1615 … … 1221 1655 $theme_settings['settings']['spacing'] = array(); 1222 1656 } 1223 $theme_settings['settings']['spacing'][' customPadding'] = $settings['enableCustomSpacing'];1657 $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; 1224 1658 } 1225 1659
Note: See TracChangeset
for help on using the changeset viewer.