diff --git a/packages/wp/Tests/BricksSynchronizationTest.php b/packages/wp/Tests/BricksSynchronizationTest.php new file mode 100644 index 0000000..1875f18 --- /dev/null +++ b/packages/wp/Tests/BricksSynchronizationTest.php @@ -0,0 +1,425 @@ +data = $data; + } + + public function get_data() { + return $this->data; + } + } +} + +if ( ! function_exists( 'get_option' ) ) { + function get_option( $option, $default = false ) { + return array_key_exists( $option, $GLOBALS['cf_test_options'] ) + ? $GLOBALS['cf_test_options'][ $option ] + : $default; + } +} + +if ( ! function_exists( 'update_option' ) ) { + function update_option( $option, $value, $autoload = null ) { + $current = get_option( $option, null ); + if ( $current === $value ) { + return false; + } + + $GLOBALS['cf_test_options'][ $option ] = $value; + ++$GLOBALS['cf_test_mutations']; + return true; + } +} + +if ( ! function_exists( 'get_post_types' ) ) { + function get_post_types( $args = array(), $output = 'names' ) { + return array( + 'page' => 'page', + 'bricks_template' => 'bricks_template', + ); + } +} + +if ( ! function_exists( 'get_posts' ) ) { + function get_posts( $args = array() ) { + $meta_keys = array_column( array_filter( $args['meta_query'] ?? array(), 'is_array' ), 'key' ); + $post_ids = array(); + + foreach ( $GLOBALS['cf_test_post_meta'] as $post_id => $post_meta ) { + if ( array_intersect( $meta_keys, array_keys( $post_meta ) ) ) { + $post_ids[] = $post_id; + } + } + + return $post_ids; + } +} + +if ( ! function_exists( 'get_post_meta' ) ) { + function get_post_meta( $post_id, $key, $single = false ) { + return $GLOBALS['cf_test_post_meta'][ $post_id ][ $key ] ?? ''; + } +} + +if ( ! function_exists( 'update_post_meta' ) ) { + function update_post_meta( $post_id, $key, $value ) { + $current = get_post_meta( $post_id, $key, true ); + if ( $current === $value ) { + return false; + } + + $GLOBALS['cf_test_post_meta'][ $post_id ][ $key ] = $value; + ++$GLOBALS['cf_test_mutations']; + return true; + } +} + +if ( ! function_exists( 'sanitize_title' ) ) { + function sanitize_title( $title ) { + $title = strtolower( trim( (string) $title ) ); + $title = preg_replace( '/[^a-z0-9_-]+/', '-', $title ); + return trim( $title, '-' ); + } +} + +if ( ! function_exists( 'wp_upload_dir' ) ) { + function wp_upload_dir() { + return array( + 'basedir' => 'C:/tmp/uploads', + 'baseurl' => 'https://example.test/uploads', + ); + } +} + +if ( ! function_exists( 'trailingslashit' ) ) { + function trailingslashit( $value ) { + return rtrim( $value, '/\\' ) . '/'; + } +} + +if ( ! function_exists( 'WP_Filesystem' ) ) { + function WP_Filesystem() { + return true; + } +} + +final class CoreFrameworkTestFilesystem { + public function get_contents( $path ) { + return $GLOBALS['cf_test_stylesheet']; + } +} + +final class CoreFrameworkTestBuilder { + public $active; + public $selectors = array(); + public $variable_refreshes = 0; + + public function __construct( bool $active ) { + $this->active = $active; + } + + public function is_bricks(): bool { + return $this->active; + } + + public function is_oxygen(): bool { + return $this->active; + } + + public function refresh_selectors( $selectors ): void { + $this->selectors[] = $selectors; + } + + public function refresh_variables(): void { + ++$this->variable_refreshes; + } +} + +if ( ! function_exists( 'CoreFrameworkBricks' ) ) { + function CoreFrameworkBricks() { + return $GLOBALS['cf_test_bricks_builder']; + } +} + +if ( ! function_exists( 'CoreFrameworkOxygen' ) ) { + function CoreFrameworkOxygen() { + return $GLOBALS['cf_test_oxygen_builder']; + } +} + +final class BricksSynchronizationTest extends TestCase { + protected function setUp(): void { + global $wp_filesystem; + + $GLOBALS['cf_test_options'] = array( + 'core_framework_main' => array( + 'bricks' => true, + 'oxygen' => false, + ), + ); + $GLOBALS['cf_test_post_meta'] = array(); + $GLOBALS['cf_test_mutations'] = 0; + $GLOBALS['cf_test_stylesheet'] = ''; + $GLOBALS['cf_test_bricks_builder'] = new CoreFrameworkTestBuilder( true ); + $GLOBALS['cf_test_oxygen_builder'] = new CoreFrameworkTestBuilder( false ); + $wp_filesystem = new CoreFrameworkTestFilesystem(); + } + + private function createBricksFunctions(): BricksFunctions { + $reflection = new ReflectionClass( BricksFunctions::class ); + return $reflection->newInstanceWithoutConstructor(); + } + + private function createRestController(): AllPoints { + $reflection = new ReflectionClass( AllPoints::class ); + return $reflection->newInstanceWithoutConstructor(); + } + + private function createRequest( array $params ) { + return new class( $params ) { + private $params; + + public function __construct( array $params ) { + $this->params = $params; + } + + public function get_param( $key ) { + return $this->params[ $key ] ?? null; + } + }; + } + + public function testEmptySelectorPayloadStillRefreshesBricksVariables(): void { + $request = $this->createRequest( + array( + 'classes' => '', + 'addonEnableArray' => array( + array( + 'addon' => 'bricks', + 'enabled' => true, + ), + ), + ) + ); + + $response = $this->createRestController()->update_classes( $request ); + + $this->assertSame( array( array() ), $GLOBALS['cf_test_bricks_builder']->selectors ); + $this->assertSame( 1, $GLOBALS['cf_test_bricks_builder']->variable_refreshes ); + $this->assertSame( + array( + 'success' => true, + 'active_builders' => array( 'bricks' ), + ), + $response->get_data() + ); + } + + public function testSelectorPayloadIsTrimmedFilteredAndDeduplicated(): void { + $request = $this->createRequest( + array( + 'classes' => array( ' padding ', '', null, 'padding', 'margin' ), + 'addonEnableArray' => array( + array( + 'addon' => 'bricks', + 'enabled' => true, + ), + ), + ) + ); + + $this->createRestController()->update_classes( $request ); + + $this->assertSame( array( array( 'padding', 'margin' ) ), $GLOBALS['cf_test_bricks_builder']->selectors ); + } + + public function testClassSynchronizationRemovesFinalCoreClassAndOnlyItsReferences(): void { + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_CLASSES_OPTION ] = array( + array( + 'id' => 'user-class', + 'name' => 'user-class', + 'settings' => array( 'color' => 'red' ), + 'category' => 'custom', + ), + array( + 'id' => 'user-owned_c', + 'name' => 'user-owned', + 'settings' => array(), + 'category' => 'custom', + ), + ); + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_LOCKED_CLASSES_OPTION ] = array( 'user-class', 'user-owned_c' ); + $GLOBALS['cf_test_post_meta'][10] = array( + '_bricks_page_content_2' => array( + array( + 'id' => 'element-one', + 'settings' => array( + '_cssGlobalClasses' => array( 'padding_c', 'user-class', 'user-owned_c' ), + '_cssClasses' => 'padding_c unrelated-class', + ), + ), + ), + '_bricks_page_settings' => array( + 'container' => array( + '_cssGlobalClasses' => 'padding_c user-class', + 'customValue' => 'padding_c', + ), + ), + 'unrelated_meta' => array( + '_cssGlobalClasses' => array( 'padding_c' ), + ), + ); + + $functions = $this->createBricksFunctions(); + $functions->refresh_selectors( array( 'padding' ) ); + + $classes = $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_CLASSES_OPTION ]; + $this->assertSame( array( 'user-class', 'user-owned', 'padding' ), array_column( $classes, 'name' ) ); + $this->assertSame( + array( 'user-class', 'user-owned_c', 'padding_c' ), + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_LOCKED_CLASSES_OPTION ] + ); + + $state_after_first_sync = array( + 'options' => $GLOBALS['cf_test_options'], + 'post_meta' => $GLOBALS['cf_test_post_meta'], + 'mutations' => $GLOBALS['cf_test_mutations'], + ); + + $functions->refresh_selectors( array( 'padding', 'padding', '' ) ); + + $this->assertSame( $state_after_first_sync['options'], $GLOBALS['cf_test_options'] ); + $this->assertSame( $state_after_first_sync['post_meta'], $GLOBALS['cf_test_post_meta'] ); + $this->assertSame( $state_after_first_sync['mutations'], $GLOBALS['cf_test_mutations'] ); + + $functions->refresh_selectors( array() ); + + $classes = $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_CLASSES_OPTION ]; + $this->assertSame( array( 'user-class', 'user-owned' ), array_column( $classes, 'name' ) ); + $this->assertSame( + array( 'user-class', 'user-owned_c' ), + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_LOCKED_CLASSES_OPTION ] + ); + $this->assertSame( + array( 'user-class', 'user-owned_c' ), + $GLOBALS['cf_test_post_meta'][10]['_bricks_page_content_2'][0]['settings']['_cssGlobalClasses'] + ); + $this->assertSame( + 'padding_c unrelated-class', + $GLOBALS['cf_test_post_meta'][10]['_bricks_page_content_2'][0]['settings']['_cssClasses'] + ); + $this->assertSame( + 'user-class', + $GLOBALS['cf_test_post_meta'][10]['_bricks_page_settings']['container']['_cssGlobalClasses'] + ); + $this->assertSame( + 'padding_c', + $GLOBALS['cf_test_post_meta'][10]['_bricks_page_settings']['container']['customValue'] + ); + $this->assertSame( + array( 'padding_c' ), + $GLOBALS['cf_test_post_meta'][10]['unrelated_meta']['_cssGlobalClasses'] + ); + + $state_after_removal = array( + 'options' => $GLOBALS['cf_test_options'], + 'post_meta' => $GLOBALS['cf_test_post_meta'], + 'mutations' => $GLOBALS['cf_test_mutations'], + ); + + $functions->refresh_selectors( array() ); + + $this->assertSame( $state_after_removal['options'], $GLOBALS['cf_test_options'] ); + $this->assertSame( $state_after_removal['post_meta'], $GLOBALS['cf_test_post_meta'] ); + $this->assertSame( $state_after_removal['mutations'], $GLOBALS['cf_test_mutations'] ); + } + + public function testEmptyInputCannotCreateBlankCoreClass(): void { + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_CLASSES_OPTION ] = array(); + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_LOCKED_CLASSES_OPTION ] = array(); + + $this->createBricksFunctions()->refresh_selectors( array( '', ' ', null, '!!!' ) ); + + $this->assertSame( array(), $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_CLASSES_OPTION ] ); + $this->assertSame( array(), $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_LOCKED_CLASSES_OPTION ] ); + } + + public function testVariableSynchronizationPreservesNonCoreVariablesAndCategories(): void { + $GLOBALS['cf_test_stylesheet'] = ':root{--primary:#ffffff;}'; + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_VARIABLES_OPTION ] = array( + array( + 'id' => 'custom-variable', + 'name' => 'custom-variable', + 'value' => '42px', + 'category' => 'custom-category', + ), + array( + 'id' => 'old-core-variable', + 'name' => 'old-core-variable', + 'value' => 'red', + 'category' => BricksFunctions::CORE_VARIABLE_CATEGORY, + ), + ); + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_VARIABLES_CATEGORY ] = array( + array( + 'id' => 'custom-category', + 'name' => 'Custom', + ), + array( + 'id' => BricksFunctions::CORE_VARIABLE_CATEGORY, + 'name' => 'Core Framework', + ), + ); + + $functions = $this->createBricksFunctions(); + $functions->refresh_variables(); + + $this->assertSame( + array( + array( + 'id' => 'custom-variable', + 'name' => 'custom-variable', + 'value' => '42px', + 'category' => 'custom-category', + ), + array( + 'id' => 'primary', + 'name' => 'primary', + 'value' => '#ffffff', + 'category' => BricksFunctions::CORE_VARIABLE_CATEGORY, + ), + ), + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_VARIABLES_OPTION ] + ); + $this->assertSame( + array( + array( + 'id' => 'custom-category', + 'name' => 'Custom', + ), + array( + 'id' => BricksFunctions::CORE_VARIABLE_CATEGORY, + 'name' => 'Core Framework', + ), + ), + $GLOBALS['cf_test_options'][ BricksFunctions::BRICKS_VARIABLES_CATEGORY ] + ); + + $mutations_after_first_sync = $GLOBALS['cf_test_mutations']; + $functions->refresh_variables(); + + $this->assertSame( $mutations_after_first_sync, $GLOBALS['cf_test_mutations'] ); + } +} diff --git a/packages/wp/src/functions/wpdb.ts b/packages/wp/src/functions/wpdb.ts index c12b870..5716369 100644 --- a/packages/wp/src/functions/wpdb.ts +++ b/packages/wp/src/functions/wpdb.ts @@ -243,7 +243,7 @@ export async function updateColors({ addonEnableArray, colors }: IUpdateColors) } interface IUpdateClasses { - readonly classes: string; + readonly classes: string[]; readonly addonEnableArray: { addon: "bricks" | "oxygen" | "gutenberg" | ""; enabled: boolean; diff --git a/packages/wp/src/hooks/usePush.ts b/packages/wp/src/hooks/usePush.ts index 1a434e5..559d351 100644 --- a/packages/wp/src/hooks/usePush.ts +++ b/packages/wp/src/hooks/usePush.ts @@ -174,13 +174,9 @@ export function usePush() { }); } - if (!classAccumulator.length) { - return; - } - await Promise.allSettled([ updateClasses({ - classes: [...new Set(classAccumulator)].join(","), + classes: [...new Set(classAccumulator)], addonEnableArray, }), ]); @@ -669,13 +665,6 @@ export function usePush() { }), ]); - await withDevTimeLogger(handleClassesRefresh)({ - cssObjects, - classPrefix: latestPreset.classPrefix, - preset: latestPreset, - addonEnableArray, - }); - responses.forEach((response) => { if (response.status === "rejected") { console.warn(response.reason); @@ -683,6 +672,18 @@ export function usePush() { }); const mainTableResponse = responses[1]; + const stylesheetWasPersisted = + mainTableResponse.status === "fulfilled" && mainTableResponse.value.success; + + if (stylesheetWasPersisted) { + await withDevTimeLogger(handleClassesRefresh)({ + cssObjects, + classPrefix: latestPreset.classPrefix, + preset: latestPreset, + addonEnableArray, + }); + } + const t2_total = performance.now(); const bytesSaved = mainTableResponse.status === "fulfilled" ? mainTableResponse.value.bytes_saved : 0; const totalTime = t2_total - t1_total; @@ -692,7 +693,7 @@ export function usePush() { "Saved CSS size": [humanFileSize(bytesSaved)], }); - if (withToast) { + if (withToast && stylesheetWasPersisted) { toast.success( addonEnableArray.some(({ enabled }) => enabled) ? "Successfully pushed and synced." diff --git a/packages/wp/wp/App/Bricks/Functions.php b/packages/wp/wp/App/Bricks/Functions.php index 667a9cd..840b09e 100644 --- a/packages/wp/wp/App/Bricks/Functions.php +++ b/packages/wp/wp/App/Bricks/Functions.php @@ -122,8 +122,24 @@ public function init(): void { * @since 0.0.1 */ public function refresh_selectors( $new_core_selectors_array ): array { - $bricks_classes = get_option( self::BRICKS_CLASSES_OPTION, array() ); - $bricks_locked_classes = get_option( self::BRICKS_LOCKED_CLASSES_OPTION, array() ); + $bricks_classes = get_option( self::BRICKS_CLASSES_OPTION, array() ); + $bricks_locked_classes = get_option( self::BRICKS_LOCKED_CLASSES_OPTION, array() ); + $new_core_selectors_array = is_array( $new_core_selectors_array ) + ? array_values( + array_unique( + array_filter( + array_map( + fn( $class ): string => is_string( $class ) ? trim( $class ) : '', + $new_core_selectors_array + ), + fn( $class ): bool => '' !== $class + ) + ) + ) + : array(); + + $bricks_classes = is_array( $bricks_classes ) ? $bricks_classes : array(); + $bricks_locked_classes = is_array( $bricks_locked_classes ) ? $bricks_locked_classes : array(); $splitted_array = array( 'core' => array(), @@ -131,7 +147,11 @@ public function refresh_selectors( $new_core_selectors_array ): array { ); foreach ( $bricks_classes as $class ) { - if ( str_ends_with( $class['id'], self::CORE_SUFFIX ) ) { + $is_core_class = isset( $class['id'], $class['name'] ) + && str_ends_with( $class['id'], self::CORE_SUFFIX ) + && self::CORE_VARIABLE_CATEGORY === ( $class['category'] ?? '' ); + + if ( $is_core_class ) { $splitted_array['core'][] = $class; continue; } @@ -142,24 +162,46 @@ public function refresh_selectors( $new_core_selectors_array ): array { $core_prev_classes = $splitted_array['core']; $others_prev_classes = $splitted_array['others']; - $core_prev_classes = array_filter( $core_prev_classes, fn( $class ): bool => in_array( $class['name'], $new_core_selectors_array ) ); + $locked_classes_to_remove = array_column( + array_filter( + $core_prev_classes, + fn( $class ): bool => ! in_array( $class['name'], $new_core_selectors_array, true ) + ), + 'id' + ); + $core_prev_classes = array_filter( + $core_prev_classes, + fn( $class ): bool => in_array( $class['name'], $new_core_selectors_array, true ) + ); + $bricks_locked_classes = array_filter( + $bricks_locked_classes, + fn( $class ): bool => ! in_array( $class, $locked_classes_to_remove, true ) + ); - $locked_classes_to_remove = array_column( array_filter( $core_prev_classes, fn( $class ): bool => ! in_array( $class['name'], $new_core_selectors_array ) ), 'id' ); - $bricks_locked_classes = array_filter( $bricks_locked_classes, fn( $class ): bool => ! in_array( $class, $locked_classes_to_remove ) ); + $this->remove_class_references( $locked_classes_to_remove ); foreach ( $new_core_selectors_array as $new_core_selector_array ) { - if ( in_array( $new_core_selector_array, array_column( $core_prev_classes, 'name' ) ) ) { + if ( in_array( $new_core_selector_array, array_column( $core_prev_classes, 'name' ), true ) ) { + continue; + } + + $sanitized_selector = sanitize_title( $new_core_selector_array ); + + if ( '' === $sanitized_selector ) { continue; } - $id = $new_core_selector_array === 'z--1' ? 'z--1_c' : sanitize_title( $new_core_selector_array ) . self::CORE_SUFFIX; - $core_prev_classes[] = array( + $id = $new_core_selector_array === 'z--1' ? 'z--1_c' : $sanitized_selector . self::CORE_SUFFIX; + $core_prev_classes[] = array( 'name' => $new_core_selector_array, 'id' => $id, 'settings' => array(), - 'category' => 'corefrm', + 'category' => self::CORE_VARIABLE_CATEGORY, ); - $bricks_locked_classes[] = $id; + + if ( ! in_array( $id, $bricks_locked_classes, true ) ) { + $bricks_locked_classes[] = $id; + } } $all = array( ...$others_prev_classes, ...$core_prev_classes ); @@ -170,6 +212,101 @@ public function refresh_selectors( $new_core_selectors_array ): array { return array( 'status' => 'success' ); } + /** + * Remove deleted Core Framework class IDs from Bricks element settings. + * + * @param string[] $class_ids Removed Core Framework class IDs. + */ + private function remove_class_references( array $class_ids ): void { + if ( empty( $class_ids ) ) { + return; + } + + $meta_keys = array( + defined( 'BRICKS_DB_PAGE_HEADER' ) ? constant( 'BRICKS_DB_PAGE_HEADER' ) : '_bricks_page_header_2', + defined( 'BRICKS_DB_PAGE_CONTENT' ) ? constant( 'BRICKS_DB_PAGE_CONTENT' ) : '_bricks_page_content_2', + defined( 'BRICKS_DB_PAGE_FOOTER' ) ? constant( 'BRICKS_DB_PAGE_FOOTER' ) : '_bricks_page_footer_2', + defined( 'BRICKS_DB_PAGE_SETTINGS' ) ? constant( 'BRICKS_DB_PAGE_SETTINGS' ) : '_bricks_page_settings', + defined( 'BRICKS_DB_TEMPLATE_SETTINGS' ) ? constant( 'BRICKS_DB_TEMPLATE_SETTINGS' ) : '_bricks_template_settings', + ); + + $meta_query = array( 'relation' => 'OR' ); + foreach ( $meta_keys as $meta_key ) { + $meta_query[] = array( + 'key' => $meta_key, + 'compare' => 'EXISTS', + ); + } + + $post_ids = get_posts( + array( + 'post_type' => array_values( get_post_types( array(), 'names' ) ), + 'post_status' => 'any', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'no_found_rows' => true, + 'suppress_filters' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'meta_query' => $meta_query, + ) + ); + + foreach ( $post_ids as $post_id ) { + foreach ( $meta_keys as $meta_key ) { + $settings = get_post_meta( $post_id, $meta_key, true ); + + if ( ! is_array( $settings ) ) { + continue; + } + + $updated_settings = $this->remove_class_references_from_settings( $settings, $class_ids ); + if ( $updated_settings !== $settings ) { + update_post_meta( $post_id, $meta_key, $updated_settings ); + } + } + } + } + + /** + * Recursively remove class IDs only from Bricks' global-class setting. + * + * @param array $settings Bricks settings or element data. + * @param string[] $class_ids Removed Core Framework class IDs. + * @return array + */ + private function remove_class_references_from_settings( array $settings, array $class_ids ): array { + foreach ( $settings as $key => $value ) { + if ( '_cssGlobalClasses' === $key ) { + if ( is_array( $value ) ) { + $settings[ $key ] = array_values( + array_filter( + $value, + fn( $class_id ): bool => ! in_array( $class_id, $class_ids, true ) + ) + ); + } elseif ( is_string( $value ) ) { + $class_references = preg_split( '/\s+/', trim( $value ) ) ?: array(); + $settings[ $key ] = implode( + ' ', + array_filter( + $class_references, + fn( $class_id ): bool => ! in_array( $class_id, $class_ids, true ) + ) + ); + } + + continue; + } + + if ( is_array( $value ) ) { + $settings[ $key ] = $this->remove_class_references_from_settings( $value, $class_ids ); + } + } + + return $settings; + } + public function refresh_variables(): array { $bricks_variables = get_option( self::BRICKS_VARIABLES_OPTION, array() ); @@ -207,7 +344,7 @@ public function refresh_variables(): array { $current_variables ); - $others_variables = array_filter( $bricks_variables, fn( $variable ): bool => $variable['category'] !== self::CORE_VARIABLE_CATEGORY ); + $others_variables = array_filter( $bricks_variables, fn( $variable ): bool => ( $variable['category'] ?? '' ) !== self::CORE_VARIABLE_CATEGORY ); $all_new_variables = array( ...$others_variables, ...$new_core_variables ); update_option( self::BRICKS_VARIABLES_OPTION, array_values( $all_new_variables ), false ); diff --git a/packages/wp/wp/App/Rest/AllPoints.php b/packages/wp/wp/App/Rest/AllPoints.php index d3694ae..c1eb86b 100644 --- a/packages/wp/wp/App/Rest/AllPoints.php +++ b/packages/wp/wp/App/Rest/AllPoints.php @@ -968,7 +968,23 @@ public function update_classes( $request ) { exit(); } - $new_selectors_array = explode( ',', $classes ) ?? array(); + if ( is_string( $classes ) ) { + $classes = explode( ',', $classes ); + } + + $new_selectors_array = is_array( $classes ) + ? array_values( + array_unique( + array_filter( + array_map( + fn( $class ): string => is_string( $class ) ? trim( $class ) : '', + $classes + ), + fn( $class ): bool => '' !== $class + ) + ) + ) + : array(); $builder_array = array( 'oxygen' => array( 'is_active' => CoreFrameworkOxygen()->is_oxygen(),