* @copyright Copyright (c) 2024, Phi Phan
*/
namespace BoldBlocks;
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;
if ( ! class_exists( BlockOverrides::class ) ) :
/**
* Create/edit custom content blocks.
*/
class BlockOverrides extends CoreComponent {
/**
* Define a list of supported core blocks
*
* @var array
*/
protected $supported_core_blocks = [
'core/paragraph' => [ 'content' ],
'core/heading' => [ 'content' ],
'core/button' => [ 'text', 'url', 'linkTarget', 'rel' ],
'core/image' => [ 'id', 'url', 'title', 'alt', 'caption' ],
];
/**
* Define a list of supported block binding
*
* @var array
*/
protected $supported_blocks = [
'boldblocks/svg-block' => [
'content',
'title',
'description',
'linkUrl',
'linkTarget',
'linkRel',
'buttonText',
],
];
/**
* Run main hooks
*
* @return void
*/
public function run() {
// Register block bindings.
add_action( 'init', [ $this, 'register_block_bindings' ] );
// Register block binding contexts for supported blocks.
add_filter( 'register_block_type_args', [ $this, 'register_custom_args' ], 10, 2 );
// Register scripts.
add_action( 'init', [ $this, 'register_scripts' ] );
// Replace the dynamic values.
add_action( 'render_block', [ $this, 'render_block_replace_dynamic_content' ], 10, 3 );
// Render specific blocks.
add_filter( 'cbb_pre_render_dynamic_content', [ $this, 'pre_render_dynamic_content' ], 10, 4 );
// Support more blocks.
add_filter( 'cbb_block_overrides_supported_blocks', [ $this, 'support_additional_blocks' ] );
}
/**
* Register custom block bindings
*
* @return void
*/
public function register_block_bindings() {
if ( function_exists( 'register_block_bindings_source' ) ) {
register_block_bindings_source(
'cbb/block-overrides',
array(
'label' => _x( 'Block Overrides', 'block bindings source', 'content-blocks-builder' ),
'get_value_callback' => [ $this, 'block_bindings_block_overrides_get_value' ],
'uses_context' => array( 'cbb/block-overrides' ),
)
);
}
}
/**
* Get value callback for block binding attributes
*
* @param array $source_args
* @param WP_Block $block_instance
* @param string $attribute_name
* @return mixed
*/
public function block_bindings_block_overrides_get_value( array $source_args, $block_instance, string $attribute_name ) {
if ( empty( $block_instance->attributes['metadata']['name'] ) ) {
return null;
}
$metadata_name = $block_instance->attributes['metadata']['name'];
return _wp_array_get( $block_instance->context, array( 'cbb/block-overrides', $metadata_name, $attribute_name ), null );
}
/**
* Register block overrides context for blocks
*
* @param array $args
* @param string $block_name
* @return void
*/
public function register_custom_args( $args, $block_name ) {
if ( array_key_exists( $block_name, $this->get_supported_blocks() ) ) {
if ( empty( $args['uses_context'] ) ) {
$args['uses_context'] = [];
}
$args['uses_context'][] = 'cbb/block-overrides';
}
return $args;
}
/**
* Register scripts
*
* @return void
*/
public function register_scripts() {
// Custom blocks handle.
$custom_blocks_handler = $this->the_plugin_instance->get_component( CustomBlocks::class );
// Load all custom blocks for registration.
wp_add_inline_script( $custom_blocks_handler->custom_blocks_handle, 'var CBB_SYNCING_SUPPORTED_BLOCKS=' . wp_json_encode( apply_filters( 'cbb_block_overrides_get_all_supported_blocks', array_merge( $this->supported_core_blocks, $this->get_supported_blocks() ) ) ), 'before' );
}
/**
* Replace original content with content from the context
*
* @param string $block_content
* @param array $block
* @param WP_Block $block_instance
* @return void
*/
public function render_block_replace_dynamic_content( $block_content, $block, $block_instance ) {
if ( ! function_exists( 'register_block_bindings_source' ) ) {
return $block_content;
}
// Get dynamic data.
$computed_attributes = $this->process_block_bindings( $block, $block_instance );
if ( empty( $computed_attributes ) || empty( $block_content ) ) {
return $block_content;
}
// Allow render blocks in a different way.
$pre_render = apply_filters( 'cbb_pre_render_dynamic_content', null, $block_content, $computed_attributes, $block_instance );
if ( ! is_null( $pre_render ) ) {
$block_content = $pre_render;
} else {
foreach ( $computed_attributes as $attribute_name => $source_value ) {
$block_content = $this->replace_html( $block_content, $attribute_name, $source_value, $block_instance, $computed_attributes );
}
}
return $block_content;
}
/**
* Render special blocks.
*
* @param null $content
* @param string $block_content
* @param array $computed_attributes
* @param WP_Block $block_instance
* @return null|string
*/
public function pre_render_dynamic_content( $content, $block_content, $computed_attributes, $block_instance ) {
if ( 'boldblocks/svg-block' === $block_instance->name ) {
// Merge with dynamic content.
$attributes = array_merge( $block_instance->attributes, $computed_attributes );
// Get class and style attributes.
$block_reader = new \WP_HTML_Tag_Processor( $block_content );
$class = '';
$inner_class = '';
$style = '';
$inner_style = '';
if ( $block_reader->next_tag() ) {
$class = $block_reader->get_attribute( 'class' );
$style = $block_reader->get_attribute( 'style' );
}
if ( $block_reader->next_tag( [ 'class_name' => 'wp-block-boldblocks-svg-block__inner' ] ) ) {
$inner_class = $block_reader->get_attribute( 'class' );
$inner_style = $block_reader->get_attribute( 'style' );
}
// Don't display empty icon.
if ( empty( $attributes['content'] ) ) {
return '';
}
$svg = $attributes['content'];
$id = \uniqid();
$title = '';
$desc = '';
$texts = '';
if ( ! empty( $attributes['title'] ) ) {
$title = "
{$attributes['title']}";
$texts .= $title;
}
if ( ! empty( $attributes['description'] ) ) {
$desc = "{$attributes['description']}";
$texts .= $desc;
}
// Add title & desc.
$svg = \preg_replace(
'/' . \preg_quote( '>', '/' ) . '/',
'>' . $texts,
$svg,
1
);
$is_link = ! empty( $attributes['linkUrl'] ) || ! empty( $attributes['linkToPost'] );
$inner_tag = $is_link ? 'a' : 'div';
$inner_attributes = '';
$button = '';
if ( $is_link ) {
if ( ! empty( $attributes['linkToPost'] ) ) {
if ( isset( $block_instance->context['postId'] ) ) {
// Get post_id from the context first.
$post_id = $block_instance->context['postId'];
} else {
// Fallback to the current post id.
$post_id = get_queried_object_id();
}
$link_url = get_permalink( $post_id );
} else {
$link_url = $attributes['linkUrl'] ?? '';
}
if ( $link_url ) {
$inner_attributes .= ' href="' . $link_url . '"';
if ( ! empty( $attributes['linkTarget'] ) ) {
$inner_attributes .= ' target="' . $attributes['linkTarget'] . '"';
}
if ( ! empty( $attributes['linkRel'] ) ) {
$inner_attributes .= ' rel="' . $attributes['linkRel'] . '"';
}
if ( ! empty( $attributes['useAsButton'] ) && ! empty( $attributes['buttonText'] ) ) {
$button = '' . $attributes['buttonText'] . '';
}
}
}
$new_content = "<{$inner_tag}{$inner_attributes}>{$svg}{$button}{$inner_tag}>
";
$block_reader = new \WP_HTML_Tag_Processor( $new_content );
$block_reader->next_tag();
$block_reader->set_attribute( 'class', $class );
if ( $style ) {
$block_reader->set_attribute( 'style', $style );
}
$block_reader->next_tag();
$block_reader->set_attribute( 'class', $inner_class );
if ( $inner_style ) {
$block_reader->set_attribute( 'style', $inner_style );
}
if ( $block_reader->next_tag( 'svg' ) ) {
$block_reader->set_attribute( 'role', 'img' );
if ( $texts ) {
if ( $title ) {
$block_reader->set_attribute( 'aria-labelledby', "svg_block_{$id}_title" );
}
if ( $desc ) {
$block_reader->set_attribute( 'aria-describedby', "svg_block_{$id}_desc" );
}
}
}
return $block_reader->get_updated_html();
} elseif ( 'core/embed' === $block_instance->name ) {
// Merge with dynamic content.
$attributes = array_merge( $block_instance->attributes, $computed_attributes );
if ( empty( $attributes['url'] ) ) {
return '';
}
// Get class and style attributes.
$block_reader = new \WP_HTML_Tag_Processor( $block_content );
$class = '';
$style = '';
if ( $block_reader->next_tag( 'figure' ) ) {
$class = $block_reader->get_attribute( 'class' );
if ( $class ) {
$class = explode( ' ', $class );
$class = array_filter(
$class,
function ( $item ) {
if ( 0 === stripos( $item, 'is-type-' ) || 0 === stripos( $item, 'is-provider-' ) || 0 === stripos( $item, 'wp-block-embed-' ) ) {
return false;
}
return $item;
}
);
$class = implode( ' ', $class );
}
$style = $block_reader->get_attribute( 'style' );
if ( $style ) {
$style = " style=\"{$style}\"";
}
}
$classes = [ 'wp-block-embed' ];
if ( $class ) {
$classes[] = $class;
}
if ( $attributes['type'] ?? '' ) {
$classes[] = "is-type-{$attributes['type']}";
}
if ( $attributes['providerNameSlug'] ?? '' ) {
$classes[] = "is-provider-{$attributes['providerNameSlug']}";
$classes[] = "wp-block-embed-{$attributes['providerNameSlug']}";
}
$markup = '' . PHP_EOL;
$markup .= $attributes['url'] . PHP_EOL;
$markup .= '
';
if ( $attributes['caption'] ?? '' ) {
$markup .= '' . $attributes['caption'] . '';
}
$markup .= '';
global $wp_embed;
return $wp_embed->autoembed( $markup );
} elseif ( 'boldblocks/youtube-block' === $block_instance->name ) {
if ( function_exists( 'better_youtube_embed_block_render_block' ) ) {
// Merge with dynamic content.
$attributes = array_merge( $block_instance->attributes, $computed_attributes );
// Get caption from the markup if there is no binding value.
if ( ! isset( $computed_attributes['caption'] ) ) {
$caption = $this->the_plugin_instance->get_component( CustomStyle::class )->get_element_inner_html( $block_content, 'FIGCAPTION' );
if ( $caption ) {
$attributes['caption'] = $caption;
}
}
// Allow inline tags.
$attributes['caption_kses'] = true;
return better_youtube_embed_block_render_block( $attributes );
}
} elseif ( 'boldblocks/counting-number' === $block_instance->name ) {
// Merge with dynamic content.
$attributes = array_merge( $block_instance->attributes, $computed_attributes );
// Get class and style attributes.
$block_reader = new \WP_HTML_Tag_Processor( $block_content );
$class = '';
$style = '';
$value_attrs = [];
if ( $block_reader->next_tag() ) {
$class = $block_reader->get_attribute( 'class' );
$style = $block_reader->get_attribute( 'style' );
}
if ( $block_reader->next_tag( [ 'class_name' => 'value' ] ) ) {
$value_attribute_names = $block_reader->get_attribute_names_with_prefix( '' );
foreach ( $value_attribute_names as $name ) {
$value_attrs[ $name ] = $block_reader->get_attribute( $name );
}
}
$new_content = '';
if ( $attributes['prefix'] ?? false ) {
$new_content .= '' . $attributes['prefix'] . '';
}
$new_content .= '' . ( $attributes['startVal'] ?? '' ) . '';
$new_content .= '' . ( $attributes['value'] ?? '' ) . '';
if ( $attributes['suffix'] ?? false ) {
$new_content .= '' . $attributes['suffix'] . '';
}
$new_content .= '
';
$overridable_attrs = [ 'data-value', 'data-start-val', 'data-prefix', 'data-suffix' ];
$block_reader = new \WP_HTML_Tag_Processor( $new_content );
if ( $block_reader->next_tag( [ 'class_name' => 'value' ] ) ) {
foreach ( $value_attrs as $attribute_key => $attribute_value ) {
if ( in_array( $attribute_key, $overridable_attrs, true ) ) {
switch ( $attribute_key ) {
case 'data-value':
$attribute_value = $attributes['value'] ?? '';
break;
case 'data-start-val':
$attribute_value = $attributes['startVal'] ?? '';
break;
case 'data-prefix':
$attribute_value = $attributes['prefix'] ?? '';
break;
case 'data-suffix':
$attribute_value = $attributes['suffix'] ?? '';
break;
default:
break;
}
}
$block_reader->set_attribute( $attribute_key, $attribute_value );
}
}
return $block_reader->get_updated_html();
}
return $content;
}
/**
* Add more blocks to the supported list
*
* @param array $supported_blocks
* @return array
*/
public function support_additional_blocks( $supported_blocks ) {
$supported_blocks['core/embed'] = [ 'url', 'caption', 'type', 'providerNameSlug', 'responsive', 'previewable' ];
$supported_blocks['core/video'] = [ 'src', 'caption' ];
$supported_blocks['core/audio'] = [ 'src', 'caption' ];
$supported_blocks['core/code'] = [ 'content' ];
$supported_blocks['core/verse'] = [ 'content' ];
$supported_blocks['core/preformatted'] = [ 'content' ];
$supported_blocks['core/pullquote'] = [ 'value', 'citation' ];
$supported_blocks['boldblocks/youtube-block'] = [ 'url', 'caption', 'aspectRatio', 'isMaxResThumbnail', 'customThumbnail' ];
$supported_blocks['boldblocks/counting-number'] = [ 'value', 'startVal', 'prefix', 'suffix' ];
return $supported_blocks;
}
/**
* Build dynamic contents
*
* @param array $parsed_block
* @param WP_Block $block_instance
* @return array
*/
private function process_block_bindings( $parsed_block, $block_instance ) {
$computed_attributes = array();
$block_name = $parsed_block['blockName'] ?? '';
$supported_blocks = $this->get_supported_blocks();
// If the block doesn't have the bindings property, isn't one of the supported
// block types, or the bindings property is not an array, return the block content.
if (
! isset( $supported_blocks[ $block_name ] ) ||
empty( $parsed_block['attrs']['metadata']['bindings'] ) ||
! is_array( $parsed_block['attrs']['metadata']['bindings'] )
) {
return $computed_attributes;
}
foreach ( $parsed_block['attrs']['metadata']['bindings'] as $attribute_name => $block_binding ) {
// If the attribute is not in the supported list, process next attribute.
if ( ! array_key_exists( $block_name, $supported_blocks ) ) {
continue;
}
// If no source is provided, or that source is not registered, process next attribute.
if ( ! isset( $block_binding['source'] ) || ! is_string( $block_binding['source'] ) ) {
continue;
}
$block_binding_source = get_block_bindings_source( $block_binding['source'] );
if ( null === $block_binding_source ) {
continue;
}
$source_args = ! empty( $block_binding['args'] ) && is_array( $block_binding['args'] ) ? $block_binding['args'] : array();
$source_value = $block_binding_source->get_value( $source_args, $block_instance, $attribute_name );
// If the value is not null, process the HTML based on the block and the attribute.
if ( ! is_null( $source_value ) ) {
$computed_attributes[ $attribute_name ] = $source_value;
}
}
return $computed_attributes;
}
/**
* Depending on the block attribute name, replace its value in the HTML based on the value provided.
*
* @param string $block_content Block content.
* @param string $attribute_name The attribute name to replace.
* @param mixed $source_value The value used to replace in the HTML.
* @param WP_Block $block_instance
* @param array $computed_attributes
* @return string The modified block content.
*/
private function replace_html( string $block_content, string $attribute_name, $source_value, $block_instance, $computed_attributes ) {
$block_type = $block_instance->block_type;
$properties = $block_type->attributes[ $attribute_name ];
// Use this filter to alter source, selector for the property.
$properties = apply_filters( 'cbb_get_binding_attribute_properties', $properties, $attribute_name, $block_instance, $computed_attributes );
if ( ! isset( $properties['source'] ) ) {
return $block_content;
}
// Make sure the seletor is valid and don't support multiple selector.
if ( empty( $properties['selector'] ) || false !== stripos( $properties['selector'], ',' ) ) {
return $block_content;
}
// Depending on the attribute source, the processing will be different.
switch ( $properties['source'] ) {
case 'html':
case 'rich-text':
$parsed_selector = $this->parse_selector( $properties['selector'] );
$tag_name = $parsed_selector['tag_name'] ?? '';
$class_name = $parsed_selector['class_name'] ?? '';
// If there is no tag_name, attempt to get it from the source.
if ( ! $tag_name ) {
$block_reader = new \WP_HTML_Tag_Processor( $block_content );
// If any element matches the selector, get the tag name.
if ( $block_reader->next_tag( $parsed_selector['query'] ) ) {
$tag_name = $block_reader->get_tag();
}
}
// Bail if there is no tag name.
if ( ! $tag_name ) {
return $block_content;
}
$pattern = '/(<' . $tag_name . '[^>]*>).*?(<\/' . $tag_name . '>)/is';
return preg_replace( $pattern, '$1' . wp_kses_post( $source_value ) . '$2', $block_content );
case 'attribute':
$amended_content = new \WP_HTML_Tag_Processor( $block_content );
$parsed_selector = $this->parse_selector( $properties['selector'] );
if ( ! $amended_content->next_tag( $parsed_selector['query'] ) ) {
return $block_content;
}
$amended_content->set_attribute( $properties['attribute'], $source_value );
return $amended_content->get_updated_html();
default:
return $block_content;
}
}
/**
* Parse selector
*
* @param string $selector
* @return array
*/
private function parse_selector( $selector ) {
$parsed_attrs = [];
$dot_pos = stripos( $selector, '.' );
if ( false === $dot_pos ) {
$parsed_attrs['tag_name'] = $selector;
} elseif ( $dot_pos > 0 ) {
$parts = explode( '.', $selector );
$parsed_attrs['tag_name'] = $parts[0];
$parsed_attrs['class_name'] = $parts[1];
} else {
$parsed_attrs['class_name'] = trim( $selector, '.' );
}
$query = [];
if ( ! empty( $parsed_attrs['tag_name'] ) ) {
$query['tag_name'] = $parsed_attrs['tag_name'];
}
if ( ! empty( $parsed_attrs['class_name'] ) ) {
$query['class_name'] = $parsed_attrs['class_name'];
}
$parsed_attrs['query'] = $query;
return $parsed_attrs;
}
/**
* Get supported blocks
*
* @return array
*/
private function get_supported_blocks() {
return apply_filters( 'cbb_block_overrides_supported_blocks', $this->supported_blocks );
}
}
endif;