diff --git a/inc/class-base-css.php b/inc/class-base-css.php index 0739628ba..aa16c163e 100644 --- a/inc/class-base-css.php +++ b/inc/class-base-css.php @@ -683,10 +683,10 @@ public function get_animation_classes( $blocks ) { ); foreach ( $blocks as $block ) { - if ( isset( $block['attrs']['className'] ) && ! empty( $block['attrs']['className'] ) ) { - if ( preg_match( '/\banimated\b/', $block['attrs']['className'] ) ) { - $classes = array_merge( $classes, explode( ' ', trim( $block['attrs']['className'] ) ) ); - } + $block_classes = Registration::get_class_name( isset( $block['attrs'] ) ? $block['attrs'] : array() ); + + if ( ! empty( $block_classes ) && preg_match( '/\banimated\b/', $block_classes ) ) { + $classes = array_merge( $classes, explode( ' ', trim( $block_classes ) ) ); } if ( isset( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { diff --git a/inc/class-registration.php b/inc/class-registration.php index 5df6f6901..0c00b8546 100644 --- a/inc/class-registration.php +++ b/inc/class-registration.php @@ -97,6 +97,38 @@ public static function get_editor_global_defaults() { return is_object( $defaults ) ? $defaults : new \stdClass(); } + /** + * Get the `className` attribute of a block as a string. + * + * @param mixed $attributes Block attributes. + * @return string + */ + public static function get_class_name( $attributes ) { + if ( ! is_array( $attributes ) || ! isset( $attributes['className'] ) ) { + return ''; + } + + $class_name = $attributes['className']; + + if ( is_array( $class_name ) ) { + // Flatten nested arrays and drop anything that is not printable. + $flat = array(); + + array_walk_recursive( + $class_name, + function ( $value ) use ( &$flat ) { + if ( is_scalar( $value ) ) { + $flat[] = (string) $value; + } + } + ); + + return implode( ' ', $flat ); + } + + return is_scalar( $class_name ) ? (string) $class_name : ''; + } + /** * Initialize the class */ @@ -1076,7 +1108,7 @@ public function subscribe_fa( $block_content, $block ) { $has_navigation_block = \WP_Block_Type_Registry::get_instance()->is_registered( 'core/navigation' ); if ( $has_navigation_block && ( 'core/navigation-link' === $block['blockName'] || 'core/navigation-submenu' === $block['blockName'] ) ) { - if ( isset( $block['attrs']['className'] ) && strpos( $block['attrs']['className'], 'fa-' ) !== false ) { + if ( strpos( self::get_class_name( isset( $block['attrs'] ) ? $block['attrs'] : array() ), 'fa-' ) !== false ) { self::$is_fa_loaded = true; // See the src/blocks/plugins/menu-icons/inline.css file for where this comes from. @@ -1118,7 +1150,7 @@ public function load_sticky( $block_content, $block ) { return $block_content; } - if ( isset( $block['attrs']['className'] ) && false !== strpos( $block['attrs']['className'], 'o-sticky' ) ) { + if ( false !== strpos( self::get_class_name( isset( $block['attrs'] ) ? $block['attrs'] : array() ), 'o-sticky' ) ) { $asset_file = include OTTER_BLOCKS_PATH . '/build/blocks/sticky.asset.php'; wp_enqueue_script( 'otter-sticky', diff --git a/inc/css/blocks/class-posts-css.php b/inc/css/blocks/class-posts-css.php index 19da67532..5e09dab5c 100644 --- a/inc/css/blocks/class-posts-css.php +++ b/inc/css/blocks/class-posts-css.php @@ -8,6 +8,7 @@ namespace ThemeIsle\GutenbergBlocks\CSS\Blocks; use ThemeIsle\GutenbergBlocks\Base_CSS; +use ThemeIsle\GutenbergBlocks\Registration; use ThemeIsle\GutenbergBlocks\CSS\CSS_Utility; @@ -429,8 +430,7 @@ function ( $position, $css_variable ) { return $value[ $position ]; }, 'condition' => function ( $attrs ) { - // @phpstan-ignore-next-line - return isset( $attrs['className'] ) && strpos( $attrs['className'], 'is-style-tiled' ) !== false; + return strpos( Registration::get_class_name( $attrs ), 'is-style-tiled' ) !== false; }, ); }, diff --git a/inc/render/class-posts-grid-block.php b/inc/render/class-posts-grid-block.php index da6b61d48..4e1afe4f1 100644 --- a/inc/render/class-posts-grid-block.php +++ b/inc/render/class-posts-grid-block.php @@ -7,6 +7,8 @@ namespace ThemeIsle\GutenbergBlocks\Render; +use ThemeIsle\GutenbergBlocks\Registration; + /** * Class Posts_Grid_Block */ @@ -26,7 +28,7 @@ public function render( $attributes ) { add_filter( 'wp_img_tag_add_auto_sizes', '__return_false' ); $has_pagination = isset( $attributes['hasPagination'] ) && $attributes['hasPagination']; $page_number = 1; - $is_tiled = isset( $attributes['className'] ) && false !== strpos( $attributes['className'], 'is-style-tiled' ); + $is_tiled = false !== strpos( Registration::get_class_name( $attributes ), 'is-style-tiled' ); if ( $has_pagination ) { if ( ! empty( get_query_var( 'page' ) ) || ! empty( get_query_var( 'paged' ) ) ) { @@ -301,7 +303,7 @@ protected function render_featured_post( $post, $attributes ) { $image_alt = get_post_meta( $thumb_id, '_wp_attachment_image_alt', true ); $style = ''; $image_url = wp_get_attachment_image_src( $thumb_id, $size ); - $is_tiled = isset( $attributes['className'] ) && false !== strpos( $attributes['className'], 'is-style-tiled' ); + $is_tiled = false !== strpos( Registration::get_class_name( $attributes ), 'is-style-tiled' ); if ( ! $image_alt ) { $image_alt = get_the_title( $id ); diff --git a/plugins/otter-pro/inc/render/class-modal-block.php b/plugins/otter-pro/inc/render/class-modal-block.php index 609337978..5fac80cf8 100644 --- a/plugins/otter-pro/inc/render/class-modal-block.php +++ b/plugins/otter-pro/inc/render/class-modal-block.php @@ -8,6 +8,7 @@ namespace ThemeIsle\OtterPro\Render; use ThemeIsle\OtterPro\Plugins\License; +use ThemeIsle\GutenbergBlocks\Registration; /** * Class Modal_CSS. @@ -45,8 +46,10 @@ public function render( $attributes, $content, $block ) { $classes = array( 'wp-block-themeisle-blocks-modal', 'is-active', 'is-front' ); - if ( ! empty( $attributes['className'] ) ) { - $classes[] = esc_attr( $attributes['className'] ); + $class_name = Registration::get_class_name( $attributes ); + + if ( ! empty( $class_name ) ) { + $classes[] = esc_attr( $class_name ); } if ( ! empty( $attributes['closeButtonType'] ) && 'outside' === $attributes['closeButtonType'] ) { diff --git a/tests/php/foreign-sabberworm-sandbox.php b/tests/php/foreign-sabberworm-sandbox.php index 21c32becd..a3c0dde6e 100644 --- a/tests/php/foreign-sabberworm-sandbox.php +++ b/tests/php/foreign-sabberworm-sandbox.php @@ -75,6 +75,8 @@ function ( $class ) { } ); + // Base_CSS reads block class names through Registration::get_class_name(). + require OTTER_BLOCKS_PATH . '/inc/class-registration.php'; require OTTER_BLOCKS_PATH . '/inc/class-base-css.php'; $base = new \ThemeIsle\GutenbergBlocks\Base_CSS(); diff --git a/tests/test-block-class-name.php b/tests/test-block-class-name.php new file mode 100644 index 000000000..0e4333f4c --- /dev/null +++ b/tests/test-block-class-name.php @@ -0,0 +1,156 @@ + + */ + private $saved_flags = array(); + + /** + * Set up test environment. + */ + public function set_up() { + parent::set_up(); + + $this->saved_flags = array( + 'is_fa_loaded' => Registration::$is_fa_loaded, + 'sticky' => Registration::$scripts_loaded['sticky'], + ); + } + + /** + * Tear down test environment. + */ + public function tear_down() { + Registration::$is_fa_loaded = $this->saved_flags['is_fa_loaded']; + Registration::$scripts_loaded['sticky'] = $this->saved_flags['sticky']; + + wp_dequeue_script( 'otter-sticky' ); + wp_deregister_script( 'otter-sticky' ); + + parent::tear_down(); + } + + /** + * A string `className` is returned untouched. + */ + public function test_get_class_name_returns_string_attribute() { + $this->assertSame( 'is-style-tiled o-sticky', Registration::get_class_name( array( 'className' => 'is-style-tiled o-sticky' ) ) ); + } + + /** + * An array `className` is flattened to a space separated list. + */ + public function test_get_class_name_flattens_array_attribute() { + $this->assertSame( + 'fa-solid fa-star extra', + Registration::get_class_name( array( 'className' => array( 'fa-solid', array( 'fa-star' ), 'extra' ) ) ) + ); + } + + /** + * Missing, empty and non-printable values fall back to an empty string. + */ + public function test_get_class_name_returns_empty_string_for_unusable_values() { + $this->assertSame( '', Registration::get_class_name( array() ) ); + $this->assertSame( '', Registration::get_class_name( 'not-an-array' ) ); + $this->assertSame( '', Registration::get_class_name( array( 'className' => array() ) ) ); + $this->assertSame( '', Registration::get_class_name( array( 'className' => new stdClass() ) ) ); + $this->assertSame( '', Registration::get_class_name( array( 'className' => array( new stdClass() ) ) ) ); + } + + /** + * The Font Awesome subscriber must read an array `className` without fataling. + */ + public function test_subscribe_fa_handles_array_class_name() { + Registration::$is_fa_loaded = false; + + $block = array( + 'blockName' => 'core/navigation-link', + 'attrs' => array( 'className' => array( 'fa-solid', 'fa-star' ) ), + ); + + $this->assertSame( 'content', Registration::instance()->subscribe_fa( 'content', $block ) ); + + if ( \WP_Block_Type_Registry::get_instance()->is_registered( 'core/navigation' ) ) { + $this->assertTrue( Registration::$is_fa_loaded, 'The array class list contains fa-, so FA must be flagged as needed.' ); + } + } + + /** + * The sticky subscriber must read an array `className` without fataling. + */ + public function test_load_sticky_handles_array_class_name() { + Registration::$scripts_loaded['sticky'] = false; + + $block = array( + 'blockName' => 'core/group', + 'attrs' => array( 'className' => array( 'o-sticky', 'o-sticky-pos-top' ) ), + ); + + $this->assertSame( 'content', Registration::instance()->load_sticky( 'content', $block ) ); + $this->assertTrue( wp_script_is( 'otter-sticky', 'enqueued' ) ); + } + + /** + * Animation class collection must read an array `className` without fataling. + */ + public function test_get_animation_classes_handles_array_class_name() { + $base_css = new Base_CSS(); + + $blocks = array( + array( + 'blockName' => 'core/paragraph', + 'attrs' => array( 'className' => array( 'animated', 'fadeIn' ) ), + ), + ); + + $this->assertSame( array( 'animated', 'fadeIn' ), array_values( $base_css->get_animation_classes( $blocks ) ) ); + } + + /** + * The posts block must render with an array `className` instead of fataling. + */ + public function test_posts_grid_block_renders_with_array_class_name() { + $this->factory()->post->create( array( 'post_title' => 'Otter array className' ) ); + + WP_Block_Supports::init(); + WP_Block_Supports::$block_to_render = array( 'blockName' => 'themeisle-blocks/posts-grid' ); + + $render = new Posts_Grid_Block(); + $output = $render->render( + array( + 'id' => 'wp-block-themeisle-blocks-posts-grid-a94bab18', + 'className' => array( 'is-style-tiled', 'custom' ), + 'columns' => 2, + 'style' => 'grid', + 'postTypes' => array(), + 'template' => array( 'title' ), + 'postsToShow' => 1, + 'order' => 'desc', + 'orderBy' => 'date', + 'offset' => 0, + 'displayTitle' => true, + 'titleTag' => 'h5', + ) + ); + + $this->assertStringContainsString( 'Otter array className', $output ); + } +}