Skip to content
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

Support pseudo selectors on elements in theme json #41786

Merged
merged 9 commits into from
Jun 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 239 additions & 7 deletions lib/compat/wordpress-6.1/class-wp-theme-json-6-1.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
getdave marked this conversation as resolved.
Show resolved Hide resolved
}

$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 ] );
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
*
Expand Down Expand Up @@ -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,
);
}
}
}
}
}

Expand Down Expand Up @@ -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.
Show resolved Hide 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,
);
}
}
}
}
}
}
Expand All @@ -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.
Expand All @@ -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 );
Expand Down
5 changes: 5 additions & 0 deletions lib/compat/wordpress-6.1/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@
"fontSize": "1.125em",
"textDecoration": "none"
}
},
"link": {
"typography": {
"textDecoration": "underline"
}
}
},
"spacing": { "blockGap": "24px" }
Expand Down
Loading