feat: Bootstrap 5 block renderer, widget cards, and sidebar post layout (v1.1.0)
All checks were successful
Create Release Package / PHP Lint (push) Successful in 1m7s
Create Release Package / Build Release (push) Successful in 1m41s

Add BlockRenderer class injecting Bootstrap classes into 8 core block types
(table, button, buttons, image, search, quote, pullquote, list) via per-block
render_block filters using WP_HTML_Tag_Processor.

Add WidgetRenderer class wrapping sidebar widgets in Bootstrap card components
with h4 heading hierarchy via dynamic_sidebar_params and widget_block_content
filters.

Add widget SCSS stylesheet for list styling, search input-group, tag cloud
pills, and card-flush list positioning.

Add single-sidebar.html.twig as the default post template with two-column
Bootstrap layout (col-lg-8 content, col-lg-4 sidebar). Full-width available
via template selection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 23:43:43 +01:00
parent 9904bf508a
commit 3165e60639
16 changed files with 955 additions and 49 deletions

303
inc/Block/BlockRenderer.php Normal file
View File

@@ -0,0 +1,303 @@
<?php
/**
* Block Renderer.
*
* Injects Bootstrap 5 classes into WordPress core block HTML output
* on the frontend. Uses WP_HTML_Tag_Processor for safe HTML manipulation.
*
* @package WPBootstrap\Block
* @since 1.1.0
*/
namespace WPBootstrap\Block;
use WP_HTML_Tag_Processor;
use WP_Block;
class BlockRenderer
{
/**
* Map of WordPress preset color slugs to Bootstrap button variants.
*/
private const COLOR_VARIANTS = [
'primary' => 'primary',
'secondary' => 'secondary',
'success' => 'success',
'danger' => 'danger',
'warning' => 'warning',
'info' => 'info',
'light' => 'light',
'dark' => 'dark',
];
/**
* Register render_block filters for each supported block type.
*/
public function __construct()
{
if ( is_admin() || wp_doing_ajax() ) {
return;
}
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return;
}
$blocks = $this->getBlockHandlers();
/**
* Filters the map of block names to handler method names.
*
* Child themes can remove blocks or add new ones.
*
* @since 1.1.0
*
* @param array $blocks Map of 'core/block-name' => 'methodName'.
*/
$blocks = apply_filters( 'wp_bootstrap_block_renderer_blocks', $blocks );
foreach ( $blocks as $blockName => $method ) {
if ( method_exists( $this, $method ) ) {
add_filter( "render_block_{$blockName}", [ $this, $method ], 10, 3 );
}
}
}
/**
* Get the default map of block names to handler methods.
*
* @return array<string, string>
*/
private function getBlockHandlers(): array
{
return [
'core/table' => 'renderTable',
'core/button' => 'renderButton',
'core/buttons' => 'renderButtons',
'core/image' => 'renderImage',
'core/search' => 'renderSearch',
'core/quote' => 'renderQuote',
'core/pullquote' => 'renderPullquote',
'core/list' => 'renderList',
];
}
/**
* Add Bootstrap table classes.
*
* Injects .table on <table>; adds .table-striped when WP stripes style is active.
*/
public function renderTable( string $content, array $block, ?WP_Block $instance = null ): string
{
if ( empty( $content ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( $content );
if ( ! $processor->next_tag( 'table' ) ) {
return $content;
}
$processor->add_class( 'table' );
$className = $block['attrs']['className'] ?? '';
if ( str_contains( $className, 'is-style-stripes' ) ) {
$processor->add_class( 'table-striped' );
}
return $processor->get_updated_html();
}
/**
* Add Bootstrap button classes.
*
* Injects .btn + color variant on .wp-block-button__link.
* Maps WP preset color slugs to Bootstrap btn-{variant} / btn-outline-{variant}.
*/
public function renderButton( string $content, array $block, ?WP_Block $instance = null ): string
{
if ( empty( $content ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( $content );
if ( ! $processor->next_tag( [ 'class_name' => 'wp-block-button__link' ] ) ) {
return $content;
}
$processor->add_class( 'btn' );
$attrs = $block['attrs'] ?? [];
// Gradient buttons: just .btn, inline style handles the color.
if ( ! empty( $attrs['gradient'] ) ) {
return $processor->get_updated_html();
}
$className = $attrs['className'] ?? '';
$isOutline = str_contains( $className, 'is-style-outline' );
if ( $isOutline ) {
$colorSlug = $attrs['textColor'] ?? 'primary';
$variant = self::COLOR_VARIANTS[ $colorSlug ] ?? 'primary';
$processor->add_class( 'btn-outline-' . $variant );
} else {
$colorSlug = $attrs['backgroundColor'] ?? 'primary';
$variant = self::COLOR_VARIANTS[ $colorSlug ] ?? 'primary';
$processor->add_class( 'btn-' . $variant );
}
return $processor->get_updated_html();
}
/**
* Add Bootstrap flex utilities to button group wrapper.
*/
public function renderButtons( string $content, array $block, ?WP_Block $instance = null ): string
{
if ( empty( $content ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( $content );
if ( ! $processor->next_tag( [ 'class_name' => 'wp-block-buttons' ] ) ) {
return $content;
}
$processor->add_class( 'd-flex' );
$processor->add_class( 'flex-wrap' );
$processor->add_class( 'gap-2' );
return $processor->get_updated_html();
}
/**
* Add .img-fluid to block images.
*/
public function renderImage( string $content, array $block, ?WP_Block $instance = null ): string
{
if ( empty( $content ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( $content );
if ( ! $processor->next_tag( 'img' ) ) {
return $content;
}
$processor->add_class( 'img-fluid' );
return $processor->get_updated_html();
}
/**
* Add Bootstrap form-control and button classes to search block.
*/
public function renderSearch( string $content, array $block, ?WP_Block $instance = null ): string
{
if ( empty( $content ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( $content );
// Add .input-group to the inner wrapper for seamless input + button.
if ( $processor->next_tag( [ 'class_name' => 'wp-block-search__inside-wrapper' ] ) ) {
$processor->add_class( 'input-group' );
}
if ( $processor->next_tag( [ 'class_name' => 'wp-block-search__input' ] ) ) {
$processor->add_class( 'form-control' );
}
if ( $processor->next_tag( [ 'class_name' => 'wp-block-search__button' ] ) ) {
$processor->add_class( 'btn' );
$processor->add_class( 'btn-primary' );
}
return $processor->get_updated_html();
}
/**
* Add Bootstrap blockquote classes to quote block.
*/
public function renderQuote( string $content, array $block, ?WP_Block $instance = null ): string
{
if ( empty( $content ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( $content );
if ( ! $processor->next_tag( 'blockquote' ) ) {
return $content;
}
$processor->add_class( 'blockquote' );
if ( $processor->next_tag( 'cite' ) ) {
$processor->add_class( 'blockquote-footer' );
}
return $processor->get_updated_html();
}
/**
* Add Bootstrap blockquote classes to pullquote block.
*/
public function renderPullquote( string $content, array $block, ?WP_Block $instance = null ): string
{
if ( empty( $content ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( $content );
if ( ! $processor->next_tag( 'blockquote' ) ) {
return $content;
}
$processor->add_class( 'blockquote' );
if ( $processor->next_tag( 'cite' ) ) {
$processor->add_class( 'blockquote-footer' );
}
return $processor->get_updated_html();
}
/**
* Add Bootstrap list-group classes when list-group style is selected.
*
* Only modifies lists with the is-style-list-group block style.
*/
public function renderList( string $content, array $block, ?WP_Block $instance = null ): string
{
if ( empty( $content ) ) {
return $content;
}
$className = $block['attrs']['className'] ?? '';
if ( ! str_contains( $className, 'is-style-list-group' ) ) {
return $content;
}
$processor = new WP_HTML_Tag_Processor( $content );
$listTag = ! empty( $block['attrs']['ordered'] ) ? 'ol' : 'ul';
if ( $processor->next_tag( $listTag ) ) {
$processor->add_class( 'list-group' );
}
while ( $processor->next_tag( 'li' ) ) {
$processor->add_class( 'list-group-item' );
}
return $processor->get_updated_html();
}
}