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

Add block nesting governance for theme.json #43796

Closed
wants to merge 61 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
e6b6e7d
Add hardcoded nesting rules for core/heading
alecgeatches Aug 22, 2022
ef5a3be
Mock block editor store, add basic nesting support
alecgeatches Aug 23, 2022
1629ce6
Move settings mock from JS to base PHP config
alecgeatches Aug 23, 2022
108e7e4
Merge pull request #1 from alecgeatches/add/hardcoded-nesting-rules
alecgeatches Aug 24, 2022
2a5744c
Add useSetting tests and mocking utilities
alecgeatches Aug 25, 2022
b663641
Remove "no tests" comment
alecgeatches Aug 25, 2022
67e1826
Merge branch 'WordPress:trunk' into trunk
alecgeatches Aug 25, 2022
84bcf5e
Move const definitions, remove lodash dependency
alecgeatches Aug 25, 2022
9a1b0c4
Fix test break from hardcoded PHP settings
alecgeatches Aug 25, 2022
51aa292
Merge pull request #2 from alecgeatches/add/use-settings-tests
alecgeatches Aug 25, 2022
b3f95d5
Add deep nested settings support and specificity
alecgeatches Aug 25, 2022
9763316
Merge branch 'WordPress:trunk' into trunk
alecgeatches Aug 26, 2022
a4b8b6e
Add nested block support in the settings
ingeniumed Aug 26, 2022
38ca848
Clean up the code based on linting
ingeniumed Aug 26, 2022
29d8aff
Fix the merge conflicts
ingeniumed Aug 26, 2022
58ebe2b
Fix tests to mock multiple parents correctly
alecgeatches Aug 26, 2022
ce939f1
Merge branch 'WordPress:trunk' into trunk
alecgeatches Aug 29, 2022
375721a
Merge pull request #3 from alecgeatches/add/use-setting-specificity
alecgeatches Aug 29, 2022
7dff721
Bake in nested paths support for getting the settings back, so that n…
ingeniumed Aug 29, 2022
71e66c2
Merge pull request #4 from alecgeatches/add/nested-settings-block-sup…
ingeniumed Aug 29, 2022
4a01365
Switch the hardcoded search for a slash to instead use the valid bloc…
ingeniumed Aug 29, 2022
e4d7763
Add the css selector for sub blocks
ingeniumed Aug 30, 2022
6c3f1ba
Merge branch 'WordPress:trunk' into trunk
alecgeatches Aug 30, 2022
a7dd80b
Add in support for nested blocks at any depth in the get_setting_node…
ingeniumed Aug 30, 2022
8f86f4f
Add supported for nested blocks at any depth
ingeniumed Aug 30, 2022
ec42442
Fix the key being 0 instead of the custom block name bug
ingeniumed Aug 30, 2022
a9ce06e
Clean up the code linting and add in some more comments
ingeniumed Aug 30, 2022
0b957fa
Add support for nested block settings in the get_settings_node function
ingeniumed Aug 31, 2022
016e5df
Fix linting errors
ingeniumed Aug 31, 2022
1c16d9d
Merge branch 'trunk' of github.com:alecgeatches/gutenberg into add/su…
ingeniumed Aug 31, 2022
70da472
Fix bug where the properties outside the blocks were being ignored
ingeniumed Aug 31, 2022
e4ba002
Merge pull request #5 from alecgeatches/add/support-multi-depth-blocks
ingeniumed Aug 31, 2022
842bab1
Add failing test for double-layered specificity
alecgeatches Aug 31, 2022
aa4b601
Clean the code up in order to make it presentable for a PR
ingeniumed Aug 31, 2022
2ba3b3c
Refactor nested search into separate function
alecgeatches Aug 31, 2022
162578f
Add a new unit test for nested block settings that ensures the parsin…
ingeniumed Aug 31, 2022
0222a62
Clean up the code based on the code review, and make the theme.json m…
ingeniumed Aug 31, 2022
8a4f235
Merge pull request #8 from alecgeatches/add/cleanup-code-for-nested-s…
ingeniumed Aug 31, 2022
42bdbbb
Merge branch 'WordPress:trunk' into trunk
alecgeatches Sep 1, 2022
cbbd070
Refactor getNestedSetting, add tests
alecgeatches Sep 1, 2022
49d261c
Remove Map usage, use object for destructuring
alecgeatches Sep 1, 2022
49311eb
Add new e2e test that loads a nested block within a new post, to test…
ingeniumed Sep 1, 2022
0dbf2ce
Add doc string, additional test for same depth
alecgeatches Sep 1, 2022
288316d
Fix getNestedSetting doc return type and comment
alecgeatches Sep 1, 2022
8bedc17
Merge branch 'trunk' into fix/use-setting-double-nesting
alecgeatches Sep 1, 2022
bd90b13
Merge pull request #7 from alecgeatches/fix/use-setting-double-nesting
ingeniumed Sep 1, 2022
d846dad
Verify the color of the nested block to ensure it's correct according…
ingeniumed Sep 1, 2022
e0771eb
Update the test description
ingeniumed Sep 1, 2022
56fde5e
Merge pull request #9 from alecgeatches/add/end-to-end-test-for-neste…
ingeniumed Sep 1, 2022
86ec63b
Merge branch 'WordPress:trunk' into trunk
alecgeatches Sep 2, 2022
d68f379
Clean up the code a bit more, reduce unnecesary lines and update the …
ingeniumed Sep 2, 2022
d16ecdf
Clean up the code a bit more, reduce unnecesary lines and update the …
ingeniumed Sep 2, 2022
e93a6d9
Minor correction to the comments
ingeniumed Sep 2, 2022
a3313ef
Merge pull request #10 from alecgeatches/add/final-touches-to-doc-and…
ingeniumed Sep 2, 2022
7fe4592
Remove the since tags from each method, to avoid being wrong about th…
ingeniumed Sep 2, 2022
833a596
Merge pull request #13 from alecgeatches/update/comments-in-functipon
alecgeatches Sep 2, 2022
549183b
Merge branch 'WordPress:trunk' into trunk
alecgeatches Sep 2, 2022
bb422fe
Merge branch 'WordPress:trunk' into trunk
alecgeatches Sep 6, 2022
ccfae21
Merge branch 'WordPress:trunk' into trunk
alecgeatches Sep 12, 2022
5d47061
Merge branch 'WordPress:trunk' into trunk
alecgeatches Sep 12, 2022
06f84cf
Merge branch 'WordPress:trunk' into trunk
alecgeatches Sep 19, 2022
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
206 changes: 206 additions & 0 deletions lib/experimental/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,210 @@ class WP_Theme_JSON_Gutenberg extends WP_Theme_JSON_6_1 {
* @var array
*/
protected static $blocks_metadata = null;

/**
* 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 ) {
if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $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();
foreach ( $valid_block_names as $block ) {
$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;

// 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;
}

// First clean up everything, save for the blocks.
$result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );

// Then, clean up blocks and append that to the result.
if ( 'settings' === $subtree && isset( $input[ $subtree ]['blocks'] ) ) {
$result['blocks'] = static::sanitize_blocks( $input[ $subtree ]['blocks'], $valid_block_names, $schema[ $subtree ] );
}

if ( empty( $result ) ) {
unset( $output[ $subtree ] );
} else {
$output[ $subtree ] = $result;
}
}

return $output;
}

/**
* Sanitize the blocks section that can be found in settings and styles, including nested blocks.
*
* @param array $current_block The current block to break down.
* @param array $valid_block_names List of valid block names.
* @param array $schema Valid schema that is allowed for this block.
* @param array $result The sanitized version of the block.
* @return array The sanitized blocks output
*/
protected static function sanitize_blocks( $current_block, $valid_block_names, $schema, $result = array() ) {
foreach ( $current_block as $block_name => $block ) {
if ( in_array( $block_name, $valid_block_names, true ) ) {
$base_result = static::remove_keys_not_in_schema( $block, $schema );
$sub_result = static::sanitize_blocks( $block, $valid_block_names, $schema );

$result[ $block_name ] = array_merge( $base_result, $sub_result );
}
}

return $result;
}

/**
* Builds metadata for the setting nodes, which returns in the form of:
*
* [
* [
* 'path' => ['path', 'to', 'some', 'node' ],
* 'selector' => 'CSS selector for some node'
* ],
* [
* 'path' => [ 'path', 'to', 'other', 'node' ],
* 'selector' => 'CSS selector for other node'
* ],
* ]
*
* @since 5.8.0
*
* @param array $theme_json The tree to extract setting nodes from.
* @param array $selectors List of selectors per block.
* @return array
*/
protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
$nodes = array();
if ( ! isset( $theme_json['settings'] ) ) {
return $nodes;
}

// Top-level.
$nodes[] = array(
'path' => array( 'settings' ),
'selector' => static::ROOT_BLOCK_SELECTOR,
);

// Calculate paths for blocks.
if ( ! isset( $theme_json['settings']['blocks'] ) ) {
return $nodes;
}

$valid_block_names = array_keys( static::get_blocks_metadata() );

return static::get_settings_of_blocks( $selectors, $valid_block_names, $nodes, $theme_json['settings']['blocks'] );
}

/**
* Builds the metadata for settings.blocks, whilst ensuring support for nested blocks. This returns in the form of:
*
* [
* [
* 'path' => ['path', 'to', 'some', 'node' ],
* 'selector' => 'CSS selector for some node'
* ],
* [
* 'path' => [ 'path', 'to', 'other', 'node' ],
* 'selector' => 'CSS selector for other node'
* ],
* ]
*
* @param array $selectors List of selectors per block.
* @param array $valid_block_names List of valid block names.
* @param array $nodes The metadata of the nodes that have been built so far.
* @param array $current_block The current block to break down.
* @param array $current_selector The current selector of the current block.
* @param array $current_path The current path to the block.
* @return array
*/
protected static function get_settings_of_blocks( $selectors, $valid_block_names, $nodes, $current_block, $current_selector = null, $current_path = array() ) {
foreach ( $current_block as $block_name => $block ) {
if ( in_array( $block_name, $valid_block_names, true ) ) {

$selector = is_null( $current_selector ) ? null : $current_selector;
if ( isset( $selectors[ $block_name ]['selector'] ) ) {
$selector = $selector . ' ' . $selectors[ $block_name ]['selector'];
}

$path = empty( $current_path ) ? array( 'settings', 'blocks' ) : $current_path;
array_push( $path, $block_name );

$nodes[] = array(
'path' => $path,
'selector' => $selector,
);

$nodes = static::get_settings_of_blocks( $selectors, $valid_block_names, $nodes, $block, $selector, $path );
}
}

return $nodes;
}

}
95 changes: 91 additions & 4 deletions packages/block-editor/src/components/use-setting/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,69 @@ const removeCustomPrefixes = ( path ) => {
return prefixedFlags[ path ] || path;
};

/**
* Find block settings nested in other block settings.
*
* Given an array of blocks names from the top level of the editor to the
* current block (`blockNamePath`), return the value for the deepest-nested
* settings value that applies to the current block.
*
* If two setting values share the same nesting depth, use the last one that
* occurs in settings (like CSS).
*
* @param {string[]} blockNamePath Block names representing the path to the
* current block from the top level of the
* block editor.
* @param {string} normalizedPath Path to the setting being retrieved.
* @param {Object} settings Object containing all block settings.
* @param {Object} result Optional. Object with keys `depth` and
* `value` used to track current most-nested
* setting.
* @param {number} depth Optional. The current recursion depth used
* to calculate the most-nested setting.
* @return {Object} Object with keys `depth` and `value`.
* Destructure the `value` key for the result.
*/
const getNestedSetting = (
blockNamePath,
normalizedPath,
settings,
result = { depth: 0, value: undefined },
depth = 1
) => {
const [ currentBlockName, ...remainingBlockNames ] = blockNamePath;
const blockSettings = settings[ currentBlockName ];

if ( remainingBlockNames.length === 0 ) {
const settingValue = get( blockSettings, normalizedPath );

if ( settingValue !== undefined && depth >= result.depth ) {
result.depth = depth;
result.value = settingValue;
}

return result;
} else if ( blockSettings !== undefined ) {
// Recurse into the parent block's settings
result = getNestedSetting(
remainingBlockNames,
normalizedPath,
blockSettings,
result,
depth + 1
);
}

// Continue down the array of blocks
return getNestedSetting(
remainingBlockNames,
normalizedPath,
settings,
result,
depth
);
};

/**
* Hook that retrieves the given setting for the block instance in use.
*
Expand Down Expand Up @@ -121,10 +184,12 @@ export default function useSetting( path ) {

let result;
const normalizedPath = removeCustomPrefixes( path );
const blockParentIds =
select( blockEditorStore ).getBlockParents( clientId );

// 1. Take settings from the block instance or its ancestors.
const candidates = [
...select( blockEditorStore ).getBlockParents( clientId ),
...blockParentIds,
clientId, // The current block is added last, so it overwrites any ancestor.
];
candidates.forEach( ( candidateClientId ) => {
Expand Down Expand Up @@ -157,11 +222,33 @@ export default function useSetting( path ) {

// 2. Fall back to the settings from the block editor store (__experimentalFeatures).
const settings = select( blockEditorStore ).getSettings();

// 2.1 Check for block-specific settings from block editor store
if ( result === undefined && blockName !== '' ) {
const blockNamePath = [
...blockParentIds.map( ( parentId ) =>
select( blockEditorStore ).getBlockName( parentId )
),
blockName,
];

const blockSettings = get(
settings,
'__experimentalFeatures.blocks',
{}
);

( { value: result } = getNestedSetting(
blockNamePath,
normalizedPath,
blockSettings
) );
}

// 2.2 Default to top-level settings from the block editor store
if ( result === undefined ) {
const defaultsPath = `__experimentalFeatures.${ normalizedPath }`;
const blockPath = `__experimentalFeatures.blocks.${ blockName }.${ normalizedPath }`;
result =
get( settings, blockPath ) ?? get( settings, defaultsPath );
result = get( settings, defaultsPath );
}

// Return if the setting was found in either the block instance or the store.
Expand Down
Loading