File "block-hooks-20250111004954.php"

Full Path: /home/pumpbmko/public_html/wp-content/plugins/gutenberg/lib/compat/wordpress-6.4/block-hooks-20250111004954.php
File size: 14.2 KB
MIME-type: text/x-php
Charset: utf-8

<?php
/**
 * Block hooks.
 *
 * @package gutenberg
 */

/**
 * Register hooked blocks for automatic insertion, based on their block.json metadata.
 *
 * @param array $settings Array of determined settings for registering a block type.
 * @param array $metadata Metadata provided for registering a block type.
 * @return array Updated settings array.
 */
function gutenberg_add_hooked_blocks( $settings, $metadata ) {
	if ( ! isset( $metadata['blockHooks'] ) ) {
		return $settings;
	}
	$block_hooks = $metadata['blockHooks'];

	/**
	 * Map the camelCased position string from block.json to the snake_cased block type position
	 * used in the hooked block registration function.
	 *
	 * @var array
	 */
	$property_mappings = array(
		'before'     => 'before',
		'after'      => 'after',
		'firstChild' => 'first_child',
		'lastChild'  => 'last_child',
	);

	$inserted_block_name = $metadata['name'];
	foreach ( $block_hooks as $anchor_block_name => $position ) {
		// Avoid infinite recursion (hooking to itself).
		if ( $inserted_block_name === $anchor_block_name ) {
			_doing_it_wrong(
				__METHOD__,
				__( 'Cannot hook block to itself.', 'gutenberg' ),
				'6.4.0'
			);
			continue;
		}

		if ( ! isset( $property_mappings[ $position ] ) ) {
			continue;
		}

		$mapped_position = $property_mappings[ $position ];

		gutenberg_add_hooked_block( $inserted_block_name, $mapped_position, $anchor_block_name );

		$settings['block_hooks'][ $anchor_block_name ] = $mapped_position;
	}

	// Copied from `get_block_editor_server_block_settings()`.
	$fields_to_pick = array(
		'api_version'      => 'apiVersion',
		'title'            => 'title',
		'description'      => 'description',
		'icon'             => 'icon',
		'attributes'       => 'attributes',
		'provides_context' => 'providesContext',
		'uses_context'     => 'usesContext',
		'selectors'        => 'selectors',
		'supports'         => 'supports',
		'category'         => 'category',
		'styles'           => 'styles',
		'textdomain'       => 'textdomain',
		'parent'           => 'parent',
		'ancestor'         => 'ancestor',
		'keywords'         => 'keywords',
		'example'          => 'example',
		'variations'       => 'variations',
		'allowed_blocks'   => 'allowedBlocks',
	);
	// Add `block_hooks` to the list of fields to pick.
	$fields_to_pick['block_hooks'] = 'blockHooks';

	$exposed_settings = array_intersect_key( $settings, $fields_to_pick );

	// TODO: Make work for blocks registered via direct call to gutenberg_add_hooked_block().
	wp_add_inline_script(
		'wp-blocks',
		'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( array( $inserted_block_name => $exposed_settings ) ) . ');'
	);

	return $settings;
}

/**
 * Register a hooked block for automatic insertion into a given block hook.
 *
 * A block hook is specified by a block type and a relative position. The hooked block
 * will be automatically inserted in the given position next to the "anchor" block
 * whenever the latter is encountered. This applies both to the frontend and to the markup
 * returned by the templates and patterns REST API endpoints.
 *
 * This is currently done by filtering parsed blocks as obtained from a block template,
 * template part, or pattern, and injecting the hooked block where applicable.
 *
 * @todo In the long run, we'd likely want some sort of registry for hooked blocks.
 *
 * @param string $hooked_block  The name of the block to insert.
 * @param string $position      The desired position of the hooked block, relative to its anchor block.
 *                              Can be 'before', 'after', 'first_child', or 'last_child'.
 * @param string $anchor_block  The name of the block to insert the hooked block next to.
 * @return void
 */
function gutenberg_add_hooked_block( $hooked_block, $position, $anchor_block ) {
		$hooked_block_array = array(
			'blockName'    => $hooked_block,
			'attrs'        => array(),
			'innerHTML'    => '',
			'innerContent' => array(),
			'innerBlocks'  => array(),
		);

		$inserter = gutenberg_insert_hooked_block( $hooked_block_array, $position, $anchor_block );
		add_filter( 'gutenberg_serialize_block', $inserter, 10, 1 );

		/*
		 * The block-types REST API controller uses objects of the `WP_Block_Type` class, which are
		 * in turn created upon block type registration. However, that class does not contain
		 * a `block_hooks` property (and is not easily extensible), so we have to use a different
		 * mechanism to communicate to the controller which hooked blocks have been registered for
		 * automatic insertion. We're doing so here (i.e. upon block registration), by adding a filter to
		 * the controller's response.
		 */
		$controller_extender = gutenberg_add_block_hooks_field_to_block_type_controller( $hooked_block, $position, $anchor_block );
		add_filter( 'rest_prepare_block_type', $controller_extender, 10, 2 );
}

/**
 * Return a function that auto-inserts a block next to a given "anchor" block.
 *
 * This is a helper function used in the implementation of block hooks.
 * It is not meant for public use.
 *
 * The auto-inserted block can be inserted before or after the anchor block,
 * or as the first or last child of the anchor block.
 *
 * Note that the returned function mutates the automatically inserted block's
 * designated parent block by inserting into the parent's `innerBlocks` array,
 * and by updating the parent's `innerContent` array accordingly.
 *
 * @param array  $inserted_block    The block to insert.
 * @param string $relative_position The position relative to the given block.
 *                                  Can be 'before', 'after', 'first_child', or 'last_child'.
 * @param string $anchor_block_type The automatically inserted block will be inserted next to instances of this block type.
 * @return callable A function that accepts a block's content and returns the content with the inserted block.
 */
function gutenberg_insert_hooked_block( $inserted_block, $relative_position, $anchor_block_type ) {
	return function ( $block ) use ( $inserted_block, $relative_position, $anchor_block_type ) {
		if ( $anchor_block_type === $block['blockName'] ) {
			if ( 'first_child' === $relative_position ) {
				array_unshift( $block['innerBlocks'], $inserted_block );
				// Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
				// when rendering blocks, we also need to prepend a value (`null`, to mark a block
				// location) to that array after HTML content for the inner blocks wrapper.
				$chunk_index = 0;
				for ( $index = $chunk_index; $index < count( $block['innerContent'] ); $index++ ) {
					if ( is_null( $block['innerContent'][ $index ] ) ) {
						$chunk_index = $index;
						break;
					}
				}
				array_splice( $block['innerContent'], $chunk_index, 0, array( null ) );
			} elseif ( 'last_child' === $relative_position ) {
				array_push( $block['innerBlocks'], $inserted_block );
				// Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
				// when rendering blocks, we also need to correctly append a value (`null`, to mark a block
				// location) to that array before the remaining HTML content for the inner blocks wrapper.
				$chunk_index = count( $block['innerContent'] );
				for ( $index = count( $block['innerContent'] ); $index > 0; $index-- ) {
					if ( is_null( $block['innerContent'][ $index - 1 ] ) ) {
						$chunk_index = $index;
						break;
					}
				}
				array_splice( $block['innerContent'], $chunk_index, 0, array( null ) );
			}
			return $block;
		}

		$anchor_block_index = array_search( $anchor_block_type, array_column( $block['innerBlocks'], 'blockName' ), true );
		if ( false !== $anchor_block_index && ( 'after' === $relative_position || 'before' === $relative_position ) ) {
			if ( 'after' === $relative_position ) {
				++$anchor_block_index;
			}
			array_splice( $block['innerBlocks'], $anchor_block_index, 0, array( $inserted_block ) );

			// Find matching `innerContent` chunk index.
			$chunk_index = 0;
			while ( $anchor_block_index > 0 ) {
				if ( ! is_string( $block['innerContent'][ $chunk_index ] ) ) {
					--$anchor_block_index;
				}
				++$chunk_index;
			}
			// Since WP_Block::render() iterates over `inner_content` (rather than `inner_blocks`)
			// when rendering blocks, we also need to insert a value (`null`, to mark a block
			// location) into that array.
			array_splice( $block['innerContent'], $chunk_index, 0, array( null ) );
		}
		return $block;
	};
}

/**
 * Add block hooks information to a block type's controller.
 *
 * @param array  $inserted_block_type The type of block to insert.
 * @param string $position            The position relative to the anchor block.
 *                                    Can be 'before', 'after', 'first_child', or 'last_child'.
 * @param string $anchor_block_type   The hooked block will be inserted next to instances of this block type.
 * @return callable A filter for the `rest_prepare_block_type` hook that adds a `block_hooks` field to the network response.
 */
function gutenberg_add_block_hooks_field_to_block_type_controller( $inserted_block_type, $position, $anchor_block_type ) {
	return function ( $response, $block_type ) use ( $inserted_block_type, $position, $anchor_block_type ) {
		if ( $block_type->name !== $inserted_block_type ) {
			return $response;
		}

		$data = $response->get_data();
		if ( ! isset( $data['block_hooks'] ) ) {
			$data['block_hooks'] = array();
		}
		$data['block_hooks'][ $anchor_block_type ] = $position;
		$response->set_data( $data );
		return $response;
	};
}

/**
 * Parse and reserialize block templates to allow running filters.
 *
 * By parsing a block template's content and then reserializing it
 * via `gutenberg_serialize_blocks()`, we are able to run filters
 * on the parsed blocks. This allows us to modify (parsed) blocks during
 * depth-first traversal already provided by the serialization process,
 * rather than having to do so in a separate pass.
 *
 * @param WP_Block_Template[] $query_result Array of found block templates.
 * @return WP_Block_Template[] Updated array of found block templates.
 */
function gutenberg_parse_and_serialize_block_templates( $query_result ) {
	foreach ( $query_result as $block_template ) {
		if ( empty( $block_template->content ) || 'custom' === $block_template->source ) {
			continue;
		}
		$blocks                  = parse_blocks( $block_template->content );
		$block_template->content = gutenberg_serialize_blocks( $blocks );
	}

	return $query_result;
}

/**
 * Filters the block template object after it has been (potentially) fetched from the theme file.
 *
 * By parsing a block template's content and then reserializing it
 * via `gutenberg_serialize_blocks()`, we are able to run filters
 * on the parsed blocks. This allows us to modify (parsed) blocks during
 * depth-first traversal already provided by the serialization process,
 * rather than having to do so in a separate pass.
 *
 * @param WP_Block_Template|null $block_template The found block template, or null if there is none.
 */
function gutenberg_parse_and_serialize_blocks( $block_template ) {
	if ( empty( $block_template->content ) ) {
		return $block_template;
	}

	$blocks                  = parse_blocks( $block_template->content );
	$block_template->content = gutenberg_serialize_blocks( $blocks );

	return $block_template;
}

/**
 * Register the `block_hooks` field for the block-types REST API controller.
 *
 * @return void
 */
function gutenberg_register_block_hooks_rest_field() {
	register_rest_field(
		'block-type',
		'block_hooks',
		array(
			'schema' => array(
				'description'       => __( 'This block is automatically inserted near any occurrence of the block types used as keys of this map, into a relative position given by the corresponding value.', 'gutenberg' ),
				'type'              => 'object',
				'patternProperties' => array(
					'^[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$' => array(
						'type' => 'string',
						'enum' => array( 'before', 'after', 'first_child', 'last_child' ),
					),
				),
			),
		)
	);
}

// Install the polyfill for Block Hooks only if it isn't already handled in WordPress core.
if ( ! function_exists( 'traverse_and_serialize_blocks' ) ) {
	add_filter( 'block_type_metadata_settings', 'gutenberg_add_hooked_blocks', 10, 2 );
	add_filter( 'get_block_templates', 'gutenberg_parse_and_serialize_block_templates', 10, 1 );
	add_filter( 'get_block_file_template', 'gutenberg_parse_and_serialize_blocks', 10, 1 );
	add_action( 'rest_api_init', 'gutenberg_register_block_hooks_rest_field' );
}

// Helper functions.
// -----------------
// The sole purpose of the following two functions (`gutenberg_serialize_block`
// and `gutenberg_serialize_blocks`), which are otherwise copies of their unprefixed
// counterparts (`serialize_block` and `serialize_blocks`) is to apply a filter
// (also called `gutenberg_serialize_block`) as an entry point for modifications
// to the parsed blocks.

/**
 * Filterable version of `serialize_block()`.
 *
 * This function is identical to `serialize_block()`, except that it applies
 * the `gutenberg_serialize_block` filter to each block before it is serialized.
 *
 * @param array $block The block to be serialized.
 * @return string The serialized block.
 *
 * @see serialize_block()
 */
function gutenberg_serialize_block( $block ) {
	$block_content = '';

	/**
	 * Filters a parsed block before it is serialized.
	 *
	 * @param array $block The block to be serialized.
	 */
	$block = apply_filters( 'gutenberg_serialize_block', $block );

	$index = 0;
	foreach ( $block['innerContent'] as $chunk ) {
		if ( is_string( $chunk ) ) {
			$block_content .= $chunk;
		} else { // Compare to WP_Block::render().
			$inner_block    = $block['innerBlocks'][ $index++ ];
			$block_content .= gutenberg_serialize_block( $inner_block );
		}
	}

	if ( ! is_array( $block['attrs'] ) ) {
		$block['attrs'] = array();
	}

	return get_comment_delimited_block_content(
		$block['blockName'],
		$block['attrs'],
		$block_content
	);
}

/**
 * Filterable version of `serialize_blocks()`.
 *
 * This function is identical to `serialize_blocks()`, except that it applies
 * the `gutenberg_serialize_block` filter to each block before it is serialized.
 *
 * @param array $blocks The blocks to be serialized.
 * @return string[] The serialized blocks.
 *
 * @see serialize_blocks()
 */
function gutenberg_serialize_blocks( $blocks ) {
	return implode( '', array_map( 'gutenberg_serialize_block', $blocks ) );
}