-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Support pseudo selectors on elements in theme json
#41786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
42f688c
4b6f4cb
4dc636d
1e7b946
5c61dcd
bf0a1c3
cd6ab64
13842c6
82f5a37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,16 @@ | |
| * @access private | ||
| */ | ||
| class WP_Theme_JSON_6_1 extends WP_Theme_JSON_6_0 { | ||
|
|
||
| /** | ||
| * Whitelist which defines which pseudo selectors are enabled for | ||
| * which elements. | ||
| * Note: this will effect both top level and block level elements. | ||
| */ | ||
| const VALID_ELEMENT_PSEUDO_SELECTORS = array( | ||
| 'link' => array( ':hover', ':focus', ':active' ), | ||
| ); | ||
|
|
||
| const ELEMENTS = array( | ||
| 'link' => 'a', | ||
| 'h1' => 'h1', | ||
|
|
@@ -43,6 +53,176 @@ public static function get_element_class_name( $element ) { | |
| return array_key_exists( $element, static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES ) ? static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] : ''; | ||
| } | ||
|
|
||
| /** | ||
| * Sanitizes the input according to the schemas. | ||
| * | ||
| * @since 5.8.0 | ||
| * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters. | ||
| * | ||
| * @param array $input Structure to sanitize. | ||
| * @param array $valid_block_names List of valid block names. | ||
| * @param array $valid_element_names List of valid element names. | ||
| * @return array The sanitized output. | ||
| */ | ||
| protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { | ||
|
|
||
| $output = array(); | ||
|
|
||
| if ( ! is_array( $input ) ) { | ||
| return $output; | ||
| } | ||
|
|
||
| // Preserve only the top most level keys. | ||
| $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); | ||
|
|
||
| // Remove any rules that are annotated as "top" in VALID_STYLES constant. | ||
| // Some styles are only meant to be available at the top-level (e.g.: blockGap), | ||
| // hence, the schema for blocks & elements should not have them. | ||
| $styles_non_top_level = static::VALID_STYLES; | ||
| foreach ( array_keys( $styles_non_top_level ) as $section ) { | ||
| foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { | ||
| if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { | ||
| unset( $styles_non_top_level[ $section ][ $prop ] ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Build the schema based on valid block & element names. | ||
| $schema = array(); | ||
| $schema_styles_elements = array(); | ||
|
|
||
| // Set allowed element pseudo selectors based on per element allow list. | ||
| // Target data structure in schema: | ||
| // e.g. | ||
| // - top level elements: `$schema['styles']['elements']['link'][':hover']`. | ||
| // - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. | ||
| foreach ( $valid_element_names as $element ) { | ||
| $schema_styles_elements[ $element ] = $styles_non_top_level; | ||
|
|
||
| if ( array_key_exists( $element, static::VALID_ELEMENT_PSEUDO_SELECTORS ) ) { | ||
| foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { | ||
| $schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| $schema_styles_blocks = array(); | ||
| $schema_settings_blocks = array(); | ||
| foreach ( $valid_block_names as $block ) { | ||
| $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; | ||
| $schema_styles_blocks[ $block ] = $styles_non_top_level; | ||
| $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; | ||
| } | ||
|
|
||
| $schema['styles'] = static::VALID_STYLES; | ||
| $schema['styles']['blocks'] = $schema_styles_blocks; | ||
| $schema['styles']['elements'] = $schema_styles_elements; | ||
| $schema['settings'] = static::VALID_SETTINGS; | ||
| $schema['settings']['blocks'] = $schema_settings_blocks; | ||
|
|
||
| // Remove anything that's not present in the schema. | ||
| foreach ( array( 'styles', 'settings' ) as $subtree ) { | ||
| if ( ! isset( $input[ $subtree ] ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| if ( ! is_array( $input[ $subtree ] ) ) { | ||
| unset( $output[ $subtree ] ); | ||
| continue; | ||
| } | ||
|
|
||
| $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line is why we need to update the schema to allow for pseudo selectors. |
||
|
|
||
| if ( empty( $result ) ) { | ||
| unset( $output[ $subtree ] ); | ||
| } else { | ||
| $output[ $subtree ] = $result; | ||
| } | ||
| } | ||
|
|
||
| return $output; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| /** | ||
| * Removes insecure data from theme.json. | ||
| * | ||
| * @since 5.9.0 | ||
| * | ||
| * @param array $theme_json Structure to sanitize. | ||
| * @return array Sanitized structure. | ||
| */ | ||
| public static function remove_insecure_properties( $theme_json ) { | ||
| $sanitized = array(); | ||
|
|
||
| $theme_json = WP_Theme_JSON_Schema::migrate( $theme_json ); | ||
|
|
||
| $valid_block_names = array_keys( static::get_blocks_metadata() ); | ||
| $valid_element_names = array_keys( static::ELEMENTS ); | ||
|
|
||
| $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); | ||
|
|
||
| $blocks_metadata = static::get_blocks_metadata(); | ||
| $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); | ||
|
|
||
| foreach ( $style_nodes as $metadata ) { | ||
| $input = _wp_array_get( $theme_json, $metadata['path'], array() ); | ||
| if ( empty( $input ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| $output = static::remove_insecure_styles( $input ); | ||
|
|
||
| // Get a reference to element name from path. | ||
| // $metadata['path'] = array('styles','elements','link');. | ||
| $current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ]; | ||
|
|
||
| // $output is stripped of pseudo selectors. Readd and process them | ||
| // for insecure styles here. | ||
| if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) { | ||
|
|
||
| foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) { | ||
| if ( isset( $input[ $pseudo_selector ] ) ) { | ||
| $output[ $pseudo_selector ] = static::remove_insecure_styles( $input[ $pseudo_selector ] ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if ( ! empty( $output ) ) { | ||
| _wp_array_set( $sanitized, $metadata['path'], $output ); | ||
| } | ||
| } | ||
|
|
||
| $setting_nodes = static::get_setting_nodes( $theme_json ); | ||
| foreach ( $setting_nodes as $metadata ) { | ||
| $input = _wp_array_get( $theme_json, $metadata['path'], array() ); | ||
| if ( empty( $input ) ) { | ||
| continue; | ||
| } | ||
|
|
||
| $output = static::remove_insecure_settings( $input ); | ||
| if ( ! empty( $output ) ) { | ||
| _wp_array_set( $sanitized, $metadata['path'], $output ); | ||
| } | ||
| } | ||
|
|
||
| if ( empty( $sanitized['styles'] ) ) { | ||
| unset( $theme_json['styles'] ); | ||
| } else { | ||
| $theme_json['styles'] = $sanitized['styles']; | ||
| } | ||
|
|
||
| if ( empty( $sanitized['settings'] ) ) { | ||
| unset( $theme_json['settings'] ); | ||
| } else { | ||
| $theme_json['settings'] = $sanitized['settings']; | ||
| } | ||
|
|
||
| return $theme_json; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Returns the metadata for each block. | ||
| * | ||
|
|
@@ -156,11 +336,27 @@ protected static function get_style_nodes( $theme_json, $selectors = array() ) { | |
| ); | ||
|
|
||
| if ( isset( $theme_json['styles']['elements'] ) ) { | ||
|
|
||
| foreach ( $theme_json['styles']['elements'] as $element => $node ) { | ||
|
|
||
| // Handle element defaults. | ||
| $nodes[] = array( | ||
| 'path' => array( 'styles', 'elements', $element ), | ||
| 'selector' => static::ELEMENTS[ $element ], | ||
| ); | ||
|
|
||
| // Handle any pseudo selectors for the element. | ||
| if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { | ||
| foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { | ||
|
|
||
| if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) { | ||
| $nodes[] = array( | ||
| 'path' => array( 'styles', 'elements', $element ), | ||
| 'selector' => static::ELEMENTS[ $element ] . $pseudo_selector, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -228,6 +424,18 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { | |
| 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), | ||
| 'selector' => $selectors[ $name ]['elements'][ $element ], | ||
| ); | ||
|
|
||
| // Handle any psuedo selectors for the element. | ||
| if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { | ||
|
getdave marked this conversation as resolved.
|
||
| foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { | ||
| if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { | ||
| $nodes[] = array( | ||
| 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), | ||
| 'selector' => $selectors[ $name ]['elements'][ $element ] . $pseudo_selector, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -243,11 +451,35 @@ private static function get_block_nodes( $theme_json, $selectors = array() ) { | |
| * @return string Styles for the block. | ||
| */ | ||
| public function get_styles_for_block( $block_metadata ) { | ||
| $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); | ||
| $selector = $block_metadata['selector']; | ||
| $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); | ||
| $declarations = static::compute_style_properties( $node, $settings ); | ||
| $block_rules = ''; | ||
|
|
||
| $node = _wp_array_get( $this->theme_json, $block_metadata['path'], array() ); | ||
|
|
||
| $selector = $block_metadata['selector']; | ||
| $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); | ||
|
|
||
| // Attempt to parse a pseudo selector (e.g. ":hover") from the $selector ("a:hover"). | ||
| $pseudo_matches = array(); | ||
| preg_match( '/:[a-z]+/', $selector, $pseudo_matches ); | ||
| $pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null; | ||
|
|
||
| // Get a reference to element name from path. | ||
| // $block_metadata['path'] = array('styles','elements','link'); | ||
| // Make sure that $block_metadata['path'] describes an element node, like ['styles', 'element', 'link']. | ||
| // Skip non-element paths like just ['styles']. | ||
| $is_processing_element = in_array( 'elements', $block_metadata['path'], true ); | ||
|
|
||
| $current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null; | ||
|
|
||
| // If the current selector is a pseudo selector that's defined in the allow list for the current | ||
| // element then compute the style properties for it. | ||
| // Otherwise just compute the styles for the default selector as normal. | ||
| if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) && isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) && in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true ) ) { | ||
| $declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings ); | ||
| } else { | ||
| $declarations = static::compute_style_properties( $node, $settings ); | ||
| } | ||
|
|
||
| $block_rules = ''; | ||
|
|
||
| // 1. Separate the ones who use the general selector | ||
| // and the ones who use the duotone selector. | ||
|
|
@@ -271,10 +503,10 @@ public function get_styles_for_block( $block_metadata ) { | |
| $block_rules .= 'body { margin: 0; }'; | ||
| } | ||
|
|
||
| // 2. Generate the rules that use the general selector. | ||
| // 2. Generate and append the rules that use the general selector. | ||
| $block_rules .= static::to_ruleset( $selector, $declarations ); | ||
|
|
||
| // 3. Generate the rules that use the duotone selector. | ||
| // 3. Generate and append the rules that use the duotone selector. | ||
| if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { | ||
| $selector_duotone = static::scope_selector( $block_metadata['selector'], $block_metadata['duotone'] ); | ||
| $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -250,6 +250,11 @@ | |
| "fontSize": "1.125em", | ||
| "textDecoration": "none" | ||
| } | ||
| }, | ||
| "link": { | ||
| "typography": { | ||
| "textDecoration": "underline" | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you recall why we have to explicitly set the link style to "underline" in the core theme.json file? I mean the default behavior of browser is to underline links, so why we had to add this explicitly?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really can't I'm afraid. If I had to guess, I'd say that for a11y reasons Navigation links would need to be underlined and perhaps default styling at the time was not not have them underlined? It may be that things have changed and browser defaults are now allowed. Perhaps @MaggieCabrera or @mikachan would know?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't think of a reason right now! Maybe we had some reset CSS back then of some kind? Nowadays it seems like we could go without it |
||
| } | ||
| }, | ||
| "spacing": { "blockGap": "24px" } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.