diff --git a/lib/block-supports/align.php b/lib/block-supports/align.php deleted file mode 100644 index 762ae13fb71f74..00000000000000 --- a/lib/block-supports/align.php +++ /dev/null @@ -1,56 +0,0 @@ -supports, array( 'align' ), false ); - } - if ( $has_align_support ) { - if ( ! $block_type->attributes ) { - $block_type->attributes = array(); - } - - if ( ! array_key_exists( 'align', $block_type->attributes ) ) { - $block_type->attributes['align'] = array( - 'type' => 'string', - 'enum' => array( 'left', 'center', 'right', 'wide', 'full', '' ), - ); - } - } -} - -/** - * Add CSS classes for block alignment to the incoming attributes array. - * This will be applied to the block markup in the front-end. - * - * @param array $attributes Comprehensive list of attributes to be applied. - * @param array $block_attributes Block attributes. - * @param WP_Block_Type $block_type Block Type. - * - * @return array Block alignment CSS classes and inline styles. - */ -function gutenberg_apply_alignment_support( $attributes, $block_attributes, $block_type ) { - $has_align_support = false; - if ( property_exists( $block_type, 'supports' ) ) { - $has_align_support = gutenberg_experimental_get( $block_type->supports, array( 'align' ), false ); - } - if ( $has_align_support ) { - $has_block_alignment = array_key_exists( 'align', $block_attributes ); - - if ( $has_block_alignment ) { - $attributes['css_classes'][] = sprintf( 'align%s', $block_attributes['align'] ); - } - } - - return $attributes; -} diff --git a/lib/block-supports/custom-classname.php b/lib/block-supports/custom-classname.php deleted file mode 100644 index 59126dd384cffd..00000000000000 --- a/lib/block-supports/custom-classname.php +++ /dev/null @@ -1,54 +0,0 @@ -supports, array( 'customClassName' ), true ); - } - if ( $has_custom_classname_support ) { - if ( ! $block_type->attributes ) { - $block_type->attributes = array(); - } - - if ( ! array_key_exists( 'className', $block_type->attributes ) ) { - $block_type->attributes['className'] = array( - 'type' => 'string', - ); - } - } -} - -/** - * Add the custom classnames to the output. - * - * @param array $attributes Comprehensive list of attributes to be applied. - * @param array $block_attributes Block attributes. - * @param WP_Block_Type $block_type Block Type. - * - * @return array Block CSS classes and inline styles. - */ -function gutenberg_apply_custom_classname_support( $attributes, $block_attributes, $block_type ) { - $has_custom_classname_support = true; - if ( property_exists( $block_type, 'supports' ) ) { - $has_custom_classname_support = gutenberg_experimental_get( $block_type->supports, array( 'customClassName' ), true ); - } - if ( $has_custom_classname_support ) { - $has_custom_classnames = array_key_exists( 'className', $block_attributes ); - - if ( $has_custom_classnames ) { - $attributes['css_classes'][] = $block_attributes['className']; - } - } - - return $attributes; -} diff --git a/lib/block-supports/generated-classname.php b/lib/block-supports/generated-classname.php deleted file mode 100644 index ee194304c05909..00000000000000 --- a/lib/block-supports/generated-classname.php +++ /dev/null @@ -1,57 +0,0 @@ -supports, array( 'className' ), true ); - } - if ( $has_generated_classname_support ) { - $block_classname = gutenberg_get_block_default_classname( $block_type->name ); - - if ( $block_classname ) { - $attributes['css_classes'][] = $block_classname; - } - } - - return $attributes; -} diff --git a/lib/block-supports/index.php b/lib/block-supports/index.php deleted file mode 100644 index 3b0f0d1540771f..00000000000000 --- a/lib/block-supports/index.php +++ /dev/null @@ -1,130 +0,0 @@ -get_all_registered(); - // Ideally we need a hook to extend the block registration - // instead of mutating the block type. - foreach ( $registered_block_types as $block_type ) { - gutenberg_register_alignment_support( $block_type ); - gutenberg_register_colors_support( $block_type ); - gutenberg_register_typography_support( $block_type ); - gutenberg_register_custom_classname_support( $block_type ); - } -} - -add_action( 'init', 'gutenberg_register_block_supports', 21 ); - -/** - * Filters the frontend output of blocks and apply the block support flags transformations. - * - * @param string $block_content rendered block content. - * @param array $block block object. - * @return string filtered block content. - */ -function gutenberg_apply_block_supports( $block_content, $block ) { - if ( ! isset( $block['attrs'] ) ) { - return $block_content; - } - - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); - // If no render_callback, assume styles have been previously handled. - if ( ! $block_type || ! $block_type->render_callback ) { - return $block_content; - } - - $attributes = array(); - $attributes = gutenberg_apply_generated_classname_support( $attributes, $block['attrs'], $block_type ); - $attributes = gutenberg_apply_colors_support( $attributes, $block['attrs'], $block_type ); - $attributes = gutenberg_apply_typography_support( $attributes, $block['attrs'], $block_type ); - $attributes = gutenberg_apply_alignment_support( $attributes, $block['attrs'], $block_type ); - $attributes = gutenberg_apply_custom_classname_support( $attributes, $block['attrs'], $block_type ); - - if ( ! count( $attributes ) ) { - return $block_content; - } - - $dom = new DOMDocument( '1.0', 'utf-8' ); - - // Suppress DOMDocument::loadHTML warnings from polluting the front-end. - $previous = libxml_use_internal_errors( true ); - - // We need to wrap the block in order to handle UTF-8 properly. - $wrapped_block_html = - '
' - . $block_content - . ''; - - $success = $dom->loadHTML( $wrapped_block_html, LIBXML_HTML_NODEFDTD | LIBXML_COMPACT ); - - // Clear errors and reset the use_errors setting. - libxml_clear_errors(); - libxml_use_internal_errors( $previous ); - - if ( ! $success ) { - return $block_content; - } - - // Structure is like ` open/close tags. The open tag needs to be adjusted so we get inside the tag - // and not the tag itself. - $start = strpos( $full_html, '', 0 ) + strlen( '' ); - $end = strpos( $full_html, '', $start ); - return trim( substr( $full_html, $start, $end - $start ) ); -} -add_filter( 'render_block', 'gutenberg_apply_block_supports', 10, 2 ); - -/** - * Normalizes spacing in a string representing a CSS rule - * - * @example - * 'color :red;' becomes 'color:red' - * - * @param string $css_rule_string CSS rule. - * @return string Normalized CSS rule. - */ -function gutenberg_normalize_css_rule( $css_rule_string ) { - return trim( implode( ': ', preg_split( '/\s*:\s*/', $css_rule_string, 2 ) ), ';' ); -} diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php deleted file mode 100644 index ef33c3249ade17..00000000000000 --- a/lib/block-supports/typography.php +++ /dev/null @@ -1,85 +0,0 @@ -supports, array( 'fontSize' ), false ); - } - - $has_line_height_support = false; - if ( property_exists( $block_type, 'supports' ) ) { - $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); - } - - if ( ! $block_type->attributes ) { - $block_type->attributes = array(); - } - - if ( ( $has_font_size_support || $has_line_height_support ) && ! array_key_exists( 'style', $block_type->attributes ) ) { - $block_type->attributes['style'] = array( - 'type' => 'object', - ); - } - - if ( $has_font_size_support && ! array_key_exists( 'fontSize', $block_type->attributes ) ) { - $block_type->attributes['fontSize'] = array( - 'type' => 'string', - ); - } -} - -/** - * Add CSS classes and inline styles for font sizes to the incoming attributes array. - * This will be applied to the block markup in the front-end. - * - * @param array $attributes Comprehensive list of attributes to be applied. - * @param array $block_attributes Block attributes. - * @param WP_Block_Type $block_type Block type. - * - * @return array Font size CSS classes and inline styles. - */ -function gutenberg_apply_typography_support( $attributes, $block_attributes, $block_type ) { - $has_font_size_support = false; - if ( property_exists( $block_type, 'supports' ) ) { - $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); - } - - $has_line_height_support = false; - if ( property_exists( $block_type, 'supports' ) ) { - $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); - } - - // Font Size. - if ( $has_font_size_support ) { - $has_named_font_size = array_key_exists( 'fontSize', $block_attributes ); - $has_custom_font_size = isset( $block_attributes['style']['typography']['fontSize'] ); - - // Apply required class or style. - if ( $has_named_font_size ) { - $attributes['css_classes'][] = sprintf( 'has-%s-font-size', $block_attributes['fontSize'] ); - } elseif ( $has_custom_font_size ) { - $attributes['inline_styles'][] = sprintf( 'font-size: %spx;', $block_attributes['style']['typography']['fontSize'] ); - } - } - - // Line Height. - if ( $has_line_height_support ) { - $has_line_height = isset( $block_attributes['style']['typography']['lineHeight'] ); - // Add the style (no classes for line-height). - if ( $has_line_height ) { - $attributes['inline_styles'][] = sprintf( 'line-height: %s;', $block_attributes['style']['typography']['lineHeight'] ); - } - } - - return $attributes; -} diff --git a/lib/class-wp-block-supports.php b/lib/class-wp-block-supports.php new file mode 100644 index 00000000000000..40fd6c2d53b174 --- /dev/null +++ b/lib/class-wp-block-supports.php @@ -0,0 +1,454 @@ +load_config(); + } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @return WP_Block_Supports The main instance. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * TODO. + */ + public static function init() { + $instance = self::get_instance(); + $instance->register_attributes(); + add_action( 'wp_footer', array( $instance, 'enqueue_styles' ) ); + add_filter( 'render_block', array( $instance, 'apply_block_supports' ), 10, 2 ); + } + + /** + * Enqueues an empty style to which inline styles may be attached. + */ + public function enqueue_styles() { + wp_enqueue_style( 'wp-block-supports' ); + } + + /** + * Generates an array of HTML attributes, such as classes, by applying to + * the given block all of the features that the block supports. + * + * @param array $parsed_block Block as parsed from content. + * @return array Array of HTML attributes. + */ + public function get_new_block_attributes( $parsed_block ) { + if ( + empty( $parsed_block ) || + ! isset( $parsed_block['attrs'] ) || + ! isset( $parsed_block['blockName'] ) + ) { + // TODO: handle as error? + return array(); + } + + $block_attributes = $parsed_block['attrs']; + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( + $parsed_block['blockName'] + ); + + if ( empty( $block_type ) ) { + // TODO: handle as error? + return array(); + } + + $output = array(); + foreach ( $this->config as $feature_name => $feature_config ) { + if ( + ! $this->get_block_support( + $block_type->supports, + $feature_name, + $feature_config['default'] + ) + ) { + continue; + } + $new_attributes = call_user_func( + $feature_config['callback'], + // Pick from $block_attributes according to feature attributes. + array_intersect_key( + $block_attributes, + array_flip( array_keys( $feature_config['attributes'] ) ) + ), + $block_type->name + ); + if ( ! empty( $new_attributes ) ) { + foreach ( $new_attributes as $attribute_name => $attribute_value ) { + if ( empty( $output[ $attribute_name ] ) ) { + $output[ $attribute_name ] = ''; + } + $output[ $attribute_name ] .= " $attribute_value"; + } + } + } + return $output; + } + + /** + * TODO. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Transformed block content. + */ + public function apply_block_supports( $block_content, $block ) { + if ( ! isset( $block['attrs'] ) ) { + return $block_content; + } + + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + // If no render_callback, assume styles have been previously handled. + if ( ! $block_type || ! $block_type->render_callback ) { + return $block_content; + } + + $new_attributes = $this->get_new_block_attributes( $block ); + if ( ! empty( $new_attributes ) ) { + return $this->inject_attributes( $block_content, $new_attributes ); + } + + return $block_content; + } + + /** + * Constructs the configuration for all supported block-supports features. + */ + private function load_config() { + $this->config = array( + 'align' => array( + 'attributes' => array( + 'align' => array( + 'type' => 'string', + 'enum' => array( 'left', 'center', 'right', 'wide', 'full', '' ), + ), + ), + 'callback' => function( $attributes ) { + if ( empty( $attributes['align'] ) ) { + return false; + } + + return array( + 'class' => sprintf( + 'align%s', + $attributes['align'] + ), + ); + }, + 'default' => false, + ), + 'className' => array( + 'attributes' => array(), + 'callback' => function( $attributes, $block_name ) { + $classes = 'HELLO-FROM-className wp-block-' . preg_replace( + '/^core-/', + '', + str_replace( '/', '-', $block_name ) + ); + + /** + * Filters the default block className for server rendered blocks. + * + * @param string $class_name The current applied classname. + * @param string $block_name The block name. + */ + $classes = apply_filters( 'block_default_classname', $classes, $block_name ); + + return array( 'class' => $classes ); + }, + 'default' => true, + ), + 'color.background' => array( + 'attributes' => array( + 'backgroundColor' => array( + 'type' => 'string', + ), + ), + 'callback' => '__return_false', + 'default' => array( false, true ), + ), + 'color.gradients' => array( + 'attributes' => array( + 'gradient' => array( + 'type' => 'string', + ), + ), + 'callback' => '__return_false', + 'default' => array( false, false ), + ), + 'color.link' => array( + 'attributes' => array(), + 'callback' => '__return_false', + 'default' => array( false, false ), + ), + 'color.text' => array( + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + 'textColor' => array( + 'type' => 'string', + ), + ), + 'callback' => function( $attributes ) { + $has_named_text_color = array_key_exists( 'textColor', $attributes ); + $has_custom_text_color = isset( $attributes['style']['color']['text'] ); + $classes = ''; + $styles = ''; + + // Apply required generic class. + if ( $has_custom_text_color || $has_named_text_color ) { + $classes .= ' has-text-color'; + } + // Apply color class or inline style. + if ( $has_named_text_color ) { + $classes .= sprintf( ' has-%s-color', $attributes['textColor'] ); + } elseif ( $has_custom_text_color ) { + $styles = sprintf( + 'color:%s;', + $attributes['style']['color']['text'] + ); + } + return array( + 'class' => $classes, + 'style' => $styles, + ); + }, + 'default' => array( false, true ), + ), + 'customClassName' => array( + 'attributes' => array( + 'className' => array( + 'type' => 'string', + ), + ), + 'callback' => function( $attributes ) { + return array_key_exists( 'className', $attributes ) ? + array( 'class' => $attributes['className'] ) : + false; + }, + 'default' => true, + ), + 'fontSize' => array( + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + 'fontSize' => array( + 'type' => 'string', + ), + ), + 'callback' => function( $attributes, $block_name ) { + die( 42 ); + $has_named_font_size = array_key_exists( 'fontSize', $attributes ); + $has_custom_font_size = isset( $attributes['style']['typography']['fontSize'] ); + + $classes = ''; + $styles = ''; + if ( $has_named_font_size ) { + $classes = sprintf( 'has-%s-font-size', $attributes['fontSize'] ); + } elseif ( $has_custom_font_size ) { + $styles = sprintf( + 'font-size:%spx;', + $attributes['style']['typography']['fontSize'] + ); + } + + return array( + 'class' => $classes, + 'style' => 'color:pink;', //$styles, + ); + }, + 'default' => false, + ), + 'lineHeight' => array( + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'callback' => function( $attributes, $block_name ) { + $has_line_height = isset( $attributes['style']['typography']['lineHeight'] ); + + $classes = ''; + if ( $has_line_height ) { + $generated_class_name = uniqid( 'wp-block-lineheight-' ); + $classes = $generated_class_name; + wp_add_inline_style( + 'wp-block-supports', + sprintf( + '.%s { line-height: %s; }', + $generated_class_name, + $attributes['style']['typography']['lineHeight'] + ) + ); + } + + return array( 'class' => $classes ); + }, + 'default' => false, + ), + ); + } + + /** + * TODO. + * + * @param array $block_supports Supports object. + * @param string $feature_name Feature name. + * @param boolean|array $default Default value if support no found. + * @return any Supports data. + */ + private function get_block_support( $block_supports, $feature_name, $default = false ) { + $path = explode( '.', $feature_name ); + $default = (array) $default; + if ( count( $path ) !== count( $default ) ) { + return false; + } + $partial_path = array(); + foreach ( $path as $i => $subkey ) { + $partial_path[] = $subkey; + $result = gutenberg_experimental_get( + $block_supports, + $partial_path, + $default[ $i ] + ); + if ( ! $result ) { + break; + } + } + return $result; + } + + /** + * TODO. + * + * @param string $input HTML input. + * @param array $attributes Array of HTML attributes to insert at the + * root element. + * @return string HTML with class(es) inserted. + */ + private function inject_attributes( $input, $attributes ) { + $output = $input; + + foreach ( $attributes as $attribute_name => $attribute_value ) { + $close_token = '>'; + $attribute_token = "$attribute_name=\""; + $close_position = strpos( $output, $close_token ); + $attribute_position = strpos( $output, $attribute_token ); + + // Unexpected. Bail. + if ( false === $close_position ) { + return $input; + } + + // If the first HTML element in the string does not contain our + // attribute... + if ( ! $attribute_position || $close_position < $attribute_position ) { + // then inject a new one at the end of the tag. + $output = substr_replace( + $output, + " $attribute_name=\"$attribute_value\">", + $close_position, + strlen( $close_token ) + ); + continue; + } + // Otherwise, overwrite the opening of the attribute in order to + // inject our value. + $output = substr_replace( + $output, + "$attribute_name=\"$attribute_value ", + $attribute_position, + strlen( $attribute_token ) + ); + } + + return $output; + } + + /** + * TODO. + */ + private function register_attributes() { + wp_register_style( 'wp-block-supports', false ); + + $block_registry = WP_Block_Type_Registry::get_instance(); + $registered_block_types = $block_registry->get_all_registered(); + foreach ( $registered_block_types as $block_type ) { + if ( ! property_exists( $block_type, 'supports' ) ) { + continue; + } + if ( ! $block_type->attributes ) { + $block_type->attributes = array(); + } + foreach ( $this->config as $feature_name => $feature_config ) { + if ( + ! $this->get_block_support( + $block_type->supports, + $feature_name, + $feature_config['default'] + ) + ) { + continue; + } + foreach ( $feature_config['attributes'] as $attribute_name => $attribute_schema ) { + if ( ! array_key_exists( $attribute_name, $block_type->attributes ) ) { + $block_type->attributes[ $attribute_name ] = $attribute_schema; + } + } + } + } + } +} + +/** + * Generates a string of classes by applying to the current block being + * rendered all of the features that the block supports. + * + * @return string String of HTML classes. + */ +function gutenberg_block_supports_classes() { + global $current_parsed_block; + $new_attributes = WP_Block_Supports::get_instance()->get_new_block_attributes( $current_parsed_block ); + return empty( $new_attributes ) ? '' : $new_attributes['class']; +} + +add_action( 'init', array( 'WP_Block_Supports', 'init' ), 22 ); diff --git a/lib/compat.php b/lib/compat.php index ee8676f11962b1..66dc56a4edfe19 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -366,11 +366,18 @@ function gutenberg_replace_default_block_categories( $default_categories ) { } add_filter( 'block_categories', 'gutenberg_replace_default_block_categories' ); +global $current_parsed_block; +$current_parsed_block = array( + 'blockName' => null, + 'attributes' => null, +); + /** * Shim that hooks into `pre_render_block` so as to override `render_block` with * a function that assigns block context. * - * This can be removed when plugin support requires WordPress 5.5.0+. + * FIXME: THIS IS NO LONGER ACCURATE: This can be removed when plugin support + * requires WordPress 5.5.0+. * * @see https://core.trac.wordpress.org/ticket/49927 * @see https://core.trac.wordpress.org/changeset/48243 @@ -382,6 +389,7 @@ function gutenberg_replace_default_block_categories( $default_categories ) { */ function gutenberg_render_block_with_assigned_block_context( $pre_render, $parsed_block ) { global $post, $wp_query; + global $current_parsed_block; /* * If a non-null value is provided, a filter has run at an earlier priority @@ -391,6 +399,8 @@ function gutenberg_render_block_with_assigned_block_context( $pre_render, $parse return $pre_render; } + $current_parsed_block = $parsed_block; + $source_block = $parsed_block; /** This filter is documented in src/wp-includes/blocks.php */ diff --git a/lib/load.php b/lib/load.php index 8397ec2562ef1e..b3316d14a7eafd 100644 --- a/lib/load.php +++ b/lib/load.php @@ -117,9 +117,7 @@ function gutenberg_is_experiment_enabled( $name ) { require dirname( __FILE__ ) . '/navigation-page.php'; require dirname( __FILE__ ) . '/experiments-page.php'; require dirname( __FILE__ ) . '/global-styles.php'; -require dirname( __FILE__ ) . '/block-supports/index.php'; -require dirname( __FILE__ ) . '/block-supports/align.php'; -require dirname( __FILE__ ) . '/block-supports/colors.php'; -require dirname( __FILE__ ) . '/block-supports/typography.php'; -require dirname( __FILE__ ) . '/block-supports/custom-classname.php'; -require dirname( __FILE__ ) . '/block-supports/generated-classname.php'; + +if ( ! class_exists( 'WP_Block_Supports' ) ) { + require_once dirname( __FILE__ ) . '/class-wp-block-supports.php'; +} diff --git a/packages/block-library/src/cover/block.json b/packages/block-library/src/cover/block.json index 29f1b17d7bb235..94e2b181870af4 100644 --- a/packages/block-library/src/cover/block.json +++ b/packages/block-library/src/cover/block.json @@ -53,6 +53,9 @@ "supports": { "anchor": true, "align": true, + "color": { + "text": true + }, "html": false, "spacing": { "padding": true diff --git a/packages/block-library/src/cover/index.php b/packages/block-library/src/cover/index.php index 6042ef8c274ce3..5d2baa524db49f 100644 --- a/packages/block-library/src/cover/index.php +++ b/packages/block-library/src/cover/index.php @@ -18,7 +18,14 @@ function render_block_core_cover( $attributes, $content ) { return str_replace( 'autoplay muted', 'autoplay muted playsinline', $content ); } - return $content; + // TODO: this is just a proof of concept, not a proper solution. + $class = gutenberg_block_supports_classes(); + return preg_replace( + '/class="/', + "class=\"$class ", + $content, + 1 + ); } /** diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 8486b05b6bab3f..a6e54e03f4972a 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -171,6 +171,8 @@ function render_block_core_latest_posts( $attributes ) { $class .= ' has-author'; } + $class .= ' begin-explicitly-added ' . gutenberg_block_supports_classes() . ' end-explicitly-added'; + return sprintf( '